@napi-rs/cli 3.9.1 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,168 @@
1
+ import {
2
+ BINDING_TARGET_STAMP_HELPER,
3
+ NAPI_BINDING_TARGET_EXPORT,
4
+ NAPI_BINDING_TARGET_STAMP_FN,
5
+ } from './binding-target.js'
6
+
1
7
  const WASI_DISPOSE_SYMBOL = 'napi.rs.wasi.dispose'
2
8
  const WASI_ROLLBACK_REGISTRY_SYMBOL = 'napi.rs.wasi.rollback.registry.v1'
3
9
 
4
- const emnapiContextLifecycle = `
10
+ /**
11
+ * `Context.destroy()` disables JavaScript calls *before* it runs cleanup hooks
12
+ * (`setStopping` -> `setCanCallIntoJs(false)` -> `runCleanup`), and the
13
+ * threadsafe function's cleanup hook then drains its queue with a null env and
14
+ * discards it. So `napi_prepare_wasm_env_cleanup` has to run while the
15
+ * environment is still live — before `destroy()`, never from a hook inside it.
16
+ *
17
+ * The loaders already order their own teardown that way. A `destroy()` called
18
+ * by anyone else — an embedder or test harness holding the context, emnapi's
19
+ * own `beforeExit` auto-destroy on a host where `suppressDestroy()` is absent —
20
+ * would skip the barrier and discard exactly the settlements it exists to
21
+ * cancel and deliver. Own the ordering on the object rather than on each call
22
+ * site: shadow `destroy` once at creation, so every caller gets the barrier.
23
+ *
24
+ * The barrier is reentrant-hostile, so the wrapper has to be reentrancy-aware.
25
+ * `napi_prepare_wasm_env_cleanup` settles the promises it cancels synchronously,
26
+ * under a non-reentrant lifecycle mutex on the Rust side; a V8 promise hook
27
+ * (`promiseHooks.onSettled`, or the `async_hooks` hook `AsyncLocalStorage`
28
+ * installs) that calls `destroy()` therefore re-enters this wrapper from inside
29
+ * the barrier, and calling the barrier again aborts the whole wasm instance.
30
+ * While a prepare is in flight the nested `destroy()` is a no-op rather than a
31
+ * deferred one: the frame that started the barrier destroys the moment it
32
+ * returns, still synchronously, and `Context.destroy()` is typed `void`, so
33
+ * answering `undefined` loses nothing a caller could have observed. Letting the
34
+ * nested call through instead would tear the environment down mid-barrier and
35
+ * strand every settlement the barrier had not reached yet.
36
+ *
37
+ * Defensive, not strict: a context whose `destroy` cannot be read or redefined
38
+ * is returned unchanged rather than failing the load. A barrier that throws
39
+ * still propagates, exactly as it does from `__destroyEmnapiContext`.
40
+ *
41
+ * This is NOT a replacement for `dispose()`: only the disposal chain yields
42
+ * event-loop turns until `napi_wasm_env_cleanup_pending` reads zero, so a
43
+ * direct `destroy()` still cannot wait for a settlement queued by another
44
+ * thread. It delivers everything the barrier settles on this thread.
45
+ */
46
+ const emnapiContextDestroyWrapper = `
47
+ function __wrapEmnapiContextDestroyForSettlement(
48
+ context,
49
+ prepareEnvCleanup,
50
+ isPreparingEnvCleanup,
51
+ ) {
52
+ let destroy
53
+ try {
54
+ destroy = context.destroy
55
+ } catch {
56
+ return context
57
+ }
58
+ if (typeof destroy !== 'function') {
59
+ return context
60
+ }
61
+ try {
62
+ Object.defineProperty(context, 'destroy', {
63
+ configurable: true,
64
+ enumerable: false,
65
+ writable: true,
66
+ value: function () {
67
+ // Reentered from a promise hook that fired inside the barrier: the
68
+ // frame running it destroys as soon as it returns.
69
+ if (isPreparingEnvCleanup?.()) {
70
+ return
71
+ }
72
+ prepareEnvCleanup?.()
73
+ return Reflect.apply(destroy, this, arguments)
74
+ },
75
+ })
76
+ } catch {}
77
+ return context
78
+ }
79
+ `
80
+
81
+ /**
82
+ * Host teardown must run while the environment can still accept N-API calls,
83
+ * and after the settlement drain: the drain is what lets the barrier's queued
84
+ * promise settlements reach JavaScript, and the task host is what publishes the
85
+ * CurrentThread turns they may still need. `__destroyEmnapiContext` is the
86
+ * single funnel every teardown path reaches — `dispose()`, the initialization
87
+ * rollback, and the CJS 'exit' handler — and it is reached only after
88
+ * `__startWasiDisposal` / the rollback have already prepared and drained, so
89
+ * one call there covers all three.
90
+ */
91
+ const createEmnapiContextLifecycle = (asyncRuntime: boolean) => {
92
+ const currentThreadHosts = asyncRuntime
93
+ ? `
94
+ let __currentThreadHostsDisposer
95
+
96
+ function __reportCurrentThreadHostDisposalError(error) {
97
+ try {
98
+ const consoleHost = globalThis.console
99
+ if (consoleHost && typeof consoleHost.error === 'function') {
100
+ consoleHost.error(error)
101
+ }
102
+ } catch {}
103
+ }
104
+
105
+ /**
106
+ * Unregister the CurrentThread task and timer hosts this loader installed.
107
+ * Idempotent, and never throws: an unregister failure must not abort
108
+ * \`Context.destroy()\`, which would retain the whole environment over a
109
+ * bookkeeping error. The failure is reported instead.
110
+ */
111
+ function __disposeCurrentThreadHosts() {
112
+ const dispose = __currentThreadHostsDisposer
113
+ if (dispose === undefined) {
114
+ return
115
+ }
116
+ __currentThreadHostsDisposer = undefined
117
+ try {
118
+ dispose()
119
+ } catch (error) {
120
+ __reportCurrentThreadHostDisposalError(error)
121
+ }
122
+ }
123
+ `
124
+ : ''
125
+ const disposeCurrentThreadHosts = asyncRuntime
126
+ ? ' __disposeCurrentThreadHosts()\n'
127
+ : ''
128
+
129
+ return `
5
130
  const __wasiDisposeSymbol = Symbol.for('${WASI_DISPOSE_SYMBOL}')
6
131
  const __wasiWorkers = new Set()
