mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Adoption](https://docs.renovatebot.com/merge-confidence/) | [Passing](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | Type | Update | |---|---|---|---|---|---|---|---| | [@napi-rs/cli](https://napi.rs/) ([source](https://redirect.github.com/napi-rs/napi-rs)) | [`3.10.4` → `3.10.5`](https://renovatebot.com/diffs/npm/@napi-rs%2fcli/3.10.4/3.10.5) |  |  |  |  | pnpm.catalog.default | patch | | [napi](https://redirect.github.com/napi-rs/napi-rs) | `3.12.7` → `3.13.0` |  |  |  |  | workspace.dependencies | minor | | [napi-build](https://redirect.github.com/napi-rs/napi-rs) | `2.4.4` → `2.5.0` |  |  |  |  | workspace.dependencies | minor | | [napi-derive](https://redirect.github.com/napi-rs/napi-rs) | `3.6.8` → `3.6.9` |  |  |  |  | workspace.dependencies | patch | --- ### Release Notes <details> <summary>napi-rs/napi-rs (@​napi-rs/cli)</summary> ### [`v3.10.5`](https://redirect.github.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.10.4...@napi-rs/cli@3.10.5) [Compare Source](https://redirect.github.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.10.4...@napi-rs/cli@3.10.5) </details> --- ### Configuration 📅 **Schedule**: (in timezone Asia/Shanghai) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/oxc-project/oxc). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cameron Clark <cameron.clark@hey.com>
1866 lines
67 KiB
JavaScript
1866 lines
67 KiB
JavaScript
import {
|
|
emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,
|
|
emnapiTSFNPlugin as __emnapiTSFNPlugin,
|
|
instantiateNapiModule as __emnapiInstantiateNapiModule,
|
|
WASI as __WASI,
|
|
} from '@napi-rs/wasm-runtime'
|
|
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
|
|
|
|
|
|
export const WASM_MEMORY = Object.freeze({
|
|
initialPages: 1024,
|
|
maximumPages: 65536,
|
|
pageBytes: 65536,
|
|
initialBytes: 1024 * 65536,
|
|
maximumBytes: 65536 * 65536,
|
|
})
|
|
|
|
let __createdInstances = 0
|
|
let __liveInstances = 0
|
|
|
|
/**
|
|
* Counters for instances created by THIS module evaluation, not process-wide:
|
|
* a second bundled copy of this loader keeps its own. Only successfully
|
|
* created instances are counted, and `liveInstances` drops when an instance's
|
|
* `dispose()` resolves.
|
|
*
|
|
* `declaredInitialMemoryBytes` is declared address space, not a host's
|
|
* committed-memory metric; pair it with host telemetry rather than treating it
|
|
* as a quota.
|
|
*/
|
|
export function getDeferredRuntimeStats() {
|
|
return Object.freeze({
|
|
createdInstances: __createdInstances,
|
|
liveInstances: __liveInstances,
|
|
declaredInitialMemoryBytes: WASM_MEMORY.initialBytes,
|
|
})
|
|
}
|
|
|
|
const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor(
|
|
ArrayBuffer.prototype,
|
|
'byteLength',
|
|
).get
|
|
const __memoryBufferGetter = Object.getOwnPropertyDescriptor(
|
|
WebAssembly.Memory.prototype,
|
|
'buffer',
|
|
).get
|
|
// One managed initialization per Memory, success or failure: an attempt that
|
|
// throws may already have written into linear memory, so the bytes are not a
|
|
// clean slate for a second instance. Module-local, like the counters above.
|
|
const __claimedMemories = new WeakSet()
|
|
|
|
function __resolveInstanceMemory(__options) {
|
|
const __provided = __options == null ? undefined : __options.memory
|
|
if (__provided === undefined || __provided === null) {
|
|
// Page counts are handed to the engine unvalidated: it already rejects a
|
|
// negative, over-4GiB or below-maximum value with a precise message, and a
|
|
// second set of bounds here would only drift from it.
|
|
const __allocated = new WebAssembly.Memory({
|
|
initial:
|
|
__options != null && __options.initialMemoryPages !== undefined
|
|
? __options.initialMemoryPages
|
|
: WASM_MEMORY.initialPages,
|
|
maximum:
|
|
__options != null && __options.maximumMemoryPages !== undefined
|
|
? __options.maximumMemoryPages
|
|
: WASM_MEMORY.maximumPages,
|
|
})
|
|
// Claimed like a caller-provided one. The handle publishes it as
|
|
// `instance.memory`, so handing it back to `createInstance()` is as easy
|
|
// as passing your own twice, and it would put two live instances on one
|
|
// linear memory: each initialization rewrites the emnapi/WASI state the
|
|
// other is still running on.
|
|
__claimedMemories.add(__allocated)
|
|
return __allocated
|
|
}
|
|
if (
|
|
__options.initialMemoryPages !== undefined ||
|
|
__options.maximumMemoryPages !== undefined
|
|
) {
|
|
throw new TypeError(
|
|
'Pass either memory or initialMemoryPages/maximumMemoryPages, not both',
|
|
)
|
|
}
|
|
let __buffer
|
|
try {
|
|
// Brand check: the getter throws for anything that is not a genuine
|
|
// WebAssembly.Memory, including a cross-realm look-alike object.
|
|
__buffer = Reflect.apply(__memoryBufferGetter, __provided, [])
|
|
} catch {
|
|
throw new TypeError('memory must be an unshared WebAssembly.Memory')
|
|
}
|
|
try {
|
|
// Throws for a SharedArrayBuffer. This loader has no threads, and shared
|
|
// growth does not detach: external views handed to the addon would
|
|
// silently outlive the bytes they describe.
|
|
Reflect.apply(__arrayBufferByteLengthGetter, __buffer, [])
|
|
} catch {
|
|
throw new TypeError(
|
|
'The deferred loader requires an unshared WebAssembly.Memory',
|
|
)
|
|
}
|
|
// The intrinsic getters above accept a genuine Memory from ANY realm, but
|
|
// the loader's dependencies do not: `WASI.setMemory` in
|
|
// `@napi-rs/wasm-runtime` and emnapi identify a Memory with a realm-local
|
|
// `instanceof`. A Memory built in another realm (a `node:vm` context, a
|
|
// same-origin iframe) would pass every check here and only fail deep inside
|
|
// initialization. Reject it up front, and before the claim below, so the
|
|
// caller keeps it usable in the realm that made it.
|
|
if (!(__provided instanceof WebAssembly.Memory)) {
|
|
throw new TypeError(
|
|
'memory must be a WebAssembly.Memory created in the same realm as this loader',
|
|
)
|
|
}
|
|
if (__claimedMemories.has(__provided)) {
|
|
throw new TypeError(
|
|
'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal',
|
|
)
|
|
}
|
|
// Last step, after every check: a rejected option bag must leave the Memory
|
|
// unclaimed, or a caller could not fix the call and retry with it.
|
|
__claimedMemories.add(__provided)
|
|
return __provided
|
|
}
|
|
|
|
export const __napiBindingTarget = 'wasm32-wasip1'
|
|
function __napiStampBindingTarget(exportsObject, target) {
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
|
|
) {
|
|
if (exportsObject.__napiBindingTarget === target) {
|
|
// Already ours: the root entry aliases the object it loaded, so a WASI
|
|
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
|
|
// is a generated loader — arrives already stamped with this same value.
|
|
return target
|
|
}
|
|
const error = new Error(
|
|
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
|
|
)
|
|
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
|
|
throw error
|
|
}
|
|
if (!Object.isExtensible(exportsObject)) {
|
|
// A `#[napi(module_exports)]` hook may seal or freeze this object
|
|
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
|
|
// never a reason to fail an otherwise successful load, so the stamp is
|
|
// skipped. What a consumer still sees then follows the entry point: the
|
|
// browser and deferred loaders declare `__napiBindingTarget` at module
|
|
// level and go on reporting it, while the CommonJS entries hand back this
|
|
// very object as `module.exports`, so there the value is absent.
|
|
return target
|
|
}
|
|
try {
|
|
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
|
|
// chain, so an inherited accessor could swallow the value or throw and
|
|
// fail an otherwise successful load. The descriptor is what a successful
|
|
// assignment would have produced.
|
|
Object.defineProperty(exportsObject, '__napiBindingTarget', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value: target,
|
|
writable: true,
|
|
})
|
|
} catch {
|
|
// Same rule as the non-extensible skip above: reporting the artifact is
|
|
// metadata, never a reason to fail an otherwise successful load. An exotic
|
|
// object (a Proxy whose defineProperty trap refuses) is skipped, not
|
|
// thrown over.
|
|
}
|
|
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
|
|
// therefore Node's CJS -> ESM named export detection — can see
|
|
// `__napiBindingTarget` statically.
|
|
return target
|
|
}
|
|
|
|
/**
|
|
* Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes.
|
|
* Accepts ONLY a precompiled WebAssembly.Module, or a Promise resolving to one
|
|
* (e.g. `import mod from './transform-react.wasm32-wasip1.wasm'` under a CompiledWasm
|
|
* module rule / wrangler module import). Byte buffers, URLs and Response
|
|
* objects are rejected: they require dynamic Wasm compilation, which
|
|
* Cloudflare Workers disallows.
|
|
*/
|
|
async function __resolveModule(__wasmInput) {
|
|
const __module = await __wasmInput
|
|
// Brand check, not `instanceof`: `WebAssembly.Module.imports` throws unless
|
|
// its argument is a genuine WebAssembly.Module, so prototype-spoofed byte
|
|
// buffers are rejected while cross-realm Module instances are accepted.
|
|
try {
|
|
WebAssembly.Module.imports(__module)
|
|
} catch {
|
|
throw new TypeError(
|
|
"instantiate() and createInstance() expect a precompiled WebAssembly.Module (or a Promise resolving to one), " +
|
|
"e.g. import mod from './transform-react.wasm32-wasip1.wasm' under a CompiledWasm module rule / wrangler module import. " +
|
|
"Byte buffers, URLs and Response objects require dynamic Wasm compilation, which Cloudflare Workers disallows.",
|
|
)
|
|
}
|
|
return __module
|
|
}
|
|
|
|
let __normalizedModules
|
|
|
|
function __rememberNormalizedModule(__module, __normalizedModule) {
|
|
if (!__normalizedModules) {
|
|
__normalizedModules = new WeakMap()
|
|
}
|
|
__normalizedModules.set(__module, __normalizedModule)
|
|
return __normalizedModule
|
|
}
|
|
|
|
async function __normalizeModuleForEmnapi(__module) {
|
|
if (__module instanceof WebAssembly.Module) {
|
|
return __module
|
|
}
|
|
if (__normalizedModules) {
|
|
const __normalizedModule = __normalizedModules.get(__module)
|
|
if (__normalizedModule) {
|
|
return __normalizedModule
|
|
}
|
|
}
|
|
// @emnapi/core currently performs realm-local `instanceof` checks after
|
|
// accepting the module. Structured cloning preserves compiled code without
|
|
// compiling bytes and produces a Module owned by the current realm.
|
|
if (typeof structuredClone === 'function') {
|
|
try {
|
|
const __normalizedModule = structuredClone(__module)
|
|
if (__normalizedModule instanceof WebAssembly.Module) {
|
|
return __rememberNormalizedModule(__module, __normalizedModule)
|
|
}
|
|
} catch {}
|
|
}
|
|
// MessageChannel uses the same structured-clone semantics and covers older
|
|
// browser/Node hosts that expose it but not the structuredClone function.
|
|
if (typeof MessageChannel === 'function') {
|
|
let __channel
|
|
try {
|
|
__channel = new MessageChannel()
|
|
const __normalizedModule = await new Promise((resolve, reject) => {
|
|
__channel.port1.onmessage = (event) => resolve(event.data)
|
|
__channel.port1.onmessageerror = () =>
|
|
reject(new TypeError('Failed to clone WebAssembly.Module'))
|
|
try {
|
|
__channel.port2.postMessage(__module)
|
|
} catch (error) {
|
|
reject(error)
|
|
}
|
|
})
|
|
if (__normalizedModule instanceof WebAssembly.Module) {
|
|
return __rememberNormalizedModule(__module, __normalizedModule)
|
|
}
|
|
} catch {
|
|
} finally {
|
|
try {
|
|
__channel?.port1.close()
|
|
} catch {}
|
|
try {
|
|
__channel?.port2.close()
|
|
} catch {}
|
|
}
|
|
}
|
|
// Last-resort compatibility for genuine, extensible foreign Modules.
|
|
try {
|
|
Object.setPrototypeOf(__module, WebAssembly.Module.prototype)
|
|
} catch {}
|
|
if (__module instanceof WebAssembly.Module) {
|
|
return __module
|
|
}
|
|
throw new TypeError(
|
|
'This host cannot normalize a cross-realm WebAssembly.Module; ' +
|
|
'provide structuredClone or MessageChannel support.',
|
|
)
|
|
}
|
|
|
|
function __wrapEmnapiContextDestroyForSettlement(
|
|
context,
|
|
prepareEnvCleanup,
|
|
isPreparingEnvCleanup,
|
|
) {
|
|
let destroy
|
|
try {
|
|
destroy = context.destroy
|
|
} catch {
|
|
return context
|
|
}
|
|
if (typeof destroy !== 'function') {
|
|
return context
|
|
}
|
|
try {
|
|
Object.defineProperty(context, 'destroy', {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: function () {
|
|
// Reentered from a promise hook that fired inside the barrier: the
|
|
// frame running it destroys as soon as it returns.
|
|
if (isPreparingEnvCleanup?.()) {
|
|
return
|
|
}
|
|
prepareEnvCleanup?.()
|
|
return Reflect.apply(destroy, this, arguments)
|
|
},
|
|
})
|
|
} catch {}
|
|
return context
|
|
}
|
|
|
|
function __captureEmnapiAutoDestroyListener(__process) {
|
|
if (
|
|
!__process ||
|
|
typeof __process.prependListener !== 'function' ||
|
|
typeof __process.removeListener !== 'function'
|
|
) {
|
|
return
|
|
}
|
|
let __autoDestroyListener
|
|
const __captureListener = (__event, __listener) => {
|
|
if (__event === 'beforeExit' && __autoDestroyListener === undefined) {
|
|
__autoDestroyListener = __listener
|
|
}
|
|
}
|
|
try {
|
|
// Run before existing newListener hooks so a hook that registers its own
|
|
// beforeExit listener cannot be mistaken for emnapi's registration.
|
|
__process.prependListener('newListener', __captureListener)
|
|
} catch {
|
|
return
|
|
}
|
|
return () => {
|
|
try {
|
|
__process.removeListener('newListener', __captureListener)
|
|
} catch {}
|
|
if (__autoDestroyListener !== undefined) {
|
|
try {
|
|
__process.removeListener('beforeExit', __autoDestroyListener)
|
|
} catch {}
|
|
}
|
|
}
|
|
}
|
|
|
|
function __attachCleanupError(__error, __cleanupError) {
|
|
try {
|
|
if (
|
|
__error &&
|
|
(typeof __error === 'object' || typeof __error === 'function') &&
|
|
__error.cause === undefined
|
|
) {
|
|
__error.cause = __cleanupError
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch
|
|
// on, so the drain turns below interleave with that dispatch instead of racing
|
|
// ahead of it on a faster queue.
|
|
const __scheduleMacrotask = (function () {
|
|
if (typeof setImmediate === 'function') {
|
|
return function (__callback) {
|
|
setImmediate(__callback)
|
|
}
|
|
}
|
|
const __MessageChannel = globalThis.MessageChannel
|
|
if (typeof __MessageChannel === 'function') {
|
|
return function (__callback) {
|
|
const __channel = new __MessageChannel()
|
|
__channel.port1.onmessage = function () {
|
|
__channel.port1.onmessage = null
|
|
try {
|
|
__channel.port1.close()
|
|
} catch {}
|
|
try {
|
|
__channel.port2.close()
|
|
} catch {}
|
|
__callback()
|
|
}
|
|
__channel.port2.postMessage(null)
|
|
}
|
|
}
|
|
return function (__callback) {
|
|
setTimeout(__callback, 0)
|
|
}
|
|
})()
|
|
|
|
// Turns to wait for while the addon still reports queued settlements. Reaching
|
|
// zero is the only success. A counter still nonzero at this bound rejects the
|
|
// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than
|
|
// destroying the context over a still-queued settlement — the wait stays
|
|
// bounded either way.
|
|
const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128
|
|
// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall
|
|
// back to the number of turns @emnapi/core needs to coalesce and dispatch a
|
|
// call made on this thread (two), plus a margin.
|
|
const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4
|
|
|
|
/**
|
|
* `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the
|
|
* tasks it cancelled: `napi_call_threadsafe_function` appends to the
|
|
* threadsafe-function queue, and @emnapi/core dispatches that queue from a
|
|
* macrotask — two coalescing turns later, even for a call made on this very
|
|
* thread. `Context.destroy()` then runs the threadsafe function's cleanup hook,
|
|
* which drains the queue with a null env and *discards* whatever is still in it.
|
|
*
|
|
* So destroying without yielding first strands exactly the promises the barrier
|
|
* exists to settle. Yield real event-loop turns until the addon reports the
|
|
* queue empty; microtask checkpoints cannot help, no number of them lets a
|
|
* macrotask run.
|
|
*
|
|
* Returns nothing when there is nothing to wait for, which keeps disposal
|
|
* synchronous in the common case.
|
|
*/
|
|
function __drainWasmEnvCleanup(__instance) {
|
|
const __pending = __instance?.exports.napi_wasm_env_cleanup_pending
|
|
const __observable = typeof __pending === 'function'
|
|
if (__observable) {
|
|
let __queued
|
|
try {
|
|
__queued = __pending()
|
|
} catch {
|
|
return
|
|
}
|
|
if (!__queued) {
|
|
return
|
|
}
|
|
}
|
|
const __limit = __observable
|
|
? __WASM_ENV_CLEANUP_DRAIN_TURNS
|
|
: __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS
|
|
return (async () => {
|
|
let __queued = 0
|
|
for (let __turn = 0; __turn < __limit; __turn++) {
|
|
await new Promise((resolve) => {
|
|
__scheduleMacrotask(resolve)
|
|
})
|
|
if (!__observable) {
|
|
continue
|
|
}
|
|
try {
|
|
__queued = __pending()
|
|
} catch {
|
|
return
|
|
}
|
|
if (!__queued) {
|
|
return
|
|
}
|
|
}
|
|
if (!__observable) {
|
|
// Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the
|
|
// contract — there is nothing to consult, so finishing the turns is
|
|
// finishing the drain.
|
|
return
|
|
}
|
|
// The counter is still nonzero after every turn the bound allows. The wait
|
|
// stays bounded — but claiming success here would be indistinguishable from
|
|
// the stranding this drain exists to prevent: disposal would go on to
|
|
// destroy the context, whose cleanup hook discards the still-queued
|
|
// settlement with a null env, and the promise it was for hangs forever.
|
|
// Reject instead, as a retryable cleanup failure: `__prepareForDisposal`
|
|
// leaves its drained flag unset, dispose() (and the instantiation-failure
|
|
// path) decline to destroy, and a later dispose() runs the drain again — by
|
|
// which time the queue has usually been delivered. A counter that is
|
|
// somehow stuck nonzero therefore costs each attempt at most another
|
|
// bounded wait and a rejection, never a stranded promise; the managed
|
|
// beforeExit destroyer still reclaims the context.
|
|
const __drainError = new Error(
|
|
'the wasm environment still reports ' +
|
|
__queued +
|
|
' queued settlement(s) after ' +
|
|
__limit +
|
|
' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again',
|
|
)
|
|
__drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING'
|
|
throw __drainError
|
|
})()
|
|
}
|
|
|
|
// A real, *referenced* timer for the async-work wait below, which polls the
|
|
// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay
|
|
// macrotask there would spin the loop instead of yielding it. Falls back to the
|
|
// macrotask scheduler on a host without timers.
|
|
function __scheduleTimer(__callback, __delay) {
|
|
const __setTimer = globalThis.setTimeout
|
|
if (typeof __setTimer !== 'function') {
|
|
__scheduleMacrotask(__callback)
|
|
return
|
|
}
|
|
try {
|
|
__setTimer(__callback, __delay)
|
|
} catch {
|
|
__scheduleMacrotask(__callback)
|
|
}
|
|
}
|
|
|
|
// A real, referenced timer rather than a zero-delay macrotask, for the same
|
|
// reason the async-work wait uses one: this polls the addon instead of
|
|
// interleaving with the @emnapi/core dispatch, so a zero-delay turn would spin
|
|
// the loop instead of yielding it.
|
|
const __WASM_RUNTIME_WORK_POLL_INTERVAL_MS = 1
|
|
// Arrivals it takes before the poll paces on the host's timers alone. One
|
|
// proves nothing: a timer armed before the host's timers stopped still fires.
|
|
const __WASM_RUNTIME_WORK_POLL_TRUSTED_ARRIVALS = 2
|
|
// How long a parked turn's own timer must already have been due before a
|
|
// backup that runs calls it dropped. Slack, not a deadline: a timer is due
|
|
// against the event loop's clock, which is read once per iteration, while
|
|
// these are `Date.now()` readings taken part-way through one, so the two
|
|
// drift apart by however long the loop has been inside the current iteration.
|
|
const __WASM_RUNTIME_WORK_POLL_STALL_MS = 50
|
|
// How long a backup itself waits. What is left of it after the slack and one
|
|
// interval — 149 ms — has to cover the *two* poll turns that can separate a
|
|
// parked turn from the last backup armed while the host's timers still
|
|
// worked, so the ceiling on a single turn is half of it. See the invariant on
|
|
// `__armWasmRuntimePollStallBackup`.
|
|
const __WASM_RUNTIME_WORK_POLL_BACKUP_MS = 200
|
|
|
|
/**
|
|
* Pacing state for one runtime-work poll.
|
|
*
|
|
* Per poll, never per module: whether the host's timers arrive is not a
|
|
* property of the module. A host can lose its timers between two disposals,
|
|
* and in the deferred shape every instance shares this module — one healthy
|
|
* instance must not disarm the fallback for the next one.
|
|
*/
|
|
function __createWasmRuntimePollPace() {
|
|
return {
|
|
// Timers armed by *this* poll that have actually arrived.
|
|
arrivals: 0,
|
|
// The turn waiting on a timer alone *right now* — undefined whenever no
|
|
// turn is parked — and when that turn's own timer came due.
|
|
settleTurn: undefined,
|
|
turnTimerDueAt: 0,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The backup that ends a turn whose timer is never going to arrive.
|
|
*
|
|
* Once the poll paces on the timer alone it has nothing left to fall back on
|
|
* if the host's timers stop mid-poll: the turn that armed the dead timer is
|
|
* the turn that parks, and a parked poll schedules nothing that could notice.
|
|
* So every turn arms one of these before it yields, and each one compares due
|
|
* times instead of measuring how long the parked turn has been waiting.
|
|
*
|
|
* Invariant: a parked turn is ended by the newest backup that was armed while
|
|
* the host's timers still worked, and a backup ends a turn only when that
|
|
* turn's own timer was already due a whole window before the backup itself.
|
|
* Neither half turns on how far apart the arms happen to fall — what bounds
|
|
* the rescue is how far back that newest live backup is:
|
|
*
|
|
* - *Ends it.* Hosts run timers in due order, so a backup that runs while a
|
|
* turn due a whole window earlier is still parked proves that turn's timer
|
|
* was dropped rather than merely late. That same comparison is what leaves a
|
|
* healthy host alone: there the turn's timer has already run and cleared
|
|
* `settleTurn` before any backup due after it can look.
|
|
* - *Two turns back, not one.* A turn that ended does not prove its own timer
|
|
* arrived: until `…_TRUSTED_ARRIVALS` is reached every turn arms both
|
|
* primitives and the macrotask wins, so such a turn can end with its own
|
|
* timer — and the backup armed one line before it — already dead. The
|
|
* arrival that then flips the poll onto the timer alone can itself be a
|
|
* timer armed before the host's timers died. So the turn that parks can sit
|
|
* two turns past the last live arm, and the newest live backup is due
|
|
* `…_BACKUP_MS` less *two* turn lengths after that turn's own timer.
|
|
* Arming on every turn is what holds it to two, rather than however far back
|
|
* a throttle last let one through.
|
|
* - *Ceiling.* Coverage therefore holds while two consecutive poll turns fit
|
|
* inside `…_BACKUP_MS` less the slack and one interval: 149 ms, so 74 ms
|
|
* per turn (measured: a 74 ms turn is still rescued, a 75 ms one parks).
|
|
* Past that the turn stays parked and the disposal promise never settles.
|
|
* The bound is deliberate: reaching it takes a host that drops timers
|
|
* mid-poll *and* keeps every poll turn busy for more than 74 ms, and neither
|
|
* Node nor WebContainer — the hosts that run the threaded artifact — does
|
|
* the second.
|
|
*
|
|
* The poll then goes back to arming both primitives until two fresh arrivals
|
|
* prove the timers again. A host that stops running the timers it has
|
|
* *already* accepted leaves nothing to fire, and the disposal promise stays
|
|
* pending rather than wedging the thread — the same outcome as a blocking
|
|
* closure that never returns. Unreferenced wherever the host allows it: the
|
|
* poll's own turn timers are what keep the loop alive, never these.
|
|
*/
|
|
function __armWasmRuntimePollStallBackup(__pace) {
|
|
const __setTimer = globalThis.setTimeout
|
|
if (typeof __setTimer !== 'function') {
|
|
// Nothing to back up: `__scheduleTimer` is on the macrotask channel
|
|
// already, and that one cannot park.
|
|
return
|
|
}
|
|
// Read before arming, so this never claims to be due earlier than the timer
|
|
// actually is: a backup ends a turn only when it is provably due after it.
|
|
const __dueAt = Date.now() + __WASM_RUNTIME_WORK_POLL_BACKUP_MS
|
|
let __handle
|
|
try {
|
|
__handle = __setTimer(() => {
|
|
const __settleTurn = __pace.settleTurn
|
|
if (
|
|
!__settleTurn ||
|
|
__pace.turnTimerDueAt > __dueAt - __WASM_RUNTIME_WORK_POLL_STALL_MS
|
|
) {
|
|
// No turn is parked, or the parked one's timer came due too close to
|
|
// this backup to call it dropped — it may still arrive, and the turn
|
|
// that armed it armed a backup due a whole window after *that*.
|
|
return
|
|
}
|
|
__pace.arrivals = 0
|
|
__pace.settleTurn = undefined
|
|
__settleTurn()
|
|
}, __WASM_RUNTIME_WORK_POLL_BACKUP_MS)
|
|
} catch {
|
|
return
|
|
}
|
|
if (__handle && typeof __handle.unref === 'function') {
|
|
try {
|
|
__handle.unref()
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One turn of the runtime-work poll.
|
|
*
|
|
* `__scheduleTimer` falls back to the macrotask scheduler when `setTimeout` is
|
|
* missing or throws, but not when it is present, returns a handle and never
|
|
* fires — fake timers in a test suite that disposes from an `afterEach`, or a
|
|
* host whose timers belong to an IO context that is already gone. That host
|
|
* would park this poll forever, and the poll is unbounded, so nothing would
|
|
* ever call `…_finish`.
|
|
*
|
|
* Arm both primitives until timers armed by this poll have arrived twice, and
|
|
* let whichever lands first end the turn; the loser resolves nothing. A host
|
|
* with working timers therefore pays the double arming for the first turn or
|
|
* two — the macrotask wins the race, but the timers behind it still arrive and
|
|
* are counted — and paces on the timer alone from then on, instead of spinning
|
|
* the loop on a zero-delay queue. A host whose timers never arrive keeps both,
|
|
* and the macrotask is what keeps the poll moving. A host whose timers stop
|
|
* after proving themselves is caught by `__armWasmRuntimePollStallBackup`,
|
|
* which ends the parked turn and puts this poll back on both.
|
|
*/
|
|
function __yieldWasmRuntimePollTurn(__pace) {
|
|
// Armed before the turn yields, and by every turn: what rescues a parked
|
|
// turn has to have been armed while the host's timers still worked, and the
|
|
// turn that parks is the one whose own timer is already dead.
|
|
__armWasmRuntimePollStallBackup(__pace)
|
|
return new Promise((resolve) => {
|
|
let __settled = false
|
|
const __settle = () => {
|
|
if (__settled) {
|
|
return
|
|
}
|
|
__settled = true
|
|
if (__pace.settleTurn === __settle) {
|
|
// Nothing is parked any more: a backup running later must not read a
|
|
// due time this turn has already answered.
|
|
__pace.settleTurn = undefined
|
|
}
|
|
resolve()
|
|
}
|
|
__scheduleTimer(() => {
|
|
__pace.arrivals++
|
|
__settle()
|
|
}, __WASM_RUNTIME_WORK_POLL_INTERVAL_MS)
|
|
// Read next to the arming it describes; see
|
|
// `__armWasmRuntimePollStallBackup` for what the two due times mean.
|
|
const __turnTimerDueAt = Date.now() + __WASM_RUNTIME_WORK_POLL_INTERVAL_MS
|
|
if (__pace.arrivals < __WASM_RUNTIME_WORK_POLL_TRUSTED_ARRIVALS) {
|
|
__scheduleMacrotask(__settle)
|
|
return
|
|
}
|
|
// Paced by the timer alone from here; the backup is what ends this turn if
|
|
// the timer never arrives.
|
|
__pace.settleTurn = __settle
|
|
__pace.turnTimerDueAt = __turnTimerDueAt
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Yield event-loop turns until the addon reports its runtime work finished.
|
|
*
|
|
* The window between `napi_prepare_wasm_env_cleanup_begin` and
|
|
* `…_finish` — the turns are the entire point of splitting the barrier, because
|
|
* on a threaded artifact the work `…_finish` joins can itself be waiting for a
|
|
* JavaScript turn from this very thread.
|
|
*
|
|
* Unbounded, for the same reason the async-work wait above is: giving up means
|
|
* calling `…_finish`, which joins on this thread, and the work it would join is
|
|
* the work waiting for a turn from this thread — so a bound does not end the
|
|
* wait, it only moves it somewhere the JavaScript thread can no longer be
|
|
* reached. A blocking closure that never returns keeps the disposal promise
|
|
* pending instead. The host contract is in `crates/async-runtime/README.md`: a
|
|
* blocking closure must never wait on a JavaScript turn.
|
|
*/
|
|
async function __pollWasmRuntimeWork(__workPending) {
|
|
const __pace = __createWasmRuntimePollPace()
|
|
for (;;) {
|
|
await __yieldWasmRuntimePollTurn(__pace)
|
|
try {
|
|
if (!__workPending()) {
|
|
return
|
|
}
|
|
} catch {
|
|
// A trap is the only way this fails, and a trapped instance has no
|
|
// reachable work left. Stop polling and finish.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait
|
|
// ends when the addon reports zero, so this only decides how promptly disposal
|
|
// notices — not how long it waits.
|
|
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1
|
|
|
|
/**
|
|
* Settles this instance's outstanding `napi_async_work` before the teardown
|
|
* that would strand it.
|
|
*
|
|
* The same hole the eager loaders had, and the same fix: the environment
|
|
* cleanup barrier covers promise settlements queued on the threadsafe-function
|
|
* queue and says nothing about `napi_async_work`, so destroying the context
|
|
* with a work still outstanding leaves its completion callback with nowhere to
|
|
* run and its promise unsettled forever.
|
|
*
|
|
* This flavor is threadless, so `compute` runs on the JavaScript thread inside
|
|
* the macrotask that dequeued it: while this is running, any outstanding work
|
|
* is queued rather than executing, and `napi_wasm_cancel_pending_async_work`
|
|
* can take all of it. The poll is still what decides when the drain is done —
|
|
* cancellation delivers those completions from a later macrotask, not
|
|
* synchronously.
|
|
*
|
|
* Per instance, from that instance's own exports: two instances of this loader
|
|
* have separate registries and must not wait on each other.
|
|
*
|
|
* Both exports are optional, so an addon built against a napi crate that
|
|
* predates them keeps the previous behavior.
|
|
*
|
|
* Returns nothing when there is nothing outstanding, which keeps disposal
|
|
* synchronous in the common case. No deadline: giving up would destroy the
|
|
* environment with a completion callback still owed.
|
|
*/
|
|
function __drainInstanceAsyncWork(__instance) {
|
|
const __exports = __instance?.exports
|
|
const __pending = __exports?.napi_wasm_async_work_pending
|
|
const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work
|
|
if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') {
|
|
return
|
|
}
|
|
|
|
const __readPending = () => {
|
|
try {
|
|
return __pending()
|
|
} catch (__error) {
|
|
// A trap is the only way this call fails: it reads a counter and cannot
|
|
// allocate or call back into JavaScript. A trapped instance can no longer
|
|
// run anything, so its outstanding work is unreachable by definition and
|
|
// best-effort is the honest answer. Anything else means the export is not
|
|
// what this loader thinks it is — a defect worth surfacing.
|
|
if (__error instanceof globalThis.WebAssembly.RuntimeError) {
|
|
return 0
|
|
}
|
|
throw __error
|
|
}
|
|
}
|
|
|
|
if (!__readPending()) {
|
|
return
|
|
}
|
|
try {
|
|
__cancelPending()
|
|
} catch {
|
|
// Cancellation only bounds the wait. Failing it means waiting for the queue
|
|
// to run instead, which the poll below already does.
|
|
}
|
|
if (!__readPending()) {
|
|
return
|
|
}
|
|
|
|
return (async () => {
|
|
while (__readPending()) {
|
|
await new Promise((__resolve) => {
|
|
__scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)
|
|
})
|
|
}
|
|
})()
|
|
}
|
|
|
|
function __createLifecycleReentryError(__operation) {
|
|
const __error = new Error(
|
|
__operation +
|
|
'() cannot run while an emnapi Context.destroy() call is still active; await the original cleanup promise instead.',
|
|
)
|
|
__error.code = 'ERR_NAPI_WASI_LIFECYCLE_REENTRY'
|
|
return __error
|
|
}
|
|
|
|
const __managedEmnapiContextDestroyers = new Set()
|
|
let __managedCleanupProcess
|
|
let __managedBeforeExitListener
|
|
let __managedDestroyPromise
|
|
let __managedDestroyersInFlight
|
|
let __managedBeforeExitRegistrationRetryCount = 0
|
|
let __managedBeforeExitRegistrationRetryScheduled = false
|
|
let __moduleLifecycleDestroyDepth = 0
|
|
|
|
function __removeManagedEmnapiCleanupListeners() {
|
|
const __process = __managedCleanupProcess
|
|
const __beforeExitListener = __managedBeforeExitListener
|
|
__managedCleanupProcess = undefined
|
|
__managedBeforeExitListener = undefined
|
|
__managedBeforeExitRegistrationRetryCount = 0
|
|
if (__process && __beforeExitListener) {
|
|
try {
|
|
__process.removeListener('beforeExit', __beforeExitListener)
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
function __scheduleManagedBeforeExitListenerRegistration() {
|
|
if (
|
|
!__managedCleanupProcess ||
|
|
__managedBeforeExitListener ||
|
|
__managedEmnapiContextDestroyers.size === 0 ||
|
|
__managedBeforeExitRegistrationRetryScheduled ||
|
|
__managedBeforeExitRegistrationRetryCount >= 3
|
|
) {
|
|
return
|
|
}
|
|
__managedBeforeExitRegistrationRetryScheduled = true
|
|
__managedBeforeExitRegistrationRetryCount++
|
|
queueMicrotask(() => {
|
|
__managedBeforeExitRegistrationRetryScheduled = false
|
|
if (
|
|
!__managedCleanupProcess ||
|
|
__managedBeforeExitListener ||
|
|
__managedEmnapiContextDestroyers.size === 0
|
|
) {
|
|
return
|
|
}
|
|
try {
|
|
__registerManagedBeforeExitListener()
|
|
} catch {}
|
|
})
|
|
}
|
|
|
|
function __registerManagedBeforeExitListener() {
|
|
if (!__managedCleanupProcess || __managedBeforeExitListener) {
|
|
return
|
|
}
|
|
try {
|
|
__managedCleanupProcess.once(
|
|
'beforeExit',
|
|
__destroyManagedEmnapiContextsBeforeExit,
|
|
)
|
|
} catch (error) {
|
|
__scheduleManagedBeforeExitListenerRegistration()
|
|
throw error
|
|
}
|
|
__managedBeforeExitListener = __destroyManagedEmnapiContextsBeforeExit
|
|
__managedBeforeExitRegistrationRetryCount = 0
|
|
}
|
|
|
|
function __settleManagedEmnapiContextDestroy(__promise) {
|
|
if (__managedDestroyPromise === __promise) {
|
|
__managedDestroyPromise = undefined
|
|
__managedDestroyersInFlight = undefined
|
|
}
|
|
if (__managedEmnapiContextDestroyers.size === 0) {
|
|
__removeManagedEmnapiCleanupListeners()
|
|
return
|
|
}
|
|
try {
|
|
__registerManagedBeforeExitListener()
|
|
} catch {}
|
|
}
|
|
|
|
function __destroyManagedEmnapiContexts(__excludedDestroyers) {
|
|
if (__managedDestroyPromise) {
|
|
return __managedDestroyPromise
|
|
}
|
|
const __destroyers = Array.from(__managedEmnapiContextDestroyers).filter(
|
|
(__destroy) => !__excludedDestroyers?.has(__destroy),
|
|
)
|
|
if (__destroyers.length === 0) {
|
|
return Promise.resolve()
|
|
}
|
|
let __resolveDestroy
|
|
let __rejectDestroy
|
|
const __promise = new Promise((resolve, reject) => {
|
|
__resolveDestroy = resolve
|
|
__rejectDestroy = reject
|
|
})
|
|
__managedDestroyPromise = __promise
|
|
__managedDestroyersInFlight = new Set(__destroyers)
|
|
void Promise.all(
|
|
__destroyers.map((__destroy) => {
|
|
try {
|
|
return Promise.resolve(__destroy()).then(
|
|
() => ({ failed: false }),
|
|
(error) => ({ failed: true, error }),
|
|
)
|
|
} catch (error) {
|
|
return { failed: true, error }
|
|
}
|
|
}),
|
|
).then((__results) => {
|
|
let __primaryError
|
|
let __failed = false
|
|
for (const __result of __results) {
|
|
if (!__result.failed) {
|
|
continue
|
|
}
|
|
if (!__failed) {
|
|
__failed = true
|
|
__primaryError = __result.error
|
|
} else {
|
|
__attachCleanupError(__primaryError, __result.error)
|
|
}
|
|
}
|
|
if (__failed) {
|
|
__rejectDestroy(__primaryError)
|
|
} else {
|
|
__resolveDestroy()
|
|
}
|
|
}, __rejectDestroy)
|
|
void __promise.then(
|
|
() => {
|
|
__settleManagedEmnapiContextDestroy(__promise)
|
|
},
|
|
() => {
|
|
__settleManagedEmnapiContextDestroy(__promise)
|
|
},
|
|
)
|
|
return __promise
|
|
}
|
|
|
|
async function __drainManagedEmnapiContexts(__excludedDestroyers) {
|
|
const __attemptedDestroyers = new Set(__excludedDestroyers)
|
|
let __primaryError
|
|
let __failed = false
|
|
while (true) {
|
|
let __promise = __managedDestroyPromise
|
|
let __destroyers = __managedDestroyersInFlight
|
|
if (!__promise) {
|
|
__promise = __destroyManagedEmnapiContexts(__attemptedDestroyers)
|
|
__destroyers = __managedDestroyersInFlight
|
|
if (!__destroyers) {
|
|
break
|
|
}
|
|
}
|
|
for (const __destroy of __destroyers) {
|
|
__attemptedDestroyers.add(__destroy)
|
|
}
|
|
try {
|
|
await __promise
|
|
} catch (error) {
|
|
if (!__failed) {
|
|
__failed = true
|
|
__primaryError = error
|
|
} else {
|
|
__attachCleanupError(__primaryError, error)
|
|
}
|
|
}
|
|
}
|
|
if (__failed) {
|
|
throw __primaryError
|
|
}
|
|
}
|
|
|
|
function __destroyManagedEmnapiContextsBeforeExit() {
|
|
// A once listener is consumed before Node invokes it, including when another
|
|
// cleanup batch is still pending.
|
|
__managedBeforeExitListener = undefined
|
|
if (__managedDestroyPromise) {
|
|
return
|
|
}
|
|
void __destroyManagedEmnapiContexts().catch((error) => {
|
|
queueMicrotask(() => {
|
|
throw error
|
|
})
|
|
})
|
|
}
|
|
|
|
function __registerManagedEmnapiContext(__process, __destroy) {
|
|
__managedEmnapiContextDestroyers.add(__destroy)
|
|
if (
|
|
!__managedCleanupProcess &&
|
|
__process &&
|
|
typeof __process.once === 'function' &&
|
|
typeof __process.removeListener === 'function'
|
|
) {
|
|
__managedCleanupProcess = __process
|
|
}
|
|
let __registered = true
|
|
return () => {
|
|
if (!__registered) {
|
|
return
|
|
}
|
|
__registered = false
|
|
__managedEmnapiContextDestroyers.delete(__destroy)
|
|
if (__managedEmnapiContextDestroyers.size === 0) {
|
|
__removeManagedEmnapiCleanupListeners()
|
|
}
|
|
}
|
|
}
|
|
|
|
async function __createManagedEmnapiContext(
|
|
__prepareEnvCleanup,
|
|
__isPreparingEnvCleanup,
|
|
) {
|
|
const __process =
|
|
typeof process === 'object' && process !== null ? process : undefined
|
|
const __finishAutoDestroyCapture =
|
|
__captureEmnapiAutoDestroyListener(__process)
|
|
let __emnapiContext
|
|
let __contextInitializationError
|
|
let __contextInitializationFailed = false
|
|
try {
|
|
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
|
|
__emnapiCreateContext({ autoDestroy: false }),
|
|
__prepareEnvCleanup,
|
|
__isPreparingEnvCleanup,
|
|
)
|
|
// emnapi 2.x still registers an unconditional process.once('beforeExit')
|
|
// auto-destroy listener on Node hosts, and suppressDestroy() only
|
|
// neutralizes its callback without removing it. This loader must stay
|
|
// side-effect free per instance, so the listener is captured and removed;
|
|
// suppressDestroy() remains the safety net when removal is unavailable.
|
|
__emnapiContext.suppressDestroy()
|
|
} catch (error) {
|
|
__contextInitializationError = error
|
|
__contextInitializationFailed = true
|
|
} finally {
|
|
// Remove only the exact emnapi callback captured above.
|
|
__finishAutoDestroyCapture?.()
|
|
}
|
|
if (__emnapiContext === undefined) {
|
|
throw __contextInitializationError
|
|
}
|
|
let __disposed = false
|
|
let __destroying = false
|
|
let __destroyPromise
|
|
let __cleanupRegistered = false
|
|
let __unregisterCleanup
|
|
const __destroy = (__blocksModuleLifecycle = false) => {
|
|
if (__disposed) {
|
|
return
|
|
}
|
|
if (__destroying) {
|
|
throw __createLifecycleReentryError('dispose')
|
|
}
|
|
if (__destroyPromise) {
|
|
return __destroyPromise
|
|
}
|
|
__destroying = true
|
|
let __result
|
|
const __finishDestroyInvocation = () => {
|
|
__destroying = false
|
|
}
|
|
const __finishModuleLifecycleDestroy = () => {
|
|
if (__blocksModuleLifecycle) {
|
|
__blocksModuleLifecycle = false
|
|
__moduleLifecycleDestroyDepth--
|
|
}
|
|
}
|
|
if (__blocksModuleLifecycle) {
|
|
__moduleLifecycleDestroyDepth++
|
|
}
|
|
try {
|
|
// Context.destroy() disables JS before cleanup hooks run, so settle
|
|
// runtime-owned promises while this environment can still call JS.
|
|
__prepareEnvCleanup?.()
|
|
if (__isPreparingEnvCleanup?.()) {
|
|
// Reached from inside the barrier, so `Context.destroy()` below would
|
|
// hit the wrapper's in-flight no-op. Recording that as a completed
|
|
// destroy is what makes the frame that *did* start the barrier skip the
|
|
// real one afterwards, leaving the context retained with its cleanup
|
|
// hooks unrun. Refuse instead: nothing is flagged, the context stays
|
|
// registered for managed beforeExit cleanup, and a later destroy still
|
|
// works. dispose() coalesces reentrancy before it can get here, so this
|
|
// is the backstop for any other caller that manages to. A handshake
|
|
// parked between the two halves of the barrier does not reach here —
|
|
// `__prepareEnvCleanup` closes one rather than skipping it.
|
|
throw __createLifecycleReentryError('dispose')
|
|
}
|
|
__result = __emnapiContext.destroy()
|
|
} catch (error) {
|
|
__finishDestroyInvocation()
|
|
__finishModuleLifecycleDestroy()
|
|
throw error
|
|
}
|
|
let __then
|
|
try {
|
|
if (
|
|
__result !== null &&
|
|
(typeof __result === 'object' || typeof __result === 'function')
|
|
) {
|
|
__then = __result.then
|
|
}
|
|
} catch (error) {
|
|
__finishDestroyInvocation()
|
|
__finishModuleLifecycleDestroy()
|
|
throw error
|
|
}
|
|
if (typeof __then === 'function') {
|
|
let __resolveResult
|
|
let __rejectResult
|
|
const __resultPromise = new Promise((resolve, reject) => {
|
|
__resolveResult = resolve
|
|
__rejectResult = reject
|
|
})
|
|
const __promise = __resultPromise.then(
|
|
(value) => {
|
|
__finishDestroyInvocation()
|
|
__finishModuleLifecycleDestroy()
|
|
__disposed = true
|
|
__destroyPromise = undefined
|
|
__unregisterCleanup?.()
|
|
return value
|
|
},
|
|
(error) => {
|
|
__finishDestroyInvocation()
|
|
__finishModuleLifecycleDestroy()
|
|
__destroyPromise = undefined
|
|
throw error
|
|
},
|
|
)
|
|
__destroyPromise = __promise
|
|
try {
|
|
Reflect.apply(__then, __result, [__resolveResult, __rejectResult])
|
|
} catch (error) {
|
|
__rejectResult(error)
|
|
}
|
|
return __promise
|
|
}
|
|
__finishDestroyInvocation()
|
|
__finishModuleLifecycleDestroy()
|
|
__disposed = true
|
|
__unregisterCleanup?.()
|
|
}
|
|
const __destroyForModuleLifecycle = () => __destroy(true)
|
|
const __registerCleanup = (
|
|
__beforeExitDestroy = __destroyForModuleLifecycle,
|
|
) => {
|
|
if (__cleanupRegistered || __disposed) {
|
|
return
|
|
}
|
|
__unregisterCleanup = __registerManagedEmnapiContext(
|
|
__process,
|
|
__beforeExitDestroy,
|
|
)
|
|
__cleanupRegistered = true
|
|
__registerManagedBeforeExitListener()
|
|
}
|
|
if (__contextInitializationFailed) {
|
|
let __registrationError
|
|
let __registrationFailed = false
|
|
try {
|
|
__registerCleanup()
|
|
} catch (error) {
|
|
__attachCleanupError(__contextInitializationError, error)
|
|
__registrationError = error
|
|
__registrationFailed = true
|
|
}
|
|
try {
|
|
await __destroyForModuleLifecycle()
|
|
} catch (error) {
|
|
__attachCleanupError(
|
|
__registrationFailed
|
|
? __registrationError
|
|
: __contextInitializationError,
|
|
error,
|
|
)
|
|
try {
|
|
__registerManagedBeforeExitListener()
|
|
} catch {}
|
|
}
|
|
throw __contextInitializationError
|
|
}
|
|
return {
|
|
context: __emnapiContext,
|
|
destroy: __destroy,
|
|
destroyForModuleLifecycle: __destroyForModuleLifecycle,
|
|
registerCleanup: __registerCleanup,
|
|
}
|
|
}
|
|
|
|
async function __createInstance(
|
|
__wasmInput,
|
|
__options,
|
|
__beforeExitDestroy,
|
|
__onManagedDestroyer,
|
|
) {
|
|
const __module = await __resolveModule(__wasmInput)
|
|
const __emnapiModule = await __normalizeModuleForEmnapi(__module)
|
|
const __wasi = new __WASI({
|
|
version: 'preview1',
|
|
})
|
|
// The wasm module is linked with `--import-memory`, so a Memory must be
|
|
// provided. It is resolved here in function scope (workerd bans global scope
|
|
// allocation) and is never shared (no threads, no SharedArrayBuffer).
|
|
// Resolve it before the emnapi context so a rejected option bag or a host
|
|
// memory-limit failure cannot leak a context that never reaches
|
|
// instantiation.
|
|
const __wasmMemory = __resolveInstanceMemory(__options)
|
|
let __lifecycleState = 'pending'
|
|
let __destroyEmnapiContext
|
|
let __destroyOwnedContext
|
|
let __destroyManagedOwnedContext
|
|
let __napiInstance
|
|
let __wasmEnvCleanupRan = false
|
|
let __wasmEnvCleanupPrepared = false
|
|
let __wasmEnvCleanupPreparing = false
|
|
// The closer for a barrier parked between `…_begin` and `…_finish`, set only
|
|
// while that window is open. `__wasmEnvCleanupPreparing` cannot tell that
|
|
// apart from a purely synchronous frame, which must not be re-entered and
|
|
// which nothing outside it can finish; this window spans real event-loop
|
|
// turns, so a caller that cannot yield — the managed beforeExit teardown —
|
|
// can land inside it, and can close it. See `__prepareEnvCleanup`.
|
|
let __finishParkedEnvCleanup
|
|
// Raised while a caller that can still yield is driving the barrier, so the
|
|
// queue it leaves behind is expected rather than lost.
|
|
let __wasmEnvCleanupYielding = false
|
|
let __wasmEnvSettlementLossReported = false
|
|
let __wasmEnvCleanupDrained = false
|
|
let __wasmEnvCleanupDrainPromise
|
|
const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing
|
|
/**
|
|
* Say so when the barrier leaves settlements queued and nothing is left that
|
|
* could deliver them.
|
|
*
|
|
* Only `dispose()` and the initialization rollback yield the event-loop turns
|
|
* @emnapi/core needs to dispatch its queue. Every other caller of the barrier
|
|
* destroys in the same turn — a raw `Context.destroy()`, the managed
|
|
* beforeExit teardown — and `Context.destroy()` runs the threadsafe
|
|
* function's cleanup hook, which drains that queue with a null env and
|
|
* discards it. The promises those settlements were for then hang forever,
|
|
* silently.
|
|
*
|
|
* Loud, once, and never throwing: this runs from inside `Context.destroy()`,
|
|
* where throwing would take the whole teardown down with it. Destroying
|
|
* anyway is still the right trade — the queue is already unreachable by then.
|
|
*/
|
|
const __reportUnreachedSettlements = () => {
|
|
if (__wasmEnvCleanupYielding || __wasmEnvSettlementLossReported) {
|
|
return
|
|
}
|
|
const __pending = __napiInstance?.exports.napi_wasm_env_cleanup_pending
|
|
if (typeof __pending !== 'function') {
|
|
return
|
|
}
|
|
let __queued
|
|
try {
|
|
__queued = __pending()
|
|
} catch {
|
|
return
|
|
}
|
|
if (!__queued) {
|
|
return
|
|
}
|
|
__wasmEnvSettlementLossReported = true
|
|
try {
|
|
const __consoleHost = globalThis.console
|
|
if (__consoleHost && typeof __consoleHost.error === 'function') {
|
|
__consoleHost.error(
|
|
'napi-rs: the wasm environment is being destroyed with ' +
|
|
__queued +
|
|
' queued promise settlement(s). Context.destroy() discards them, so those promises never settle. Dispose the instance instead: only dispose() yields the event-loop turns the settlements need.',
|
|
)
|
|
}
|
|
} catch {}
|
|
}
|
|
const __prepareEnvCleanup = () => {
|
|
if (__wasmEnvCleanupPrepared) {
|
|
return
|
|
}
|
|
// A handshake parked between its two halves is one this frame can close,
|
|
// and must: every caller of this is about to destroy the context, and the
|
|
// turns the poll is waiting for will not come. Closing it runs `…_finish`,
|
|
// which is the call that joins, so this degrades to exactly the single
|
|
// call below. Leaving it open destroys the context with the barrier still
|
|
// raised and the runtime never joined.
|
|
const __finishParked = __finishParkedEnvCleanup
|
|
if (__finishParked !== undefined) {
|
|
__finishParked()
|
|
__reportUnreachedSettlements()
|
|
return
|
|
}
|
|
if (__wasmEnvCleanupPreparing) {
|
|
return
|
|
}
|
|
const __prepareWasmEnvCleanup =
|
|
__napiInstance?.exports.napi_prepare_wasm_env_cleanup
|
|
if (typeof __prepareWasmEnvCleanup === 'function') {
|
|
// The addon settles the promises it cancels synchronously, under a
|
|
// non-reentrant lifecycle mutex: anything a promise hook calls from in
|
|
// here must not reach this export again.
|
|
__wasmEnvCleanupPreparing = true
|
|
try {
|
|
__prepareWasmEnvCleanup()
|
|
} finally {
|
|
__wasmEnvCleanupPreparing = false
|
|
}
|
|
__wasmEnvCleanupRan = true
|
|
__reportUnreachedSettlements()
|
|
}
|
|
__wasmEnvCleanupPrepared = true
|
|
}
|
|
/**
|
|
* The barrier for the callers that can yield: `__prepareEnvCleanup` with real
|
|
* event-loop turns in the middle.
|
|
*
|
|
* `napi_prepare_wasm_env_cleanup` waits — it returns only once the addon's
|
|
* async runtime has quiesced, and the work it waits for can itself be waiting
|
|
* for a JavaScript turn from this thread. The addon's two-phase form splits
|
|
* that: `…_begin` stops the runtime without joining and reports whether
|
|
* anything is still live, `napi_wasm_runtime_work_pending` answers that again
|
|
* without blocking, and `…_finish` joins.
|
|
*
|
|
* The poll is unbounded — see `__pollWasmRuntimeWork` — but a caller that
|
|
* cannot yield closes the handshake itself rather than waiting for it, so
|
|
* `…_finish` still runs on every teardown path. Feature-detected like every
|
|
* other export here, and returns nothing whenever the handshake finished
|
|
* without yielding.
|
|
*/
|
|
const __prepareEnvCleanupWithTurns = () => {
|
|
if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) {
|
|
return
|
|
}
|
|
const __exports = __napiInstance?.exports
|
|
const __begin = __exports?.napi_prepare_wasm_env_cleanup_begin
|
|
const __finish = __exports?.napi_prepare_wasm_env_cleanup_finish
|
|
if (typeof __begin !== 'function' || typeof __finish !== 'function') {
|
|
// No split to use. The settlement drain still follows this, so the queue
|
|
// the single call leaves behind is expected rather than lost.
|
|
__wasmEnvCleanupYielding = true
|
|
try {
|
|
__prepareEnvCleanup()
|
|
} finally {
|
|
__wasmEnvCleanupYielding = false
|
|
}
|
|
return
|
|
}
|
|
const __workPending = __exports?.napi_wasm_runtime_work_pending
|
|
// The in-flight flag stays raised across the turns below, so a `destroy()`
|
|
// from one of the JavaScript handlers they run is the same no-op it is
|
|
// inside the single call: the barrier is up and the runtime is
|
|
// mid-teardown, and destroying between the halves would strand exactly what
|
|
// this delivers.
|
|
__wasmEnvCleanupPreparing = true
|
|
let __live
|
|
try {
|
|
__live = __begin()
|
|
} catch (__error) {
|
|
__wasmEnvCleanupPreparing = false
|
|
throw __error
|
|
}
|
|
__wasmEnvCleanupRan = true
|
|
const __finishEnvCleanup = () => {
|
|
if (__wasmEnvCleanupPrepared) {
|
|
// Already closed by a caller that could not yield. `…_finish` is
|
|
// idempotent, but the flags it lowers are not.
|
|
return
|
|
}
|
|
__finishParkedEnvCleanup = undefined
|
|
try {
|
|
__finish()
|
|
} finally {
|
|
__wasmEnvCleanupPreparing = false
|
|
}
|
|
__wasmEnvCleanupPrepared = true
|
|
}
|
|
if (!__live || typeof __workPending !== 'function') {
|
|
__finishEnvCleanup()
|
|
return
|
|
}
|
|
// Publish the closer before yielding: from here until `__finishEnvCleanup`
|
|
// runs, a caller that cannot yield is entitled to end this handshake.
|
|
__finishParkedEnvCleanup = __finishEnvCleanup
|
|
return __pollWasmRuntimeWork(__workPending).then(
|
|
__finishEnvCleanup,
|
|
__finishEnvCleanup,
|
|
)
|
|
}
|
|
// The barrier + settlement drain, hoisted out of the context destroyer so the
|
|
// drain can yield without widening the destroyer's reentry window. Both
|
|
// yielding paths run it — dispose() and the initialization-failure rollback.
|
|
// The destroyer still runs the barrier itself (idempotently) for the one path
|
|
// that cannot yield: managed beforeExit cleanup of an instance whose rollback
|
|
// is being retried.
|
|
//
|
|
// "Drained" is recorded only once a wait has actually finished. Scheduling a
|
|
// macrotask can fail — a host-provided or patched `setImmediate` that throws
|
|
// is enough — and dispose() stays retryable after it rejects, so marking the
|
|
// drain complete up front would make the retry skip it and destroy the context
|
|
// with the barrier's settlements still queued.
|
|
const __drainAfterEnvCleanup = () => {
|
|
if (!__wasmEnvCleanupRan) {
|
|
return
|
|
}
|
|
const __drained = __drainWasmEnvCleanup(__napiInstance)
|
|
if (!__drained || typeof __drained.then !== 'function') {
|
|
__wasmEnvCleanupDrained = true
|
|
return
|
|
}
|
|
return __drained.then((__value) => {
|
|
__wasmEnvCleanupDrained = true
|
|
return __value
|
|
})
|
|
}
|
|
const __prepareForDisposal = () => {
|
|
if (__wasmEnvCleanupDrained) {
|
|
return
|
|
}
|
|
if (__wasmEnvCleanupDrainPromise) {
|
|
return __wasmEnvCleanupDrainPromise
|
|
}
|
|
// The barrier itself can yield now, so the memo below has to cover it too:
|
|
// a reentrant caller must join this handshake rather than start a second
|
|
// one while the first is parked between the two halves.
|
|
const __prepared = __prepareEnvCleanupWithTurns()
|
|
const __settled =
|
|
__prepared && typeof __prepared.then === 'function'
|
|
? __prepared.then(__drainAfterEnvCleanup)
|
|
: __drainAfterEnvCleanup()
|
|
if (!__settled || typeof __settled.then !== 'function') {
|
|
return
|
|
}
|
|
const __tracked = __settled.then(
|
|
(__value) => {
|
|
__wasmEnvCleanupDrainPromise = undefined
|
|
return __value
|
|
},
|
|
(__error) => {
|
|
__wasmEnvCleanupDrainPromise = undefined
|
|
throw __error
|
|
},
|
|
)
|
|
__wasmEnvCleanupDrainPromise = __tracked
|
|
return __tracked
|
|
}
|
|
let __disposed = false
|
|
const __runInstanceDisposal = async () => {
|
|
if (__lifecycleState !== 'failed') {
|
|
__lifecycleState = 'disposal'
|
|
}
|
|
// Outstanding async work first, while the environment is still completely
|
|
// live: its completion callbacks run addon code, and the barrier and
|
|
// `Context.destroy()` below each take that away. Undefined unless work is
|
|
// outstanding.
|
|
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)
|
|
if (__asyncWorkDrained) {
|
|
await __asyncWorkDrained
|
|
}
|
|
// Then settle what the barrier cancelled, before the environment stops
|
|
// accepting JavaScript calls. Undefined unless something is queued, so
|
|
// an idle disposal is not delayed by a single turn.
|
|
const __drained = __prepareForDisposal()
|
|
if (__drained) {
|
|
await __drained
|
|
}
|
|
const __result = await (__beforeExitDestroy
|
|
? __destroyManagedOwnedContext()
|
|
: __destroyOwnedContext())
|
|
// Only a completed destroy retires the instance; a throw above leaves
|
|
// the counter untouched so a retried dispose() cannot double-decrement.
|
|
if (!__disposed) {
|
|
__disposed = true
|
|
__liveInstances -= 1
|
|
}
|
|
return __result
|
|
}
|
|
let __instanceDisposePromise
|
|
/**
|
|
* The disposal frame runs the barrier, and the barrier settles the promises it
|
|
* cancels synchronously — so a promise hook firing inside it can call this
|
|
* same instance's dispose() again while the first call is still in its drain.
|
|
* That nested call finds the barrier flagged in flight, prepares nothing,
|
|
* drains nothing, and falls straight through to the context destroyer, whose
|
|
* `Context.destroy()` hits the wrapper's in-flight no-op. It would record a
|
|
* destruction that never happened, and the outer frame would then skip the
|
|
* real one: both disposals resolve, no cleanup hook runs, the context stays
|
|
* retained.
|
|
*
|
|
* Memoize before any of that starts, exactly like the eager loaders'
|
|
* `__disposeWasiBinding`, so there is only ever one disposal frame per
|
|
* instance and a reentrant caller awaits it instead of racing it. Cleared on
|
|
* rejection: a drain that failed has to stay retryable.
|
|
*/
|
|
const __disposeInstance = () => {
|
|
if (__instanceDisposePromise) {
|
|
return __instanceDisposePromise
|
|
}
|
|
let __resolveDispose
|
|
let __rejectDispose
|
|
const __disposePromise = new Promise((__resolve, __reject) => {
|
|
__resolveDispose = __resolve
|
|
__rejectDispose = __reject
|
|
})
|
|
__instanceDisposePromise = __disposePromise
|
|
__runInstanceDisposal().then(__resolveDispose, (__error) => {
|
|
__instanceDisposePromise = undefined
|
|
__rejectDispose(__error)
|
|
})
|
|
return __disposePromise
|
|
}
|
|
const __destroyBeforeExit = __beforeExitDestroy
|
|
? async () => {
|
|
if (__lifecycleState === 'failed') {
|
|
await __destroyManagedOwnedContext()
|
|
return
|
|
}
|
|
__lifecycleState = 'disposal'
|
|
try {
|
|
await __beforeExitDestroy()
|
|
} catch (error) {
|
|
if (__lifecycleState !== 'failed') {
|
|
throw error
|
|
}
|
|
// The singleton's initialization rejection is already observable
|
|
// through instantiate() and dispose(). Managed beforeExit cleanup
|
|
// owns only context destruction, including retrying a failed rollback.
|
|
await __destroyManagedOwnedContext()
|
|
}
|
|
}
|
|
: undefined
|
|
const {
|
|
context: __emnapiContext,
|
|
destroy,
|
|
destroyForModuleLifecycle,
|
|
registerCleanup: __registerCleanup,
|
|
} = await __createManagedEmnapiContext(
|
|
__prepareEnvCleanup,
|
|
__isPreparingEnvCleanup,
|
|
)
|
|
__destroyEmnapiContext = destroy
|
|
__destroyOwnedContext = () => __destroyEmnapiContext()
|
|
__destroyManagedOwnedContext = destroyForModuleLifecycle
|
|
try {
|
|
if (__destroyBeforeExit) {
|
|
__onManagedDestroyer(__destroyBeforeExit)
|
|
await __registerCleanup(__destroyBeforeExit)
|
|
}
|
|
let __napiModule
|
|
;({
|
|
instance: __napiInstance,
|
|
napiModule: __napiModule,
|
|
} = await __emnapiInstantiateNapiModule(__emnapiModule, {
|
|
context: __emnapiContext,
|
|
asyncWorkPoolSize: 0,
|
|
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
|
wasi: __wasi,
|
|
overwriteImports(importObject) {
|
|
importObject.env = {
|
|
...importObject.env,
|
|
...importObject.napi,
|
|
...importObject.emnapi,
|
|
memory: __wasmMemory,
|
|
}
|
|
return importObject
|
|
},
|
|
beforeInit({ instance }) {
|
|
__napiInstance = instance
|
|
for (const name of Object.keys(instance.exports)) {
|
|
if (name.startsWith('__napi_register__')) {
|
|
instance.exports[name]()
|
|
}
|
|
}
|
|
},
|
|
}))
|
|
// `instantiate()` and `createInstance().exports` hand out this object; a
|
|
// named module export does not travel with it. After the instance host
|
|
// install, which hands the same object to addon-provided registration
|
|
// functions that may put anything on it, and inside this `try`, so a
|
|
// claimed name flips `__lifecycleState` to 'failed' and tears the instance
|
|
// down rather than escaping a half-built one.
|
|
__napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
|
|
if (__lifecycleState === 'pending') {
|
|
__lifecycleState = 'succeeded'
|
|
}
|
|
__createdInstances += 1
|
|
__liveInstances += 1
|
|
return {
|
|
exports: __napiModule.exports,
|
|
get memory() {
|
|
return __wasmMemory
|
|
},
|
|
get memoryBytes() {
|
|
// The Memory outlives the environment, so this stays readable after a
|
|
// FAILED dispose() (which leaves the instance undisposed and
|
|
// retryable). It reports 0 only once disposal has actually completed.
|
|
return __disposed ? 0 : __wasmMemory.buffer.byteLength
|
|
},
|
|
get disposed() {
|
|
return __disposed
|
|
},
|
|
dispose: __disposeInstance,
|
|
}
|
|
} catch (error) {
|
|
__lifecycleState = 'failed'
|
|
// Instantiation can fail *after* registration has run, and registration runs
|
|
// with a live environment: a module-init hook can start async work and then
|
|
// return an error, and the promise it created may already have escaped into
|
|
// JavaScript. Settle what the barrier cancels before the context is
|
|
// destroyed, exactly like dispose() does; destroying without yielding
|
|
// discards the queue with a null env. Undefined unless something is queued,
|
|
// so a failure before beforeInit costs no extra turn.
|
|
let __settlementsUnreached = false
|
|
try {
|
|
// Registration can start async work too, and this path destroys the same
|
|
// environment its completions need. A drain that cannot finish leaves the
|
|
// work outstanding, so it counts as settlements unreached and stops the
|
|
// rollback short of destroying, exactly like a failed barrier drain.
|
|
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)
|
|
if (__asyncWorkDrained) {
|
|
await __asyncWorkDrained
|
|
}
|
|
const __drained = __prepareForDisposal()
|
|
if (__drained) {
|
|
await __drained
|
|
}
|
|
} catch (drainError) {
|
|
__attachCleanupError(error, drainError)
|
|
__settlementsUnreached = true
|
|
}
|
|
let __registrationError
|
|
let __registrationFailed = false
|
|
if (!__beforeExitDestroy) {
|
|
try {
|
|
// Independent instances are caller-owned while pending and after
|
|
// success. Register only failed rollback so cleanup remains retryable.
|
|
await __registerCleanup()
|
|
} catch (registrationError) {
|
|
__attachCleanupError(error, registrationError)
|
|
__registrationError = registrationError
|
|
__registrationFailed = true
|
|
}
|
|
}
|
|
if (__settlementsUnreached) {
|
|
// The barrier or the drain did not finish, so the settlements it queued
|
|
// are still in the threadsafe-function queue. Destroying now runs the
|
|
// cleanup hook that drains that queue with a null env and discards it,
|
|
// stranding a promise that already escaped into JavaScript — with nothing
|
|
// left that could ever settle it. dispose() refuses to destroy for exactly
|
|
// this reason (a rejected drain there never reaches the destroy), so this
|
|
// path refuses too.
|
|
//
|
|
// Nothing leaks. The registration just above — and, for the singleton, the
|
|
// one made before instantiation — leaves this context in
|
|
// `__managedEmnapiContextDestroyers`, so beforeExit destroys it and
|
|
// dispose() can retry it. Only the destruction is deferred, and the turns
|
|
// that pass in the meantime are exactly what the queue needed.
|
|
try {
|
|
__registerManagedBeforeExitListener()
|
|
} catch {}
|
|
throw error
|
|
}
|
|
try {
|
|
await __destroyManagedOwnedContext()
|
|
} catch (disposeError) {
|
|
// Initialization is the primary failure. Preserve it even if cleanup
|
|
// also fails, while retaining the cleanup error when the value is
|
|
// extensible and has no existing cause.
|
|
__attachCleanupError(
|
|
__registrationFailed ? __registrationError : error,
|
|
disposeError,
|
|
)
|
|
try {
|
|
__registerManagedBeforeExitListener()
|
|
} catch {}
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an independent instance. Call and await dispose() when the instance
|
|
* is no longer needed so emnapi cleanup hooks run deterministically.
|
|
*
|
|
* The optional second argument selects this instance's linear memory: either
|
|
* `memory` (an unshared, single-use WebAssembly.Memory you allocated) or
|
|
* `initialMemoryPages` / `maximumMemoryPages`, never both. Omitted, the
|
|
* loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages.
|
|
*
|
|
* A provided Memory must come from this loader's own realm: the WASI and
|
|
* emnapi layers underneath identify one with a realm-local `instanceof`, so a
|
|
* Memory built in a `node:vm` context or another frame is rejected. Every
|
|
* Memory an instance runs on is single-use, the loader-allocated one included:
|
|
* `instance.memory` cannot be recycled into a second `createInstance()`.
|
|
*/
|
|
export async function createInstance(__wasmInput, __options) {
|
|
return __createInstance(__wasmInput, __options)
|
|
}
|
|
|
|
let __defaultModulePromise
|
|
let __defaultInstancePromise
|
|
let __defaultDisposePromise
|
|
let __defaultDisposalStarted = false
|
|
const __defaultManagedDestroyers = new WeakMap()
|
|
let __moduleDisposePromise
|
|
|
|
/**
|
|
* Instantiate a module-local singleton. Concurrent and repeated calls
|
|
* with the same module share one instance and one Memory allocation.
|
|
*/
|
|
export function instantiate(__wasmInput) {
|
|
const __modulePromise = __resolveModule(__wasmInput)
|
|
if (__moduleLifecycleDestroyDepth !== 0) {
|
|
void __modulePromise.catch(() => {})
|
|
return Promise.reject(__createLifecycleReentryError('instantiate'))
|
|
}
|
|
if (__moduleDisposePromise) {
|
|
void __modulePromise.catch(() => {})
|
|
return __moduleDisposePromise.then(() => instantiate(__modulePromise))
|
|
}
|
|
if (__defaultDisposalStarted) {
|
|
// Observe rejected input immediately, but preserve lifecycle ordering and
|
|
// error precedence by instantiating only after disposal succeeds. A failed
|
|
// disposal retains the old instance only so its cleanup can be retried.
|
|
void __modulePromise.catch(() => {})
|
|
const __disposePromise = __defaultDisposePromise ?? dispose()
|
|
return __disposePromise.then(() => instantiate(__modulePromise))
|
|
}
|
|
if (!__defaultInstancePromise) {
|
|
__defaultModulePromise = __modulePromise
|
|
const __instancePromise = __modulePromise.then((__module) =>
|
|
__createInstance(
|
|
__module,
|
|
undefined,
|
|
__disposeDefaultInstance,
|
|
(__managedDestroyer) => {
|
|
__defaultManagedDestroyers.set(
|
|
__instancePromise,
|
|
__managedDestroyer,
|
|
)
|
|
},
|
|
),
|
|
)
|
|
__defaultInstancePromise = __instancePromise
|
|
void __instancePromise.catch(() => {
|
|
if (__defaultInstancePromise === __instancePromise) {
|
|
__defaultInstancePromise = undefined
|
|
__defaultModulePromise = undefined
|
|
}
|
|
})
|
|
return __instancePromise.then((__instance) => __instance.exports)
|
|
}
|
|
const __defaultModulePromiseForCall = __defaultModulePromise
|
|
const __defaultInstancePromiseForCall = __defaultInstancePromise
|
|
return Promise.all([__defaultModulePromiseForCall, __modulePromise]).then(
|
|
async ([__defaultModule, __module]) => {
|
|
if (__defaultModule !== __module) {
|
|
throw new Error(
|
|
'instantiate() already owns a different WebAssembly.Module; call dispose() first or use createInstance() for independent instances.',
|
|
)
|
|
}
|
|
return (await __defaultInstancePromiseForCall).exports
|
|
},
|
|
)
|
|
}
|
|
|
|
async function __disposeDefaultInstance(__onDestroy) {
|
|
if (__defaultDisposePromise) {
|
|
return __defaultDisposePromise
|
|
}
|
|
const __instancePromise = __defaultInstancePromise
|
|
if (!__instancePromise) {
|
|
__defaultDisposalStarted = false
|
|
return
|
|
}
|
|
__defaultDisposalStarted = true
|
|
const __disposePromise = (async () => {
|
|
let __instance
|
|
try {
|
|
__instance = await __instancePromise
|
|
} catch (error) {
|
|
const __managedDestroyer =
|
|
__defaultManagedDestroyers.get(__instancePromise)
|
|
if (__managedDestroyer) {
|
|
__onDestroy?.(__managedDestroyer)
|
|
}
|
|
__defaultManagedDestroyers.delete(__instancePromise)
|
|
throw error
|
|
}
|
|
const __managedDestroyer =
|
|
__defaultManagedDestroyers.get(__instancePromise)
|
|
if (__managedDestroyer) {
|
|
__onDestroy?.(__managedDestroyer)
|
|
}
|
|
await __instance.dispose()
|
|
if (__defaultInstancePromise === __instancePromise) {
|
|
__defaultInstancePromise = undefined
|
|
__defaultModulePromise = undefined
|
|
__defaultDisposalStarted = false
|
|
}
|
|
__defaultManagedDestroyers.delete(__instancePromise)
|
|
})()
|
|
__defaultDisposePromise = __disposePromise
|
|
try {
|
|
await __disposePromise
|
|
} finally {
|
|
if (__defaultDisposePromise === __disposePromise) {
|
|
__defaultDisposePromise = undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
async function __dispose() {
|
|
let __defaultDisposeError
|
|
let __defaultDisposeFailed = false
|
|
let __attemptedDefaultDestroyer
|
|
try {
|
|
await __disposeDefaultInstance((__destroyer) => {
|
|
__attemptedDefaultDestroyer = __destroyer
|
|
})
|
|
} catch (error) {
|
|
__defaultDisposeError = error
|
|
__defaultDisposeFailed = true
|
|
}
|
|
const __excludedDestroyers = new Set()
|
|
if (__defaultDisposeFailed && __attemptedDefaultDestroyer) {
|
|
__excludedDestroyers.add(__attemptedDefaultDestroyer)
|
|
}
|
|
try {
|
|
await __drainManagedEmnapiContexts(__excludedDestroyers)
|
|
} catch (error) {
|
|
if (!__defaultDisposeFailed) {
|
|
throw error
|
|
}
|
|
if (error !== __defaultDisposeError) {
|
|
__attachCleanupError(__defaultDisposeError, error)
|
|
}
|
|
}
|
|
if (__defaultDisposeFailed) {
|
|
throw __defaultDisposeError
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispose the singleton created by instantiate(). A later call may create a
|
|
* fresh instance, including from a different module. This also retries cleanup
|
|
* retained after a failed initialization rollback.
|
|
*/
|
|
export function dispose() {
|
|
if (__moduleLifecycleDestroyDepth !== 0) {
|
|
return Promise.reject(__createLifecycleReentryError('dispose'))
|
|
}
|
|
if (__moduleDisposePromise) {
|
|
return __moduleDisposePromise
|
|
}
|
|
let __resolveDispose
|
|
let __rejectDispose
|
|
const __promise = new Promise((resolve, reject) => {
|
|
__resolveDispose = resolve
|
|
__rejectDispose = reject
|
|
})
|
|
__moduleDisposePromise = __promise
|
|
void __dispose().then(__resolveDispose, __rejectDispose)
|
|
void __promise.then(
|
|
() => {
|
|
if (__moduleDisposePromise === __promise) {
|
|
__moduleDisposePromise = undefined
|
|
}
|
|
},
|
|
() => {
|
|
if (__moduleDisposePromise === __promise) {
|
|
__moduleDisposePromise = undefined
|
|
}
|
|
},
|
|
)
|
|
return __promise
|
|
}
|