@napi-rs/cli 3.9.1 → 3.10.0

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,139 @@
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()
7
132
  let __napiInstance
8
133
  let __emnapiContextDestroyed = false
9
134
  let __emnapiContextDestroyPromise
10
135
  let __emnapiWasmEnvCleanupPrepared = false
136
+ let __emnapiWasmEnvCleanupPreparing = false
11
137
  let __emnapiWasmEnvCleanupRan = false
12
138
  let __emnapiWasmEnvCleanupDrained = false
13
139
  let __emnapiWasmEnvCleanupDrainPromise
@@ -18,7 +144,7 @@ let __completeWasiDisposal = function () {}
18
144
  // that stopped short of destroying the context. See
19
145
  // \`__rollbackWasiInitialization\`.
20
146
  let __retainWasiRollbackForRetry = function () {}
21
-
147
+ ${currentThreadHosts}
22
148
  function __isThenable(value) {
23
149
  return (
24
150
  value !== null &&
@@ -80,14 +206,26 @@ function __attachCleanupErrors(error, cleanupErrors) {
80
206
  } catch {}
81
207
  return aggregate
82
208
  }
209
+ ${emnapiContextDestroyWrapper}
210
+ function __isPreparingWasmEnvCleanup() {
211
+ return __emnapiWasmEnvCleanupPreparing
212
+ }
83
213
 
84
214
  function __prepareWasmEnvCleanup() {
85
- if (__emnapiWasmEnvCleanupPrepared) {
215
+ if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
86
216
  return
87
217
  }
88
218
  const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
89
219
  if (typeof prepare === 'function') {
90
- prepare()
220
+ // The addon settles the promises it cancels synchronously, under a
221
+ // non-reentrant lifecycle mutex: anything a promise hook calls from in
222
+ // here must not reach this export again.
223
+ __emnapiWasmEnvCleanupPreparing = true
224
+ try {
225
+ prepare()
226
+ } finally {
227
+ __emnapiWasmEnvCleanupPreparing = false
228
+ }
91
229
  __emnapiWasmEnvCleanupRan = true
92
230
  }
93
231
  __emnapiWasmEnvCleanupPrepared = true
@@ -259,6 +397,7 @@ function __destroyEmnapiContext() {
259
397
  return __emnapiContextDestroyPromise
260
398
  }
261
399
 
400
+ ${disposeCurrentThreadHosts}\
262
401
  __prepareWasmEnvCleanup()
263
402
  const result = __emnapiContext.destroy()
264
403
  if (!__isThenable(result)) {
@@ -513,6 +652,7 @@ function __rollbackWasiInitialization() {
513
652
  return __destroyContextForWasiRollback(cleanupErrors)
514
653
  }
515
654
  `
655
+ }
516
656
 
517
657
  export const createWasiBrowserBinding = (
518
658
  wasiFilename: string,
@@ -523,11 +663,24 @@ export const createWasiBrowserBinding = (
523
663
  buffer = false,
524
664
  errorEvent = false,
525
665
  threads = true,
666
+ // `platformArchABI` of the flavor this loader belongs to. Defaults from
667
+ // `threads` so callers that predate the parameter keep their identity.
668
+ platformArchABI = threads ? 'wasm32-wasi' : 'wasm32-wasip1',
669
+ asyncRuntime = false,
526
670
  ) => {
527
671
  // Threaded builds always get a pre-created worker pool (see
528
672
  // `reuseWorkerOption` below), and pool pre-creation is asynchronous, so
529
673
  // they always initialize asynchronously.
530
674
  const effectiveAsyncInit = asyncInit || threads
675
+ const asyncRuntimeImport = asyncRuntime
676
+ ? `import { installCurrentThreadHosts as __installCurrentThreadHosts } from '@napi-rs/async-runtime'\n`
677
+ : ''
678
+ const installAsyncRuntimeHosts = asyncRuntime
679
+ ? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
680
+ __napiModule.exports,
681
+ )
682
+ `
683
+ : ''
531
684
  const fsImport = fs
532
685
  ? buffer
533
686
  ? `import { memfs, Buffer } from '@napi-rs/wasm-runtime/fs'`
@@ -652,8 +805,11 @@ ${workerRuntimeImport}\
652
805
  WASI as __WASI,
653
806
  } from '@napi-rs/wasm-runtime'
654
807
  import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
808
+ ${asyncRuntimeImport}\
655
809
  ${fsImport}
656
810
  ${bufferImport}
811
+ export const __napiBindingTarget = '${platformArchABI}'
812
+ ${BINDING_TARGET_STAMP_HELPER}
657
813
  ${wasiCreation}
658
814
 
659
815
  const __wasmUrl = new URL('./${wasiFilename}.wasm', import.meta.url).href
@@ -677,12 +833,16 @@ ${threads ? ' shared: true,\n' : ''}\
677
833
  })
678
834
  ${workerPoolSizeBinding}\
679
835
  let __emnapiContext