132
+ // The thread manager has to be reachable *before* anything that can throw
133
+ // during load or registration. Initialization can fail after the pool has
134
+ // already spawned workers, and the rollback still has to mark their
135
+ // terminations as expected — but \`__napiModule\` is assigned only when
136
+ // instantiation RETURNS, so on exactly that path it is still undefined. A
137
+ // plugin factory runs while the emnapi module is being created, before the
138
+ // wasm is loaded and before any registration function runs, and its context
139
+ // carries the very same manager instance.
140
+ let __wasiThreadManager
141
+
142
+ function __captureWasiThreadManager(context) {
143
+ if (context && context.PThread) {
144
+ __wasiThreadManager = context.PThread
145
+ }
146
+ return {}
147
+ }
148
+
149
+ function __getWasiThreadManager() {
150
+ const manager =
151
+ __wasiThreadManager !== undefined
152
+ ? __wasiThreadManager
153
+ : __napiModule
154
+ ? __napiModule.PThread
155
+ : undefined
156
+ if (manager && typeof manager.terminateWorker === 'function') {
157
+ return manager
158
+ }
159
+ return undefined
160
+ }
7
161
  let __napiInstance
8
162
  let __emnapiContextDestroyed = false
9
163
  let __emnapiContextDestroyPromise
10
164
  let __emnapiWasmEnvCleanupPrepared = false
165
+ let __emnapiWasmEnvCleanupPreparing = false
11
166
  let __emnapiWasmEnvCleanupRan = false
12
167
  let __emnapiWasmEnvCleanupDrained = false
13
168
  let __emnapiWasmEnvCleanupDrainPromise
@@ -18,7 +173,7 @@ let __completeWasiDisposal = function () {}
18
173
  // that stopped short of destroying the context. See
19
174
  // \`__rollbackWasiInitialization\`.
20
175
  let __retainWasiRollbackForRetry = function () {}
21
-
176
+ ${currentThreadHosts}
22
177
  function __isThenable(value) {
23
178
  return (
24
179
  value !== null &&
@@ -80,14 +235,26 @@ function __attachCleanupErrors(error, cleanupErrors) {
80
235
  } catch {}
81
236
  return aggregate
82
237
  }
238
+ ${emnapiContextDestroyWrapper}
239
+ function __isPreparingWasmEnvCleanup() {
240
+ return __emnapiWasmEnvCleanupPreparing
241
+ }
83
242
 
84
243
  function __prepareWasmEnvCleanup() {
85
- if (__emnapiWasmEnvCleanupPrepared) {
244
+ if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
86
245
  return
87
246
  }
88
247
  const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
89
248
  if (typeof prepare === 'function') {
90
- prepare()
249
+ // The addon settles the promises it cancels synchronously, under a
250
+ // non-reentrant lifecycle mutex: anything a promise hook calls from in
251
+ // here must not reach this export again.
252
+ __emnapiWasmEnvCleanupPreparing = true
253
+ try {
254
+ prepare()
255
+ } finally {
256
+ __emnapiWasmEnvCleanupPreparing = false
257
+ }
91
258
  __emnapiWasmEnvCleanupRan = true
92
259
  }
93
260
  __emnapiWasmEnvCleanupPrepared = true
@@ -259,6 +426,7 @@ function __destroyEmnapiContext() {
259
426
  return __emnapiContextDestroyPromise
260
427
  }
261
428
 
429
+ ${disposeCurrentThreadHosts}\
262
430
  __prepareWasmEnvCleanup()
263
431
  const result = __emnapiContext.destroy()
264
432
  if (!__isThenable(result)) {
@@ -280,13 +448,80 @@ function __destroyEmnapiContext() {
280
448
  return destroyPromise
281
449
  }
282
450
 
451
+ /**
452
+ * Holds the event loop open until \`work\` settles.
453
+ *
454
+ * Nothing else can: the pool workers are deliberately unreferenced so an idle
455
+ * binding cannot keep a process alive, and referencing them again for the
456
+ * termination does not hold either — emnapi unreferences a worker the moment it
457
+ * reports \`async-thread-ready\`, which for a worker that was still starting
458
+ * lands *after* the termination began. Without a handle of its own, an
459
+ * \`await dispose()\` with nothing else pending exits the process with its
460
+ * promise unsettled, and everything after the \`await\` is skipped.
461
+ *
462
+ * The timer is cleared as soon as the work settles, so this never outlives the
463
+ * disposal that asked for it.
464
+ */
465
+ function __keepEventLoopAliveUntil(work) {
466
+ const setTimer = globalThis.setInterval
467
+ const clearTimer = globalThis.clearInterval
468
+ if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') {
469
+ return work
470
+ }
471
+ let timer
472
+ try {
473
+ timer = setTimer(function () {}, 50)
474
+ } catch {
475
+ return work
476
+ }
477
+ const release = function () {
478
+ try {
479
+ clearTimer(timer)
480
+ } catch {}
481
+ }
482
+ return work.then(
483
+ (value) => {
484
+ release()
485
+ return value
486
+ },
487
+ (error) => {
488
+ release()
489
+ throw error
490
+ },
491
+ )
492
+ }
493
+
494
+ /**
495
+ * \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own
496
+ * thread manager performed the termination. A bare \`worker.terminate()\` reaches
497
+ * the manager's \`exit\` listener instead, which reports
498
+ * \`worker (tid = N) sent an error! ... stopped with exit code 1\` and rethrows
499
+ * inside the emit — aborting the \`once('exit')\` that backs the terminate
500
+ * promise, so disposal never settles and the process dies with an uncaught
501
+ * exception. Mark the termination through the manager first.
502
+ *
503
+ * The manager comes from \`__getWasiThreadManager\`, not from \`__napiModule\`:
504
+ * the initialization rollback runs on the one path where instantiation never
505
+ * returned, so \`__napiModule\` is still undefined there while the workers it
506
+ * spawned are already registered and loaded.
507
+ *
508
+ * Not \`terminateAllThreads()\`: that one recreates the pool it just shut down.
509
+ */
283
510
  function __terminateWasiWorkers() {
284
511
  const cleanupErrors = []
285
512
  const pending = []
513
+ const threadManager = __getWasiThreadManager()
286
514
 
287
515
  for (const worker of __wasiWorkers) {
288
516
  let result
289
517
  try {
518
+ if (threadManager) {
519
+ threadManager.terminateWorker(worker)
520
+ // \`terminateWorker\` leaves behind a reporter that logs every message
521
+ // still queued on the port, which Node flushes on exit. Nothing is
522
+ // listening for those any more.
523
+ worker.onmessage = undefined
524
+ }
290
525
  result = worker.terminate()
291
526
  } catch (error) {
292
527
  cleanupErrors.push(error)
@@ -316,7 +551,9 @@ function __terminateWasiWorkers() {
316
551
  )
317
552
  }
318
553
  }
319
- return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
554
+ return pending.length > 0
555
+ ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
556
+ : finish()
320
557
  }
321
558
 
322
559
  function __finishWasiDisposal() {
@@ -513,6 +750,7 @@ function __rollbackWasiInitialization() {
513
750
  return __destroyContextForWasiRollback(cleanupErrors)
514
751
  }
515
752
  `
753
+ }
516
754
 
517
755
  export const createWasiBrowserBinding = (
518
756
  wasiFilename: string,
@@ -523,11 +761,24 @@ export const createWasiBrowserBinding = (
523
761
  buffer = false,
524
762
  errorEvent = false,
525
763
  threads = true,
764
+ // `platformArchABI` of the flavor this loader belongs to. Defaults from
765
+ // `threads` so callers that predate the parameter keep their identity.
766
+ platformArchABI = threads ? 'wasm32-wasi' : 'wasm32-wasip1',
767
+ asyncRuntime = false,
526
768
  ) => {
527
769
  // Threaded builds always get a pre-created worker pool (see
528
770
  // `reuseWorkerOption` below), and pool pre-creation is asynchronous, so
529
771
  // they always initialize asynchronously.
530
772
  const effectiveAsyncInit = asyncInit || threads
773
+ const asyncRuntimeImport = asyncRuntime
774
+ ? `import { installCurrentThreadHosts as __installCurrentThreadHosts } from '@napi-rs/async-runtime'\n`
775
+ : ''
776
+ const installAsyncRuntimeHosts = asyncRuntime
777
+ ? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
778
+ __napiModule.exports,
779
+ )
780
+ `
781
+ : ''
531
782
  const fsImport = fs
532
783
  ? buffer
533
784
  ? `import { memfs, Buffer } from '@napi-rs/wasm-runtime/fs'`
@@ -631,7 +882,7 @@ const __workerPoolSize = Math.max(
631
882
  // `napi_generic_failure` stubs; with threads it would shadow the
632
883
  // `@emnapi/core` threaded TSFN/async-work protocol the plugins implement.
633
884
  const emnapiPluginImport = ` emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin as __emnapiTSFNPlugin,\n`
634
- const emnapiPluginOption = ` plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],\n`
885
+ const emnapiPluginOption = ` plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],\n`
635
886
  const workerOption = threads
636
887
  ? ` onCreateWorker() {
637
888
  const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -652,8 +903,11 @@ ${workerRuntimeImport}\
652
903
  WASI as __WASI,
653
904
  } from '@napi-rs/wasm-runtime'
654
905
  import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
906
+ ${asyncRuntimeImport}\
655
907
  ${fsImport}
656
908
  ${bufferImport}
909
+ export const __napiBindingTarget = '${platformArchABI}'
910
+ ${BINDING_TARGET_STAMP_HELPER}
657
911
  ${wasiCreation}
658
912
 
659
913
  const __wasmUrl = new URL('./${wasiFilename}.wasm', import.meta.url).href
@@ -677,12 +931,16 @@ ${threads ? ' shared: true,\n' : ''}\
677
931
  })
678
932
  ${workerPoolSizeBinding}\
679
933
  let __emnapiContext
680
- ${emnapiContextLifecycle}
934
+ ${createEmnapiContextLifecycle(asyncRuntime)}
681
935
  let __wasiModule
682
936
  let __napiModule
683
937
 
684
938
  try {
685
- __emnapiContext = __emnapiCreateContext({ autoDestroy: false })
939
+ __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
940
+ __emnapiCreateContext({ autoDestroy: false }),
941
+ __prepareWasmEnvCleanup,
942
+ __isPreparingWasmEnvCleanup,
943
+ )
686
944
  __emnapiContext.suppressDestroy()
687
945
  ${emnapiInjectBuffer}
688
946
  ;({
@@ -715,6 +973,13 @@ ${workerOption}\
715
973
  },
716
974
  }))
717
975
  __publishWasiDispose(__napiModule.exports)
976
+ ${installAsyncRuntimeHosts}\
977
+ // The default export hands out this object; a named module export does not
978
+ // travel with it, so carry the marker on the binding itself too. After the
979
+ // host install, which hands the same object to addon-provided registration
980
+ // functions that may put anything on it, and inside this \`try\`, so a claimed
981
+ // name fails the load through the rollback below rather than past it.
982
+ ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
718
983
  } catch (error) {
719
984
  const cleanupErrors = await __rollbackWasiInitialization()
720
985
  throw __attachCleanupErrors(error, cleanupErrors)
@@ -722,19 +987,232 @@ ${workerOption}\
722
987
  `
723
988
  }
724
989
 
990
+ /**
991
+ * Module-scope prelude of the deferred loader: the compiled-in memory
992
+ * descriptor, the per-module instance counters, and the resolver that turns a
993
+ * `createInstance()` options bag into the one `WebAssembly.Memory` that
994
+ * instance runs on.
995
+ *
996
+ * Nothing here allocates — workerd bans allocation in global scope, so the
997
+ * Memory itself is created inside `__createInstance`.
998
+ */
999
+ const DEFERRED_MEMORY_PREAMBLE = (
1000
+ initialMemory: number,
1001
+ maximumMemory: number,
1002
+ ) => `
1003
+ export const WASM_MEMORY = Object.freeze({
1004
+ initialPages: ${initialMemory},
1005
+ maximumPages: ${maximumMemory},
1006
+ pageBytes: 65536,
1007
+ initialBytes: ${initialMemory} * 65536,
1008
+ maximumBytes: ${maximumMemory} * 65536,
1009
+ })
1010
+
1011
+ let __createdInstances = 0
1012
+ let __liveInstances = 0
1013
+
1014
+ /**
1015
+ * Counters for instances created by THIS module evaluation, not process-wide:
1016
+ * a second bundled copy of this loader keeps its own. Only successfully
1017
+ * created instances are counted, and \`liveInstances\` drops when an instance's
1018
+ * \`dispose()\` resolves.
1019
+ *
1020
+ * \`declaredInitialMemoryBytes\` is declared address space, not a host's
1021
+ * committed-memory metric; pair it with host telemetry rather than treating it
1022
+ * as a quota.
1023
+ */
1024
+ export function getDeferredRuntimeStats() {
1025
+ return Object.freeze({
1026
+ createdInstances: __createdInstances,
1027
+ liveInstances: __liveInstances,
1028
+ declaredInitialMemoryBytes: WASM_MEMORY.initialBytes,
1029
+ })
1030
+ }
1031
+
1032
+ const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor(
1033
+ ArrayBuffer.prototype,
1034
+ 'byteLength',
1035
+ ).get
1036
+ const __memoryBufferGetter = Object.getOwnPropertyDescriptor(
1037
+ WebAssembly.Memory.prototype,
1038
+ 'buffer',
1039
+ ).get
1040
+ // One managed initialization per Memory, success or failure: an attempt that
1041
+ // throws may already have written into linear memory, so the bytes are not a
1042
+ // clean slate for a second instance. Module-local, like the counters above.
1043
+ const __claimedMemories = new WeakSet()
1044
+
1045
+ function __resolveInstanceMemory(__options) {
1046
+ const __provided = __options == null ? undefined : __options.memory
1047
+ if (__provided === undefined || __provided === null) {
1048
+ // Page counts are handed to the engine unvalidated: it already rejects a
1049
+ // negative, over-4GiB or below-maximum value with a precise message, and a
1050
+ // second set of bounds here would only drift from it.
1051
+ const __allocated = new WebAssembly.Memory({
1052
+ initial:
1053
+ __options != null && __options.initialMemoryPages !== undefined
1054
+ ? __options.initialMemoryPages
1055
+ : WASM_MEMORY.initialPages,
1056
+ maximum:
1057
+ __options != null && __options.maximumMemoryPages !== undefined
1058
+ ? __options.maximumMemoryPages
1059
+ : WASM_MEMORY.maximumPages,
1060
+ })
1061
+ // Claimed like a caller-provided one. The handle publishes it as
1062
+ // \`instance.memory\`, so handing it back to \`createInstance()\` is as easy
1063
+ // as passing your own twice, and it would put two live instances on one
1064
+ // linear memory: each initialization rewrites the emnapi/WASI state the
1065
+ // other is still running on.
1066
+ __claimedMemories.add(__allocated)
1067
+ return __allocated
1068
+ }
1069
+ if (
1070
+ __options.initialMemoryPages !== undefined ||
1071
+ __options.maximumMemoryPages !== undefined
1072
+ ) {
1073
+ throw new TypeError(
1074
+ 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both',
1075
+ )
1076
+ }
1077
+ let __buffer
1078
+ try {
1079
+ // Brand check: the getter throws for anything that is not a genuine
1080
+ // WebAssembly.Memory, including a cross-realm look-alike object.
1081
+ __buffer = Reflect.apply(__memoryBufferGetter, __provided, [])
1082
+ } catch {
1083
+ throw new TypeError('memory must be an unshared WebAssembly.Memory')
1084
+ }
1085
+ try {
1086
+ // Throws for a SharedArrayBuffer. This loader has no threads, and shared
1087
+ // growth does not detach: external views handed to the addon would
1088
+ // silently outlive the bytes they describe.
1089
+ Reflect.apply(__arrayBufferByteLengthGetter, __buffer, [])
1090
+ } catch {
1091
+ throw new TypeError(
1092
+ 'The deferred loader requires an unshared WebAssembly.Memory',
1093
+ )
1094
+ }
1095
+ // The intrinsic getters above accept a genuine Memory from ANY realm, but
1096
+ // the loader's dependencies do not: \`WASI.setMemory\` in
1097
+ // \`@napi-rs/wasm-runtime\` and emnapi identify a Memory with a realm-local
1098
+ // \`instanceof\`. A Memory built in another realm (a \`node:vm\` context, a
1099
+ // same-origin iframe) would pass every check here and only fail deep inside
1100
+ // initialization. Reject it up front, and before the claim below, so the
1101
+ // caller keeps it usable in the realm that made it.
1102
+ if (!(__provided instanceof WebAssembly.Memory)) {
1103
+ throw new TypeError(
1104
+ 'memory must be a WebAssembly.Memory created in the same realm as this loader',
1105
+ )
1106
+ }
1107
+ if (__claimedMemories.has(__provided)) {
1108
+ throw new TypeError(
1109
+ 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal',
1110
+ )
1111
+ }
1112
+ // Last step, after every check: a rejected option bag must leave the Memory
1113
+ // unclaimed, or a caller could not fix the call and retry with it.
1114
+ __claimedMemories.add(__provided)
1115
+ return __provided
1116
+ }
1117
+ `
1118
+
725
1119
  export const createWasiDeferredBrowserBinding = (
726
1120
  wasiFilename: string,
1121
+ // Fed by `napi.wasm.threadlessInitialMemory ?? napi.wasm.initialMemory`.
727
1122
  // 64 MiB leaves headroom for JS/runtime state under workerd's 128 MiB
728
1123
  // isolate limit. The regular Node/browser loaders retain their historical
729
1124
  // 4,000-page default.
730
1125
  initialMemory = 1024,
731
1126
  maximumMemory = 65536,
732
1127
  buffer = false,
1128
+ // Deferred loaders are only emitted for non-threaded flavors, so the
1129
+ // default matches the only flavor `napi build` generates one for.
1130
+ platformArchABI = 'wasm32-wasip1',
1131
+ asyncRuntime = false,
733
1132
  ) => {
734
1133
  const bufferImport = buffer ? `import { Buffer } from 'buffer'` : ''
735
1134
  const emnapiInjectBuffer = buffer
736
1135
  ? ' __emnapiContext.features.Buffer = Buffer\n'
737
1136
  : ''
1137
+ // This flavor creates N independent instances per realm, each with its own
1138
+ // emnapi context and its own `napiModule.exports`, so it uses the
1139
+ // per-instance helpers rather than `installCurrentThreadHosts`: those return
1140
+ // exact, idempotent disposers with no realm-global dedup, roll themselves
1141
+ // back on a setup failure, and degrade to a no-op disposer when the realm has
1142
+ // no `setTimeout`/`clearTimeout`.
1143
+ // The `/workerd` subpath, not the barrel: the barrel's `index.cjs` also
1144
+ // requires `current-thread-hosts.cjs`, whose realm-global registry and Node
1145
+ // timer-handle bookkeeping this flavor never executes, and a CJS barrel is
1146
+ // not tree-shakeable out of a worker bundle.
1147
+ const asyncRuntimeImport = asyncRuntime
1148
+ ? `import {
1149
+ registerWorkerdCurrentThreadTaskHost as __registerWorkerdCurrentThreadTaskHost,
1150
+ registerWorkerdTimerHost as __registerWorkerdTimerHost,
1151
+ } from '@napi-rs/async-runtime/workerd'
1152
+ `
1153
+ : ''
1154
+ // `__createManagedEmnapiContext` calls `__prepareEnvCleanup?.()` on EVERY
1155
+ // destroy path (dispose(), managed beforeExit, module lifecycle), so a second
1156
+ // hook next to it covers them all with one edit.
1157
+ const managedHostDisposeParam = asyncRuntime ? ' __disposeHosts,\n' : ''
1158
+ const managedHostDisposeCall = asyncRuntime
1159
+ ? ` __disposeHosts?.()\n`
1160
+ : ''
1161
+ const instanceHostState = asyncRuntime
1162
+ ? ` let __disposeInstanceHosts
1163
+ const __reportInstanceHostDisposalError = (__error) => {
1164
+ try {
1165
+ const __consoleHost = globalThis.console
1166
+ if (__consoleHost && typeof __consoleHost.error === 'function') {
1167
+ __consoleHost.error(__error)
1168
+ }
1169
+ } catch {}
1170
+ }
1171
+ // Runs between the settlement drain and \`Context.destroy()\`; never throws,
1172
+ // for the same reason the eager loaders' disposer does not.
1173
+ const __disposeHostsBeforeDestroy = () => {
1174
+ const __dispose = __disposeInstanceHosts
1175
+ if (__dispose === undefined) {
1176
+ return
1177
+ }
1178
+ __disposeInstanceHosts = undefined
1179
+ __dispose()
1180
+ }
1181
+ `
1182
+ : ''
1183
+ const installInstanceHosts = asyncRuntime
1184
+ ? ` const __disposeTaskHost = __registerWorkerdCurrentThreadTaskHost(
1185
+ __napiModule.exports,
1186
+ )
1187
+ try {
1188
+ const __disposeTimerHost = __registerWorkerdTimerHost(
1189
+ __napiModule.exports,
1190
+ )
1191
+ __disposeInstanceHosts = () => {
1192
+ try {
1193
+ __disposeTimerHost()
1194
+ } catch (__error) {
1195
+ __reportInstanceHostDisposalError(__error)
1196
+ }
1197
+ try {
1198
+ __disposeTaskHost()
1199
+ } catch (__error) {
1200
+ __reportInstanceHostDisposalError(__error)
1201
+ }
1202
+ }
1203
+ } catch (__error) {
1204
+ try {
1205
+ __disposeTaskHost()
1206
+ } catch (__cleanupError) {
1207
+ __attachCleanupError(__error, __cleanupError)
1208
+ }
1209
+ throw __error
1210
+ }
1211
+ `
1212
+ : ''
1213
+ const managedHostDisposeArg = asyncRuntime
1214
+ ? ' __disposeHostsBeforeDestroy,\n'
1215
+ : ''
738
1216
  return `import {
739
1217
  emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,
740
1218
  emnapiTSFNPlugin as __emnapiTSFNPlugin,
@@ -742,7 +1220,11 @@ export const createWasiDeferredBrowserBinding = (
742
1220
  WASI as __WASI,
743
1221
  } from '@napi-rs/wasm-runtime'
744
1222
  import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
1223
+ ${asyncRuntimeImport}\
745
1224
  ${bufferImport}
1225
+ ${DEFERRED_MEMORY_PREAMBLE(initialMemory, maximumMemory)}
1226
+ export const __napiBindingTarget = '${platformArchABI}'
1227
+ ${BINDING_TARGET_STAMP_HELPER}
746
1228
 
747
1229
  /**
748
1230
  * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes.
@@ -841,7 +1323,7 @@ async function __normalizeModuleForEmnapi(__module) {
841
1323
  'provide structuredClone or MessageChannel support.',
842
1324
  )
843
1325
  }
844
-
1326
+ ${emnapiContextDestroyWrapper}
845
1327
  function __captureEmnapiAutoDestroyListener(__process) {
846
1328
  if (
847
1329
  !__process ||
@@ -1229,7 +1711,10 @@ function __registerManagedEmnapiContext(__process, __destroy) {
1229
1711
  }
1230
1712
  }
1231
1713
 
1232
- async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1714
+ async function __createManagedEmnapiContext(
1715
+ __prepareEnvCleanup,
1716
+ __isPreparingEnvCleanup,
1717
+ ${managedHostDisposeParam}) {
1233
1718
  const __process =
1234
1719
  typeof process === 'object' && process !== null ? process : undefined
1235
1720
  const __finishAutoDestroyCapture =
@@ -1238,7 +1723,11 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1238
1723
  let __contextInitializationError
1239
1724
  let __contextInitializationFailed = false
1240
1725
  try {
1241
- __emnapiContext = __emnapiCreateContext({ autoDestroy: false })
1726
+ __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
1727
+ __emnapiCreateContext({ autoDestroy: false }),
1728
+ __prepareEnvCleanup,
1729
+ __isPreparingEnvCleanup,
1730
+ )
1242
1731
  // emnapi 2.x still registers an unconditional process.once('beforeExit')
1243
1732
  // auto-destroy listener on Node hosts, and suppressDestroy() only
1244
1733
  // neutralizes its callback without removing it. This loader must stay
@@ -1288,6 +1777,18 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1288
1777
  // Context.destroy() disables JS before cleanup hooks run, so settle
1289
1778
  // runtime-owned promises while this environment can still call JS.
1290
1779
  __prepareEnvCleanup?.()
1780
+ if (__isPreparingEnvCleanup?.()) {
1781
+ // Reached from inside the barrier, so \`Context.destroy()\` below would
1782
+ // hit the wrapper's in-flight no-op. Recording that as a completed
1783
+ // destroy is what makes the frame that *did* start the barrier skip the
1784
+ // real one afterwards, leaving the context retained with its cleanup
1785
+ // hooks unrun. Refuse instead: nothing is flagged, the context stays
1786
+ // registered for managed beforeExit cleanup, and a later destroy still
1787
+ // works. dispose() coalesces reentrancy before it can get here, so this
1788
+ // is the backstop for any other caller that manages to.
1789
+ throw __createLifecycleReentryError('dispose')
1790
+ }
1791
+ ${managedHostDisposeCall}\
1291
1792
  __result = __emnapiContext.destroy()
1292
1793
  } catch (error) {
1293
1794
  __finishDestroyInvocation()
@@ -1392,6 +1893,7 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1392
1893
 
1393
1894
  async function __createInstance(
1394
1895
  __wasmInput,
1896
+ __options,
1395
1897
  __beforeExitDestroy,
1396
1898
  __onManagedDestroyer,
1397
1899
  ) {
@@ -1401,31 +1903,40 @@ async function __createInstance(
1401
1903
  version: 'preview1',
1402
1904
  })
1403
1905
  // The wasm module is linked with \`--import-memory\`, so a Memory must be
1404
- // provided. It is allocated here in function scope (workerd bans global
1405
- // scope allocation) and is not shared (no threads, no SharedArrayBuffer).
1406
- // Allocate it before the emnapi context so a host memory-limit failure cannot
1407
- // leak a context that never reaches instantiation.
1408
- const __wasmMemory = new WebAssembly.Memory({
1409
- initial: ${initialMemory},
1410
- maximum: ${maximumMemory},
1411
- })
1906
+ // provided. It is resolved here in function scope (workerd bans global scope
1907
+ // allocation) and is never shared (no threads, no SharedArrayBuffer).
1908
+ // Resolve it before the emnapi context so a rejected option bag or a host
1909
+ // memory-limit failure cannot leak a context that never reaches
1910
+ // instantiation.
1911
+ const __wasmMemory = __resolveInstanceMemory(__options)
1412
1912
  let __lifecycleState = 'pending'
1413
1913
  let __destroyEmnapiContext
1414
1914
  let __destroyOwnedContext
1415
1915
  let __destroyManagedOwnedContext
1416
1916
  let __napiInstance
1917
+ ${instanceHostState}\
1417
1918
  let __wasmEnvCleanupRan = false
1418
1919
  let __wasmEnvCleanupPrepared = false
1920
+ let __wasmEnvCleanupPreparing = false
1419
1921
  let __wasmEnvCleanupDrained = false
1420
1922
  let __wasmEnvCleanupDrainPromise
1923
+ const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing
1421
1924
  const __prepareEnvCleanup = () => {
1422
- if (__wasmEnvCleanupPrepared) {
1925
+ if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) {
1423
1926
  return
1424
1927
  }
1425
1928
  const __prepareWasmEnvCleanup =
1426
1929
  __napiInstance?.exports.napi_prepare_wasm_env_cleanup
1427
1930
  if (typeof __prepareWasmEnvCleanup === 'function') {
1428
- __prepareWasmEnvCleanup()
1931
+ // The addon settles the promises it cancels synchronously, under a
1932
+ // non-reentrant lifecycle mutex: anything a promise hook calls from in
1933
+ // here must not reach this export again.
1934
+ __wasmEnvCleanupPreparing = true
1935
+ try {
1936
+ __prepareWasmEnvCleanup()
1937
+ } finally {
1938
+ __wasmEnvCleanupPreparing = false
1939
+ }
1429
1940
  __wasmEnvCleanupRan = true
1430
1941
  }
1431
1942
  __wasmEnvCleanupPrepared = true
@@ -1472,6 +1983,63 @@ async function __createInstance(
1472
1983
  __wasmEnvCleanupDrainPromise = __tracked
1473
1984
  return __tracked
1474
1985
  }
1986
+ let __disposed = false
1987
+ const __runInstanceDisposal = async () => {
1988
+ if (__lifecycleState !== 'failed') {
1989
+ __lifecycleState = 'disposal'
1990
+ }
1991
+ // Settle what the barrier cancelled before the environment stops
1992
+ // accepting JavaScript calls. Undefined unless something is queued, so
1993
+ // an idle disposal is not delayed by a single turn.
1994
+ const __drained = __prepareForDisposal()
1995
+ if (__drained) {
1996
+ await __drained
1997
+ }
1998
+ const __result = await (__beforeExitDestroy
1999
+ ? __destroyManagedOwnedContext()
2000
+ : __destroyOwnedContext())
2001
+ // Only a completed destroy retires the instance; a throw above leaves
2002
+ // the counter untouched so a retried dispose() cannot double-decrement.
2003
+ if (!__disposed) {
2004
+ __disposed = true
2005
+ __liveInstances -= 1
2006
+ }
2007
+ return __result
2008
+ }
2009
+ let __instanceDisposePromise
2010
+ /**
2011
+ * The disposal frame runs the barrier, and the barrier settles the promises it
2012
+ * cancels synchronously — so a promise hook firing inside it can call this
2013
+ * same instance's dispose() again while the first call is still in its drain.
2014
+ * That nested call finds the barrier flagged in flight, prepares nothing,
2015
+ * drains nothing, and falls straight through to the context destroyer, whose
2016
+ * \`Context.destroy()\` hits the wrapper's in-flight no-op. It would record a
2017
+ * destruction that never happened, and the outer frame would then skip the
2018
+ * real one: both disposals resolve, no cleanup hook runs, the context stays
2019
+ * retained.
2020
+ *
2021
+ * Memoize before any of that starts, exactly like the eager loaders'
2022
+ * \`__disposeWasiBinding\`, so there is only ever one disposal frame per
2023
+ * instance and a reentrant caller awaits it instead of racing it. Cleared on
2024
+ * rejection: a drain that failed has to stay retryable.
2025
+ */
2026
+ const __disposeInstance = () => {
2027
+ if (__instanceDisposePromise) {
2028
+ return __instanceDisposePromise
2029
+ }
2030
+ let __resolveDispose
2031
+ let __rejectDispose
2032
+ const __disposePromise = new Promise((__resolve, __reject) => {
2033
+ __resolveDispose = __resolve
2034
+ __rejectDispose = __reject
2035
+ })
2036
+ __instanceDisposePromise = __disposePromise
2037
+ __runInstanceDisposal().then(__resolveDispose, (__error) => {
2038
+ __instanceDisposePromise = undefined
2039
+ __rejectDispose(__error)
2040
+ })
2041
+ return __disposePromise
2042
+ }
1475
2043
  const __destroyBeforeExit = __beforeExitDestroy
1476
2044
  ? async () => {
1477
2045
  if (__lifecycleState === 'failed') {
@@ -1497,7 +2065,10 @@ async function __createInstance(
1497
2065
  destroy,
1498
2066
  destroyForModuleLifecycle,
1499
2067
  registerCleanup: __registerCleanup,
1500
- } = await __createManagedEmnapiContext(__prepareEnvCleanup)
2068
+ } = await __createManagedEmnapiContext(
2069
+ __prepareEnvCleanup,
2070
+ __isPreparingEnvCleanup,
2071
+ ${managedHostDisposeArg} )
1501
2072
  __destroyEmnapiContext = destroy
1502
2073
  __destroyOwnedContext = () => __destroyEmnapiContext()
1503
2074
  __destroyManagedOwnedContext = destroyForModuleLifecycle
@@ -1534,26 +2105,34 @@ ${emnapiInjectBuffer}\
1534
2105
  }
1535
2106
  },
1536
2107
  }))
2108
+ ${installInstanceHosts}\
2109
+ // \`instantiate()\` and \`createInstance().exports\` hand out this object; a
2110
+ // named module export does not travel with it. After the instance host
2111
+ // install, which hands the same object to addon-provided registration
2112
+ // functions that may put anything on it, and inside this \`try\`, so a
2113
+ // claimed name flips \`__lifecycleState\` to 'failed' and tears the instance
2114
+ // down rather than escaping a half-built one.
2115
+ ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
1537
2116
  if (__lifecycleState === 'pending') {
1538
2117
  __lifecycleState = 'succeeded'
1539
2118
  }
2119
+ __createdInstances += 1
2120
+ __liveInstances += 1
1540
2121
  return {
1541
2122
  exports: __napiModule.exports,
1542
- async dispose() {
1543
- if (__lifecycleState !== 'failed') {
1544
- __lifecycleState = 'disposal'
1545
- }
1546
- // Settle what the barrier cancelled before the environment stops
1547
- // accepting JavaScript calls. Undefined unless something is queued, so
1548
- // an idle disposal is not delayed by a single turn.
1549
- const __drained = __prepareForDisposal()
1550
- if (__drained) {
1551
- await __drained
1552
- }
1553
- return __beforeExitDestroy
1554
- ? __destroyManagedOwnedContext()
1555
- : __destroyOwnedContext()
2123
+ get memory() {
2124
+ return __wasmMemory
1556
2125
  },
2126
+ get memoryBytes() {
2127
+ // The Memory outlives the environment, so this stays readable after a
2128
+ // FAILED dispose() (which leaves the instance undisposed and
2129
+ // retryable). It reports 0 only once disposal has actually completed.
2130
+ return __disposed ? 0 : __wasmMemory.buffer.byteLength
2131
+ },
2132
+ get disposed() {
2133
+ return __disposed
2134
+ },
2135
+ dispose: __disposeInstance,
1557
2136
  }
1558
2137
  } catch (error) {
1559
2138
  __lifecycleState = 'failed'
@@ -1625,11 +2204,22 @@ ${emnapiInjectBuffer}\
1625
2204
  }
1626
2205
 
1627
2206
  /**
1628
- * Create an independent instance. Call dispose() when the instance is no
1629
- * longer needed so emnapi cleanup hooks run deterministically.
2207
+ * Create an independent instance. Call and await dispose() when the instance
2208
+ * is no longer needed so emnapi cleanup hooks run deterministically.
2209
+ *
2210
+ * The optional second argument selects this instance's linear memory: either
2211
+ * \`memory\` (an unshared, single-use WebAssembly.Memory you allocated) or
2212
+ * \`initialMemoryPages\` / \`maximumMemoryPages\`, never both. Omitted, the
2213
+ * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages.
2214
+ *
2215
+ * A provided Memory must come from this loader's own realm: the WASI and
2216
+ * emnapi layers underneath identify one with a realm-local \`instanceof\`, so a
2217
+ * Memory built in a \`node:vm\` context or another frame is rejected. Every
2218
+ * Memory an instance runs on is single-use, the loader-allocated one included:
2219
+ * \`instance.memory\` cannot be recycled into a second \`createInstance()\`.
1630
2220
  */
1631
- export async function createInstance(__wasmInput) {
1632
- return __createInstance(__wasmInput)
2221
+ export async function createInstance(__wasmInput, __options) {
2222
+ return __createInstance(__wasmInput, __options)
1633
2223
  }
1634
2224
 
1635
2225
  let __defaultModulePromise
@@ -1666,6 +2256,7 @@ export function instantiate(__wasmInput) {
1666
2256
  const __instancePromise = __modulePromise.then((__module) =>
1667
2257
  __createInstance(
1668
2258
  __module,
2259
+ undefined,
1669
2260
  __disposeDefaultInstance,
1670
2261
  (__managedDestroyer) => {
1671
2262
  __defaultManagedDestroyers.set(
@@ -1814,21 +2405,90 @@ export function dispose() {
1814
2405
 
1815
2406
  export const createWasiDeferredBrowserBindingTypeDef = (
1816
2407
  packageName: string,
2408
+ platformArchABI = 'wasm32-wasip1',
1817
2409
  ) => `export type WasiBinding = typeof import('${packageName}')
1818
2410
 
1819
2411
  export type WasiModuleInput =
1820
2412
  | WebAssembly.Module
1821
2413
  | PromiseLike<WebAssembly.Module>
1822
2414
 
2415
+ /** Run the instance on a linear memory the caller allocated. */
2416
+ export interface WasiCallerMemoryOptions {
2417
+ /**
2418
+ * A caller-allocated linear memory for this instance. It must be unshared
2419
+ * and created in this loader's own realm — the WASI and emnapi layers
2420
+ * underneath identify a Memory with a realm-local \`instanceof\`, so one from
2421
+ * a \`node:vm\` context or another frame is rejected. It is single-use: once
2422
+ * a validated initialization attempt has begun, the same Memory cannot be
2423
+ * passed again — including after that attempt failed, and after the instance
2424
+ * was disposed.
2425
+ */
2426
+ memory: WebAssembly.Memory
2427
+ /** Not available beside \`memory\`: the loader allocates neither. */
2428
+ initialMemoryPages?: never
2429
+ /** Not available beside \`memory\`: the loader allocates neither. */
2430
+ maximumMemoryPages?: never
2431
+ }
2432
+
2433
+ /** Let the loader allocate the linear memory, optionally sized. */
2434
+ export interface WasiAllocatedMemoryOptions {
2435
+ /** Not available beside the page counts: they size the loader's own Memory. */
2436
+ memory?: never
2437
+ /** @default WASM_MEMORY.initialPages */
2438
+ initialMemoryPages?: number
2439
+ /** @default WASM_MEMORY.maximumPages */
2440
+ maximumMemoryPages?: number
2441
+ }
2442
+
2443
+ /**
2444
+ * Either memory form, never a mix of the two: the loader throws a TypeError
2445
+ * on \`memory\` beside a page count. \`{}\` and an omitted argument select the
2446
+ * loader defaults.
2447
+ */
2448
+ export type WasiInstanceOptions =
2449
+ | WasiCallerMemoryOptions
2450
+ | WasiAllocatedMemoryOptions
2451
+
2452
+ export interface WasiRuntimeStats {
2453
+ /** Instances created by this evaluated loader module, not process-wide. */
2454
+ createdInstances: number
2455
+ /** Created instances whose dispose() has not completed. */
2456
+ liveInstances: number
2457
+ /** Declared initial address space, not committed memory. */
2458
+ declaredInitialMemoryBytes: number
2459
+ }
2460
+
1823
2461
  export interface WasiInstance {
1824
2462
  readonly exports: WasiBinding
2463
+ /** This instance's linear memory. Claimed, so it cannot start another one. */
2464
+ readonly memory: WebAssembly.Memory
2465
+ /** Current linear-memory size; 0 once dispose() has completed. */
2466
+ readonly memoryBytes: number
2467
+ readonly disposed: boolean
1825
2468
  dispose(): Promise<void>
1826
2469
  }
1827
2470
 
2471
+ /** The memory descriptor compiled into this loader. */
2472
+ export const WASM_MEMORY: Readonly<{
2473
+ initialPages: number
2474
+ maximumPages: number
2475
+ pageBytes: number
2476
+ initialBytes: number
2477
+ maximumBytes: number
2478
+ }>
2479
+
2480
+ export function getDeferredRuntimeStats(): Readonly<WasiRuntimeStats>
2481
+
1828
2482
  export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
1829
- export function createInstance(wasmInput: WasiModuleInput): Promise<WasiInstance>
2483
+ export function createInstance(
2484
+ wasmInput: WasiModuleInput,
2485
+ options?: WasiInstanceOptions,
2486
+ ): Promise<WasiInstance>
1830
2487
  /** Dispose the singleton and retry retained failed-initialization cleanup. */
1831
2488
  export function dispose(): Promise<void>
2489
+
2490
+ /** The WASI flavor this deferred loader instantiates. */
2491
+ export declare const __napiBindingTarget: '${platformArchABI}'
1832
2492
  `
1833
2493
 
1834
2494
  export const createWasiBinding = (
@@ -1842,7 +2502,20 @@ export const createWasiBinding = (
1842
2502
  // wasm artifact.
1843
2503
  platformArchABI = 'wasm32-wasi',
1844
2504
  packageWasmFileName = wasmFileName,
2505
+ asyncRuntime = false,
1845
2506
  ) => {
2507
+ const asyncRuntimeImport = asyncRuntime
2508
+ ? `const {
2509
+ installCurrentThreadHosts: __installCurrentThreadHosts,
2510
+ } = require('@napi-rs/async-runtime')
2511
+ `
2512
+ : ''
2513
+ const installAsyncRuntimeHosts = asyncRuntime
2514
+ ? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
2515
+ __napiModule.exports,
2516
+ )
2517
+ `
2518
+ : ''
1846
2519
  const workerImports = threads
1847
2520
  ? `const { Worker } = require('node:worker_threads')
1848
2521
  `
@@ -1955,10 +2628,10 @@ function __createWasiWorker(filename) {
1955
2628
  }
1956
2629
  })(),
1957
2630
  reuseWorker: true,
1958
- plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
2631
+ plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
1959
2632
  `
1960
2633
  : ` asyncWorkPoolSize: 0,
1961
- plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
2634
+ plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
1962
2635
  `
1963
2636
  // Every build links a "basic" emnapi archive without the C async-work and
1964
2637
  // threadsafe-function implementations (the `emnapi-napi-rs(-mt)` archives shipped by the emnapi package), so the
@@ -1996,6 +2669,10 @@ function __createWasiWorker(filename) {
1996
2669
  }
1997
2670
 
1998
2671
  worker.unref()
2672
+ // These stubs stay in place for the worker's whole life, disposal
2673
+ // included: \`__keepEventLoopAliveUntil\` is what holds the process open
2674
+ // while a termination is pending, precisely because a worker's own
2675
+ // references cannot be relied on for it.
1999
2676
  }
2000
2677
  return worker
2001
2678
  },
@@ -2005,6 +2682,9 @@ function __createWasiWorker(filename) {
2005
2682
  return `/* eslint-disable */
2006
2683
  /* auto-generated by NAPI-RS */
2007
2684
 
2685
+ const __napiBindingTarget = '${platformArchABI}'
2686
+ ${BINDING_TARGET_STAMP_HELPER}
2687
+
2008
2688
  const __nodeFs = require('node:fs')
2009
2689
  const __nodePath = require('node:path')
2010
2690
  const { WASI: __nodeWASI } = require('node:wasi')
@@ -2016,6 +2696,7 @@ ${workerRuntimeImport}\
2016
2696
  instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync,
2017
2697
  } = require('@napi-rs/wasm-runtime')
2018
2698
  const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
2699
+ ${asyncRuntimeImport}\
2019
2700
  ${workerExecArgv}\
2020
2701
 
2021
2702
  const __cwd = process.cwd()
@@ -2059,7 +2740,7 @@ if (__nodeFs.existsSync(__wasmDebugFilePath)) {
2059
2740
 
2060
2741
  const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
2061
2742
  let __emnapiContext
2062
- ${emnapiContextLifecycle}
2743
+ ${createEmnapiContextLifecycle(asyncRuntime)}
2063
2744
  const __wasiRollbackRegistrySymbol = Symbol.for('${WASI_ROLLBACK_REGISTRY_SYMBOL}')
2064
2745
  const __wasiRollbackRegistryKey =
2065
2746
  typeof __filename === 'string' ? __filename : __wasmFilePath
@@ -2233,7 +2914,11 @@ function __captureEmnapiAutoDestroyListener() {
2233
2914
  try {
2234
2915
  const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
2235
2916
  try {
2236
- __emnapiContext = __emnapiCreateContext({ autoDestroy: false })
2917
+ __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
2918
+ __emnapiCreateContext({ autoDestroy: false }),
2919
+ __prepareWasmEnvCleanup,
2920
+ __isPreparingWasmEnvCleanup,
2921
+ )
2237
2922
  // emnapi 2.x still registers an unconditional once-listener for
2238
2923
  // beforeExit that auto-destroys the context, and suppressDestroy() only
2239
2924
  // neutralizes its callback without removing it. This loader owns cleanup
@@ -2273,6 +2958,23 @@ ${workerOption}\
2273
2958
  },
2274
2959
  }))