680
- ${emnapiContextLifecycle}
836
+ ${createEmnapiContextLifecycle(asyncRuntime)}
681
837
  let __wasiModule
682
838
  let __napiModule
683
839
 
684
840
  try {
685
- __emnapiContext = __emnapiCreateContext({ autoDestroy: false })
841
+ __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
842
+ __emnapiCreateContext({ autoDestroy: false }),
843
+ __prepareWasmEnvCleanup,
844
+ __isPreparingWasmEnvCleanup,
845
+ )
686
846
  __emnapiContext.suppressDestroy()
687
847
  ${emnapiInjectBuffer}
688
848
  ;({
@@ -715,6 +875,13 @@ ${workerOption}\
715
875
  },
716
876
  }))
717
877
  __publishWasiDispose(__napiModule.exports)
878
+ ${installAsyncRuntimeHosts}\
879
+ // The default export hands out this object; a named module export does not
880
+ // travel with it, so carry the marker on the binding itself too. After the
881
+ // host install, which hands the same object to addon-provided registration
882
+ // functions that may put anything on it, and inside this \`try\`, so a claimed
883
+ // name fails the load through the rollback below rather than past it.
884
+ ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
718
885
  } catch (error) {
719
886
  const cleanupErrors = await __rollbackWasiInitialization()
720
887
  throw __attachCleanupErrors(error, cleanupErrors)
@@ -722,19 +889,232 @@ ${workerOption}\
722
889
  `
723
890
  }
724
891
 
892
+ /**
893
+ * Module-scope prelude of the deferred loader: the compiled-in memory
894
+ * descriptor, the per-module instance counters, and the resolver that turns a
895
+ * `createInstance()` options bag into the one `WebAssembly.Memory` that
896
+ * instance runs on.
897
+ *
898
+ * Nothing here allocates — workerd bans allocation in global scope, so the
899
+ * Memory itself is created inside `__createInstance`.
900
+ */
901
+ const DEFERRED_MEMORY_PREAMBLE = (
902
+ initialMemory: number,
903
+ maximumMemory: number,
904
+ ) => `
905
+ export const WASM_MEMORY = Object.freeze({
906
+ initialPages: ${initialMemory},
907
+ maximumPages: ${maximumMemory},
908
+ pageBytes: 65536,
909
+ initialBytes: ${initialMemory} * 65536,
910
+ maximumBytes: ${maximumMemory} * 65536,
911
+ })
912
+
913
+ let __createdInstances = 0
914
+ let __liveInstances = 0
915
+
916
+ /**
917
+ * Counters for instances created by THIS module evaluation, not process-wide:
918
+ * a second bundled copy of this loader keeps its own. Only successfully
919
+ * created instances are counted, and \`liveInstances\` drops when an instance's
920
+ * \`dispose()\` resolves.
921
+ *
922
+ * \`declaredInitialMemoryBytes\` is declared address space, not a host's
923
+ * committed-memory metric; pair it with host telemetry rather than treating it
924
+ * as a quota.
925
+ */
926
+ export function getDeferredRuntimeStats() {
927
+ return Object.freeze({
928
+ createdInstances: __createdInstances,
929
+ liveInstances: __liveInstances,
930
+ declaredInitialMemoryBytes: WASM_MEMORY.initialBytes,
931
+ })
932
+ }
933
+
934
+ const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor(
935
+ ArrayBuffer.prototype,
936
+ 'byteLength',
937
+ ).get
938
+ const __memoryBufferGetter = Object.getOwnPropertyDescriptor(
939
+ WebAssembly.Memory.prototype,
940
+ 'buffer',
941
+ ).get
942
+ // One managed initialization per Memory, success or failure: an attempt that
943
+ // throws may already have written into linear memory, so the bytes are not a
944
+ // clean slate for a second instance. Module-local, like the counters above.
945
+ const __claimedMemories = new WeakSet()
946
+
947
+ function __resolveInstanceMemory(__options) {
948
+ const __provided = __options == null ? undefined : __options.memory
949
+ if (__provided === undefined || __provided === null) {
950
+ // Page counts are handed to the engine unvalidated: it already rejects a
951
+ // negative, over-4GiB or below-maximum value with a precise message, and a
952
+ // second set of bounds here would only drift from it.
953
+ const __allocated = new WebAssembly.Memory({
954
+ initial:
955
+ __options != null && __options.initialMemoryPages !== undefined
956
+ ? __options.initialMemoryPages
957
+ : WASM_MEMORY.initialPages,
958
+ maximum:
959
+ __options != null && __options.maximumMemoryPages !== undefined
960
+ ? __options.maximumMemoryPages
961
+ : WASM_MEMORY.maximumPages,
962
+ })
963
+ // Claimed like a caller-provided one. The handle publishes it as
964
+ // \`instance.memory\`, so handing it back to \`createInstance()\` is as easy
965
+ // as passing your own twice, and it would put two live instances on one
966
+ // linear memory: each initialization rewrites the emnapi/WASI state the
967
+ // other is still running on.
968
+ __claimedMemories.add(__allocated)
969
+ return __allocated
970
+ }
971
+ if (
972
+ __options.initialMemoryPages !== undefined ||
973
+ __options.maximumMemoryPages !== undefined
974
+ ) {
975
+ throw new TypeError(
976
+ 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both',
977
+ )
978
+ }
979
+ let __buffer
980
+ try {
981
+ // Brand check: the getter throws for anything that is not a genuine
982
+ // WebAssembly.Memory, including a cross-realm look-alike object.
983
+ __buffer = Reflect.apply(__memoryBufferGetter, __provided, [])
984
+ } catch {
985
+ throw new TypeError('memory must be an unshared WebAssembly.Memory')
986
+ }
987
+ try {
988
+ // Throws for a SharedArrayBuffer. This loader has no threads, and shared
989
+ // growth does not detach: external views handed to the addon would
990
+ // silently outlive the bytes they describe.
991
+ Reflect.apply(__arrayBufferByteLengthGetter, __buffer, [])
992
+ } catch {
993
+ throw new TypeError(
994
+ 'The deferred loader requires an unshared WebAssembly.Memory',
995
+ )
996
+ }
997
+ // The intrinsic getters above accept a genuine Memory from ANY realm, but
998
+ // the loader's dependencies do not: \`WASI.setMemory\` in
999
+ // \`@napi-rs/wasm-runtime\` and emnapi identify a Memory with a realm-local
1000
+ // \`instanceof\`. A Memory built in another realm (a \`node:vm\` context, a
1001
+ // same-origin iframe) would pass every check here and only fail deep inside
1002
+ // initialization. Reject it up front, and before the claim below, so the
1003
+ // caller keeps it usable in the realm that made it.
1004
+ if (!(__provided instanceof WebAssembly.Memory)) {
1005
+ throw new TypeError(
1006
+ 'memory must be a WebAssembly.Memory created in the same realm as this loader',
1007
+ )
1008
+ }
1009
+ if (__claimedMemories.has(__provided)) {
1010
+ throw new TypeError(
1011
+ 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal',
1012
+ )
1013
+ }
1014
+ // Last step, after every check: a rejected option bag must leave the Memory
1015
+ // unclaimed, or a caller could not fix the call and retry with it.
1016
+ __claimedMemories.add(__provided)
1017
+ return __provided
1018
+ }
1019
+ `
1020
+
725
1021
  export const createWasiDeferredBrowserBinding = (
726
1022
  wasiFilename: string,
1023
+ // Fed by `napi.wasm.threadlessInitialMemory ?? napi.wasm.initialMemory`.
727
1024
  // 64 MiB leaves headroom for JS/runtime state under workerd's 128 MiB
728
1025
  // isolate limit. The regular Node/browser loaders retain their historical
729
1026
  // 4,000-page default.
730
1027
  initialMemory = 1024,
731
1028
  maximumMemory = 65536,
732
1029
  buffer = false,
1030
+ // Deferred loaders are only emitted for non-threaded flavors, so the
1031
+ // default matches the only flavor `napi build` generates one for.
1032
+ platformArchABI = 'wasm32-wasip1',
1033
+ asyncRuntime = false,
733
1034
  ) => {
734
1035
  const bufferImport = buffer ? `import { Buffer } from 'buffer'` : ''
735
1036
  const emnapiInjectBuffer = buffer
736
1037
  ? ' __emnapiContext.features.Buffer = Buffer\n'
737
1038
  : ''
1039
+ // This flavor creates N independent instances per realm, each with its own
1040
+ // emnapi context and its own `napiModule.exports`, so it uses the
1041
+ // per-instance helpers rather than `installCurrentThreadHosts`: those return
1042
+ // exact, idempotent disposers with no realm-global dedup, roll themselves
1043
+ // back on a setup failure, and degrade to a no-op disposer when the realm has
1044
+ // no `setTimeout`/`clearTimeout`.
1045
+ // The `/workerd` subpath, not the barrel: the barrel's `index.cjs` also
1046
+ // requires `current-thread-hosts.cjs`, whose realm-global registry and Node
1047
+ // timer-handle bookkeeping this flavor never executes, and a CJS barrel is
1048
+ // not tree-shakeable out of a worker bundle.
1049
+ const asyncRuntimeImport = asyncRuntime
1050
+ ? `import {
1051
+ registerWorkerdCurrentThreadTaskHost as __registerWorkerdCurrentThreadTaskHost,
1052
+ registerWorkerdTimerHost as __registerWorkerdTimerHost,
1053
+ } from '@napi-rs/async-runtime/workerd'
1054
+ `
1055
+ : ''
1056
+ // `__createManagedEmnapiContext` calls `__prepareEnvCleanup?.()` on EVERY
1057
+ // destroy path (dispose(), managed beforeExit, module lifecycle), so a second
1058
+ // hook next to it covers them all with one edit.
1059
+ const managedHostDisposeParam = asyncRuntime ? ' __disposeHosts,\n' : ''
1060
+ const managedHostDisposeCall = asyncRuntime
1061
+ ? ` __disposeHosts?.()\n`
1062
+ : ''
1063
+ const instanceHostState = asyncRuntime
1064
+ ? ` let __disposeInstanceHosts
1065
+ const __reportInstanceHostDisposalError = (__error) => {
1066
+ try {
1067
+ const __consoleHost = globalThis.console
1068
+ if (__consoleHost && typeof __consoleHost.error === 'function') {
1069
+ __consoleHost.error(__error)
1070
+ }
1071
+ } catch {}
1072
+ }
1073
+ // Runs between the settlement drain and \`Context.destroy()\`; never throws,
1074
+ // for the same reason the eager loaders' disposer does not.
1075
+ const __disposeHostsBeforeDestroy = () => {
1076
+ const __dispose = __disposeInstanceHosts
1077
+ if (__dispose === undefined) {
1078
+ return
1079
+ }
1080
+ __disposeInstanceHosts = undefined
1081
+ __dispose()
1082
+ }
1083
+ `
1084
+ : ''
1085
+ const installInstanceHosts = asyncRuntime
1086
+ ? ` const __disposeTaskHost = __registerWorkerdCurrentThreadTaskHost(
1087
+ __napiModule.exports,
1088
+ )
1089
+ try {
1090
+ const __disposeTimerHost = __registerWorkerdTimerHost(
1091
+ __napiModule.exports,
1092
+ )
1093
+ __disposeInstanceHosts = () => {
1094
+ try {
1095
+ __disposeTimerHost()
1096
+ } catch (__error) {
1097
+ __reportInstanceHostDisposalError(__error)
1098
+ }
1099
+ try {
1100
+ __disposeTaskHost()
1101
+ } catch (__error) {
1102
+ __reportInstanceHostDisposalError(__error)
1103
+ }
1104
+ }
1105
+ } catch (__error) {
1106
+ try {
1107
+ __disposeTaskHost()
1108
+ } catch (__cleanupError) {
1109
+ __attachCleanupError(__error, __cleanupError)
1110
+ }
1111
+ throw __error
1112
+ }
1113
+ `
1114
+ : ''
1115
+ const managedHostDisposeArg = asyncRuntime
1116
+ ? ' __disposeHostsBeforeDestroy,\n'
1117
+ : ''
738
1118
  return `import {
739
1119
  emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,
740
1120
  emnapiTSFNPlugin as __emnapiTSFNPlugin,
@@ -742,7 +1122,11 @@ export const createWasiDeferredBrowserBinding = (
742
1122
  WASI as __WASI,
743
1123
  } from '@napi-rs/wasm-runtime'
744
1124
  import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
1125
+ ${asyncRuntimeImport}\
745
1126
  ${bufferImport}
1127
+ ${DEFERRED_MEMORY_PREAMBLE(initialMemory, maximumMemory)}
1128
+ export const __napiBindingTarget = '${platformArchABI}'
1129
+ ${BINDING_TARGET_STAMP_HELPER}
746
1130
 
747
1131
  /**
748
1132
  * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes.
@@ -841,7 +1225,7 @@ async function __normalizeModuleForEmnapi(__module) {
841
1225
  'provide structuredClone or MessageChannel support.',
842
1226
  )
843
1227
  }
844
-
1228
+ ${emnapiContextDestroyWrapper}
845
1229
  function __captureEmnapiAutoDestroyListener(__process) {
846
1230
  if (
847
1231
  !__process ||
@@ -1229,7 +1613,10 @@ function __registerManagedEmnapiContext(__process, __destroy) {
1229
1613
  }
1230
1614
  }
1231
1615
 
1232
- async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1616
+ async function __createManagedEmnapiContext(
1617
+ __prepareEnvCleanup,
1618
+ __isPreparingEnvCleanup,
1619
+ ${managedHostDisposeParam}) {
1233
1620
  const __process =
1234
1621
  typeof process === 'object' && process !== null ? process : undefined
1235
1622
  const __finishAutoDestroyCapture =
@@ -1238,7 +1625,11 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1238
1625
  let __contextInitializationError
1239
1626
  let __contextInitializationFailed = false
1240
1627
  try {
1241
- __emnapiContext = __emnapiCreateContext({ autoDestroy: false })
1628
+ __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
1629
+ __emnapiCreateContext({ autoDestroy: false }),
1630
+ __prepareEnvCleanup,
1631
+ __isPreparingEnvCleanup,
1632
+ )
1242
1633
  // emnapi 2.x still registers an unconditional process.once('beforeExit')
1243
1634
  // auto-destroy listener on Node hosts, and suppressDestroy() only
1244
1635
  // neutralizes its callback without removing it. This loader must stay
@@ -1288,6 +1679,18 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1288
1679
  // Context.destroy() disables JS before cleanup hooks run, so settle
1289
1680
  // runtime-owned promises while this environment can still call JS.
1290
1681
  __prepareEnvCleanup?.()
1682
+ if (__isPreparingEnvCleanup?.()) {
1683
+ // Reached from inside the barrier, so \`Context.destroy()\` below would
1684
+ // hit the wrapper's in-flight no-op. Recording that as a completed
1685
+ // destroy is what makes the frame that *did* start the barrier skip the
1686
+ // real one afterwards, leaving the context retained with its cleanup
1687
+ // hooks unrun. Refuse instead: nothing is flagged, the context stays
1688
+ // registered for managed beforeExit cleanup, and a later destroy still
1689
+ // works. dispose() coalesces reentrancy before it can get here, so this
1690
+ // is the backstop for any other caller that manages to.
1691
+ throw __createLifecycleReentryError('dispose')
1692
+ }
1693
+ ${managedHostDisposeCall}\
1291
1694
  __result = __emnapiContext.destroy()
1292
1695
  } catch (error) {
1293
1696
  __finishDestroyInvocation()
@@ -1392,6 +1795,7 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
1392
1795
 
1393
1796
  async function __createInstance(
1394
1797
  __wasmInput,
1798
+ __options,
1395
1799
  __beforeExitDestroy,
1396
1800
  __onManagedDestroyer,
1397
1801
  ) {
@@ -1401,31 +1805,40 @@ async function __createInstance(
1401
1805
  version: 'preview1',
1402
1806
  })
1403
1807
  // 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
- })
1808
+ // provided. It is resolved here in function scope (workerd bans global scope
1809
+ // allocation) and is never shared (no threads, no SharedArrayBuffer).
1810
+ // Resolve it before the emnapi context so a rejected option bag or a host
1811
+ // memory-limit failure cannot leak a context that never reaches
1812
+ // instantiation.
1813
+ const __wasmMemory = __resolveInstanceMemory(__options)
1412
1814
  let __lifecycleState = 'pending'
1413
1815
  let __destroyEmnapiContext
1414
1816
  let __destroyOwnedContext
1415
1817
  let __destroyManagedOwnedContext
1416
1818
  let __napiInstance
1819
+ ${instanceHostState}\
1417
1820
  let __wasmEnvCleanupRan = false
1418
1821
  let __wasmEnvCleanupPrepared = false
1822
+ let __wasmEnvCleanupPreparing = false
1419
1823
  let __wasmEnvCleanupDrained = false
1420
1824
  let __wasmEnvCleanupDrainPromise
1825
+ const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing
1421
1826
  const __prepareEnvCleanup = () => {
1422
- if (__wasmEnvCleanupPrepared) {
1827
+ if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) {
1423
1828
  return
1424
1829
  }
1425
1830
  const __prepareWasmEnvCleanup =
1426
1831
  __napiInstance?.exports.napi_prepare_wasm_env_cleanup
1427
1832
  if (typeof __prepareWasmEnvCleanup === 'function') {
1428
- __prepareWasmEnvCleanup()
1833
+ // The addon settles the promises it cancels synchronously, under a
1834
+ // non-reentrant lifecycle mutex: anything a promise hook calls from in
1835
+ // here must not reach this export again.
1836
+ __wasmEnvCleanupPreparing = true
1837
+ try {
1838
+ __prepareWasmEnvCleanup()
1839
+ } finally {
1840
+ __wasmEnvCleanupPreparing = false
1841
+ }
1429
1842
  __wasmEnvCleanupRan = true
1430
1843
  }
1431
1844
  __wasmEnvCleanupPrepared = true
@@ -1472,6 +1885,63 @@ async function __createInstance(
1472
1885
  __wasmEnvCleanupDrainPromise = __tracked
1473
1886
  return __tracked
1474
1887
  }
1888
+ let __disposed = false
1889
+ const __runInstanceDisposal = async () => {
1890
+ if (__lifecycleState !== 'failed') {
1891
+ __lifecycleState = 'disposal'
1892
+ }
1893
+ // Settle what the barrier cancelled before the environment stops
1894
+ // accepting JavaScript calls. Undefined unless something is queued, so
1895
+ // an idle disposal is not delayed by a single turn.
1896
+ const __drained = __prepareForDisposal()
1897
+ if (__drained) {
1898
+ await __drained
1899
+ }
1900
+ const __result = await (__beforeExitDestroy
1901
+ ? __destroyManagedOwnedContext()
1902
+ : __destroyOwnedContext())
1903
+ // Only a completed destroy retires the instance; a throw above leaves
1904
+ // the counter untouched so a retried dispose() cannot double-decrement.
1905
+ if (!__disposed) {
1906
+ __disposed = true
1907
+ __liveInstances -= 1
1908
+ }
1909
+ return __result
1910
+ }
1911
+ let __instanceDisposePromise
1912
+ /**
1913
+ * The disposal frame runs the barrier, and the barrier settles the promises it
1914
+ * cancels synchronously — so a promise hook firing inside it can call this
1915
+ * same instance's dispose() again while the first call is still in its drain.
1916
+ * That nested call finds the barrier flagged in flight, prepares nothing,
1917
+ * drains nothing, and falls straight through to the context destroyer, whose
1918
+ * \`Context.destroy()\` hits the wrapper's in-flight no-op. It would record a
1919
+ * destruction that never happened, and the outer frame would then skip the
1920
+ * real one: both disposals resolve, no cleanup hook runs, the context stays
1921
+ * retained.
1922
+ *
1923
+ * Memoize before any of that starts, exactly like the eager loaders'
1924
+ * \`__disposeWasiBinding\`, so there is only ever one disposal frame per
1925
+ * instance and a reentrant caller awaits it instead of racing it. Cleared on
1926
+ * rejection: a drain that failed has to stay retryable.
1927
+ */
1928
+ const __disposeInstance = () => {
1929
+ if (__instanceDisposePromise) {
1930
+ return __instanceDisposePromise
1931
+ }
1932
+ let __resolveDispose
1933
+ let __rejectDispose
1934
+ const __disposePromise = new Promise((__resolve, __reject) => {
1935
+ __resolveDispose = __resolve
1936
+ __rejectDispose = __reject
1937
+ })
1938
+ __instanceDisposePromise = __disposePromise
1939
+ __runInstanceDisposal().then(__resolveDispose, (__error) => {
1940
+ __instanceDisposePromise = undefined
1941
+ __rejectDispose(__error)
1942
+ })
1943
+ return __disposePromise
1944
+ }
1475
1945
  const __destroyBeforeExit = __beforeExitDestroy
1476
1946
  ? async () => {
1477
1947
  if (__lifecycleState === 'failed') {
@@ -1497,7 +1967,10 @@ async function __createInstance(
1497
1967
  destroy,
1498
1968
  destroyForModuleLifecycle,
1499
1969
  registerCleanup: __registerCleanup,
1500
- } = await __createManagedEmnapiContext(__prepareEnvCleanup)
1970
+ } = await __createManagedEmnapiContext(
1971
+ __prepareEnvCleanup,
1972
+ __isPreparingEnvCleanup,
1973
+ ${managedHostDisposeArg} )
1501
1974
  __destroyEmnapiContext = destroy
1502
1975
  __destroyOwnedContext = () => __destroyEmnapiContext()
1503
1976
  __destroyManagedOwnedContext = destroyForModuleLifecycle
@@ -1534,26 +2007,34 @@ ${emnapiInjectBuffer}\
1534
2007
  }
1535
2008
  },
1536
2009
  }))