2275
2960
  __publishWasiDispose(__napiModule.exports)
2961
+ ${installAsyncRuntimeHosts}\
2962
+ // The CommonJS tail below aliases \`__napiModule.exports\`; a named module
2963
+ // export does not travel with it, so carry the marker on the binding itself
2964
+ // too. Three things pin the stamp to exactly this spot:
2965
+ // - inside this \`try\`, because the guard throws on a
2966
+ // \`#[napi(module_exports)]\` hook that claimed the name, and only the
2967
+ // catch below tears the environment — context, workers, exit listener —
2968
+ // back down;
2969
+ // - after the async runtime host install, which hands this same object to
2970
+ // addon-provided registration functions that may put anything on it;
2971
+ // - assigning onto the loader's own \`module.exports\`, which is still the
2972
+ // original object here, so an addon accessor with a refusing setter is
2973
+ // never written through. \`cjs-module-lexer\` — Node's CJS -> ESM named
2974
+ // export detection — reads the static \`module.exports.<name> =\` either
2975
+ // way, and the later \`module.exports = __napiModule.exports\` does not
2976
+ // undo that.
2977
+ module.exports.${NAPI_BINDING_TARGET_EXPORT} = ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
2276
2978
  __registerWasiExitListener()
2277
2979
  } catch (error) {
2278
2980
  const rollback = {