2010
+ ${installInstanceHosts}\
2011
+ // \`instantiate()\` and \`createInstance().exports\` hand out this object; a
2012
+ // named module export does not travel with it. After the instance host
2013
+ // install, which hands the same object to addon-provided registration
2014
+ // functions that may put anything on it, and inside this \`try\`, so a
2015
+ // claimed name flips \`__lifecycleState\` to 'failed' and tears the instance
2016
+ // down rather than escaping a half-built one.
2017
+ ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
1537
2018
  if (__lifecycleState === 'pending') {
1538
2019
  __lifecycleState = 'succeeded'
1539
2020
  }
2021
+ __createdInstances += 1
2022
+ __liveInstances += 1
1540
2023
  return {
1541
2024
  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()
2025
+ get memory() {
2026
+ return __wasmMemory
1556
2027
  },
2028
+ get memoryBytes() {
2029
+ // The Memory outlives the environment, so this stays readable after a
2030
+ // FAILED dispose() (which leaves the instance undisposed and
2031
+ // retryable). It reports 0 only once disposal has actually completed.
2032
+ return __disposed ? 0 : __wasmMemory.buffer.byteLength
2033
+ },
2034
+ get disposed() {
2035
+ return __disposed
2036
+ },
2037
+ dispose: __disposeInstance,
1557
2038
  }
1558
2039
  } catch (error) {
1559
2040
  __lifecycleState = 'failed'
@@ -1625,11 +2106,22 @@ ${emnapiInjectBuffer}\
1625
2106
  }
1626
2107
 
1627
2108
  /**
1628
- * Create an independent instance. Call dispose() when the instance is no
1629
- * longer needed so emnapi cleanup hooks run deterministically.
2109
+ * Create an independent instance. Call and await dispose() when the instance
2110
+ * is no longer needed so emnapi cleanup hooks run deterministically.
2111
+ *
2112
+ * The optional second argument selects this instance's linear memory: either
2113
+ * \`memory\` (an unshared, single-use WebAssembly.Memory you allocated) or
2114
+ * \`initialMemoryPages\` / \`maximumMemoryPages\`, never both. Omitted, the
2115
+ * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages.
2116
+ *
2117
+ * A provided Memory must come from this loader's own realm: the WASI and
2118
+ * emnapi layers underneath identify one with a realm-local \`instanceof\`, so a
2119
+ * Memory built in a \`node:vm\` context or another frame is rejected. Every
2120
+ * Memory an instance runs on is single-use, the loader-allocated one included:
2121
+ * \`instance.memory\` cannot be recycled into a second \`createInstance()\`.
1630
2122
  */
1631
- export async function createInstance(__wasmInput) {
1632
- return __createInstance(__wasmInput)
2123
+ export async function createInstance(__wasmInput, __options) {
2124
+ return __createInstance(__wasmInput, __options)
1633
2125
  }
1634
2126
 
1635
2127
  let __defaultModulePromise
@@ -1666,6 +2158,7 @@ export function instantiate(__wasmInput) {
1666
2158
  const __instancePromise = __modulePromise.then((__module) =>
1667
2159
  __createInstance(
1668
2160
  __module,
2161
+ undefined,
1669
2162
  __disposeDefaultInstance,
1670
2163
  (__managedDestroyer) => {
1671
2164
  __defaultManagedDestroyers.set(
@@ -1814,21 +2307,90 @@ export function dispose() {
1814
2307
 
1815
2308
  export const createWasiDeferredBrowserBindingTypeDef = (
1816
2309
  packageName: string,
2310
+ platformArchABI = 'wasm32-wasip1',
1817
2311
  ) => `export type WasiBinding = typeof import('${packageName}')
1818
2312
 
1819
2313
  export type WasiModuleInput =
1820
2314
  | WebAssembly.Module
1821
2315
  | PromiseLike<WebAssembly.Module>
1822
2316
 
2317
+ /** Run the instance on a linear memory the caller allocated. */
2318
+ export interface WasiCallerMemoryOptions {
2319
+ /**
2320
+ * A caller-allocated linear memory for this instance. It must be unshared
2321
+ * and created in this loader's own realm — the WASI and emnapi layers
2322
+ * underneath identify a Memory with a realm-local \`instanceof\`, so one from
2323
+ * a \`node:vm\` context or another frame is rejected. It is single-use: once
2324
+ * a validated initialization attempt has begun, the same Memory cannot be
2325
+ * passed again — including after that attempt failed, and after the instance
2326
+ * was disposed.
2327
+ */
2328
+ memory: WebAssembly.Memory
2329
+ /** Not available beside \`memory\`: the loader allocates neither. */
2330
+ initialMemoryPages?: never
2331
+ /** Not available beside \`memory\`: the loader allocates neither. */
2332
+ maximumMemoryPages?: never
2333
+ }
2334
+
2335
+ /** Let the loader allocate the linear memory, optionally sized. */
2336
+ export interface WasiAllocatedMemoryOptions {
2337
+ /** Not available beside the page counts: they size the loader's own Memory. */
2338
+ memory?: never
2339
+ /** @default WASM_MEMORY.initialPages */
2340
+ initialMemoryPages?: number
2341
+ /** @default WASM_MEMORY.maximumPages */
2342
+ maximumMemoryPages?: number
2343
+ }
2344
+
2345
+ /**
2346
+ * Either memory form, never a mix of the two: the loader throws a TypeError
2347
+ * on \`memory\` beside a page count. \`{}\` and an omitted argument select the
2348
+ * loader defaults.
2349
+ */
2350
+ export type WasiInstanceOptions =
2351
+ | WasiCallerMemoryOptions
2352
+ | WasiAllocatedMemoryOptions
2353
+
2354
+ export interface WasiRuntimeStats {
2355
+ /** Instances created by this evaluated loader module, not process-wide. */
2356
+ createdInstances: number
2357
+ /** Created instances whose dispose() has not completed. */
2358
+ liveInstances: number
2359
+ /** Declared initial address space, not committed memory. */
2360
+ declaredInitialMemoryBytes: number
2361
+ }
2362
+
1823
2363
  export interface WasiInstance {
1824
2364
  readonly exports: WasiBinding
2365
+ /** This instance's linear memory. Claimed, so it cannot start another one. */
2366
+ readonly memory: WebAssembly.Memory
2367
+ /** Current linear-memory size; 0 once dispose() has completed. */
2368
+ readonly memoryBytes: number
2369
+ readonly disposed: boolean
1825
2370
  dispose(): Promise<void>
1826
2371
  }
1827
2372
 
2373
+ /** The memory descriptor compiled into this loader. */
2374
+ export const WASM_MEMORY: Readonly<{
2375
+ initialPages: number
2376
+ maximumPages: number
2377
+ pageBytes: number
2378
+ initialBytes: number
2379
+ maximumBytes: number
2380
+ }>
2381
+
2382
+ export function getDeferredRuntimeStats(): Readonly<WasiRuntimeStats>
2383
+
1828
2384
  export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
1829
- export function createInstance(wasmInput: WasiModuleInput): Promise<WasiInstance>
2385
+ export function createInstance(
2386
+ wasmInput: WasiModuleInput,
2387
+ options?: WasiInstanceOptions,
2388
+ ): Promise<WasiInstance>
1830
2389
  /** Dispose the singleton and retry retained failed-initialization cleanup. */
1831
2390
  export function dispose(): Promise<void>
2391
+
2392
+ /** The WASI flavor this deferred loader instantiates. */
2393
+ export declare const __napiBindingTarget: '${platformArchABI}'
1832
2394
  `
1833
2395
 
1834
2396
  export const createWasiBinding = (
@@ -1842,7 +2404,20 @@ export const createWasiBinding = (
1842
2404
  // wasm artifact.
1843
2405
  platformArchABI = 'wasm32-wasi',
1844
2406
  packageWasmFileName = wasmFileName,
2407
+ asyncRuntime = false,
1845
2408
  ) => {
2409
+ const asyncRuntimeImport = asyncRuntime
2410
+ ? `const {
2411
+ installCurrentThreadHosts: __installCurrentThreadHosts,
2412
+ } = require('@napi-rs/async-runtime')
2413
+ `
2414
+ : ''
2415
+ const installAsyncRuntimeHosts = asyncRuntime
2416
+ ? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
2417
+ __napiModule.exports,
2418
+ )
2419
+ `
2420
+ : ''
1846
2421
  const workerImports = threads
1847
2422
  ? `const { Worker } = require('node:worker_threads')
1848
2423
  `
@@ -2005,6 +2580,9 @@ function __createWasiWorker(filename) {
2005
2580
  return `/* eslint-disable */
2006
2581
  /* auto-generated by NAPI-RS */
2007
2582
 
2583
+ const __napiBindingTarget = '${platformArchABI}'
2584
+ ${BINDING_TARGET_STAMP_HELPER}
2585
+
2008
2586
  const __nodeFs = require('node:fs')
2009
2587
  const __nodePath = require('node:path')
2010
2588
  const { WASI: __nodeWASI } = require('node:wasi')
@@ -2016,6 +2594,7 @@ ${workerRuntimeImport}\
2016
2594
  instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync,
2017
2595
  } = require('@napi-rs/wasm-runtime')
2018
2596
  const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
2597
+ ${asyncRuntimeImport}\
2019
2598
  ${workerExecArgv}\
2020
2599
 
2021
2600
  const __cwd = process.cwd()
@@ -2059,7 +2638,7 @@ if (__nodeFs.existsSync(__wasmDebugFilePath)) {
2059
2638
 
2060
2639
  const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
2061
2640
  let __emnapiContext
2062
- ${emnapiContextLifecycle}
2641
+ ${createEmnapiContextLifecycle(asyncRuntime)}
2063
2642
  const __wasiRollbackRegistrySymbol = Symbol.for('${WASI_ROLLBACK_REGISTRY_SYMBOL}')
2064
2643
  const __wasiRollbackRegistryKey =
2065
2644
  typeof __filename === 'string' ? __filename : __wasmFilePath
@@ -2233,7 +2812,11 @@ function __captureEmnapiAutoDestroyListener() {
2233
2812
  try {
2234
2813
  const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
2235
2814
  try {
2236
- __emnapiContext = __emnapiCreateContext({ autoDestroy: false })
2815
+ __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
2816
+ __emnapiCreateContext({ autoDestroy: false }),
2817
+ __prepareWasmEnvCleanup,
2818
+ __isPreparingWasmEnvCleanup,
2819
+ )
2237
2820
  // emnapi 2.x still registers an unconditional once-listener for
2238
2821
  // beforeExit that auto-destroys the context, and suppressDestroy() only
2239
2822
  // neutralizes its callback without removing it. This loader owns cleanup
@@ -2273,6 +2856,23 @@ ${workerOption}\
2273
2856
  },
2274
2857
  }))
2275
2858
  __publishWasiDispose(__napiModule.exports)
2859
+ ${installAsyncRuntimeHosts}\
2860
+ // The CommonJS tail below aliases \`__napiModule.exports\`; a named module
2861
+ // export does not travel with it, so carry the marker on the binding itself
2862
+ // too. Three things pin the stamp to exactly this spot:
2863
+ // - inside this \`try\`, because the guard throws on a
2864
+ // \`#[napi(module_exports)]\` hook that claimed the name, and only the
2865
+ // catch below tears the environment — context, workers, exit listener —
2866
+ // back down;
2867
+ // - after the async runtime host install, which hands this same object to
2868
+ // addon-provided registration functions that may put anything on it;
2869
+ // - assigning onto the loader's own \`module.exports\`, which is still the
2870
+ // original object here, so an addon accessor with a refusing setter is
2871
+ // never written through. \`cjs-module-lexer\` — Node's CJS -> ESM named
2872
+ // export detection — reads the static \`module.exports.<name> =\` either
2873
+ // way, and the later \`module.exports = __napiModule.exports\` does not
2874
+ // undo that.
2875
+ module.exports.${NAPI_BINDING_TARGET_EXPORT} = ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
2276
2876
  __registerWasiExitListener()
2277
2877
  } catch (error) {
2278
2878
  const rollback = {