@napi-rs/cli 3.10.5 → 3.10.7

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.
@@ -5,8 +5,50 @@ import {
5
5
  } from './binding-target.js'
6
6
 
7
7
  const WASI_DISPOSE_SYMBOL = 'napi.rs.wasi.dispose'
8
+ const WASI_THREAD_POOL_RECONCILE_SYMBOL = 'napi.rs.wasi.reconcileThreadPool'
8
9
  const WASI_ROLLBACK_REGISTRY_SYMBOL = 'napi.rs.wasi.rollback.registry.v1'
9
10
 
11
+ /**
12
+ * Threaded loaders only: a view of the addon's crash flag for the pool workers.
13
+ *
14
+ * napi-async-runtime's shutdown waits run on the loader thread, inside the
15
+ * environment cleanup calls, and check one word of the shared wasm memory
16
+ * between 1 ms slices; once it is set they trap instead of waiting on a dead
17
+ * thread for good. napi's `napi_wasm_thread_crash_flag_address` export says
18
+ * where that word is. Each worker gets an `Int32Array` over it and raises it
19
+ * with `Atomics.store` when its wasm thread dies — right after the loader's own
20
+ * crash flag, where there is one. The store needs no instance, so a worker that
21
+ * fails while it loads, after the thread spawn that created it had already
22
+ * returned, raises it too.
23
+ *
24
+ * Read in `beforeInit`: before any registration code runs, and on Node before
25
+ * any pool worker exists (the pool is created on demand). An addon built with
26
+ * an older napi has no export, and a value that is not a 4-aligned address
27
+ * inside the memory is ignored: the view stays undefined and the waits stay
28
+ * unbounded, as before.
29
+ */
30
+ const ADDON_CRASH_FLAG_CAPTURE = `// A view of the addon's crash flag, one word of the shared wasm memory, for the
31
+ // pool workers: they raise it when their wasm thread dies, and the shutdown
32
+ // waits inside the cleanup calls on this thread then trap instead of waiting on
33
+ // the dead thread for good. Undefined for an addon built with an older napi.
34
+ let __wasiAddonCrashFlag
35
+
36
+ function __captureWasiAddonCrashFlag(instance) {
37
+ try {
38
+ const getAddress = instance.exports.napi_wasm_thread_crash_flag_address
39
+ if (typeof getAddress !== 'function') {
40
+ return
41
+ }
42
+ const address = getAddress() >>> 0
43
+ const buffer = __sharedMemory.buffer
44
+ if (address === 0 || address % 4 !== 0 || address + 4 > buffer.byteLength) {
45
+ return
46
+ }
47
+ __wasiAddonCrashFlag = new Int32Array(buffer, address, 1)
48
+ } catch {}
49
+ }
50
+ `
51
+
10
52
  /**
11
53
  * `Context.destroy()` disables JavaScript calls *before* it runs cleanup hooks
12
54
  * (`setStopping` -> `setCanCallIntoJs(false)` -> `runCleanup`), and the
@@ -88,7 +130,12 @@ function __wrapEmnapiContextDestroyForSettlement(
88
130
  * `__startWasiDisposal` / the rollback have already prepared and drained, so
89
131
  * one call there covers all three.
90
132
  */
91
- const createEmnapiContextLifecycle = (asyncRuntime: boolean) => {
133
+ const createEmnapiContextLifecycle = (
134
+ asyncRuntime: boolean,
135
+ // Only the threaded Node loader defines the crash latch
136
+ // (`__hasWasiThreadCrashed`, `__disposeWasiBindingAfterThreadCrash`).
137
+ threadCrashLatch = false,
138
+ ) => {
92
139
  const currentThreadHosts = asyncRuntime
93
140
  ? `
94
141
  let __currentThreadHostsDisposer
@@ -122,9 +169,203 @@ function __disposeCurrentThreadHosts() {
122
169
  }
123
170
  `
124
171
  : ''
172
+ const currentThreadHostTimersAfterThreadCrash =
173
+ asyncRuntime && threadCrashLatch
174
+ ? `
175
+ /**
176
+ * The CurrentThread timer host arms one referenced \`setTimeout\` per sleep in
177
+ * flight, so a long sleep holds the event loop open until it fires.
178
+ * \`__disposeCurrentThreadHosts\` releases them, but only
179
+ * \`__destroyEmnapiContext\` calls it, and the crash disposal never gets there:
180
+ * after a wasm thread died it must not enter wasm, and both the host
181
+ * unregister calls and a settled timer promise do. A dispose() that rejected
182
+ * after a crash then left the process alive until the longest sleep ended.
183
+ *
184
+ * So the loader keeps the package's own \`cancel\` for every timer in flight,
185
+ * and \`__releaseCurrentThreadHostTimers\` calls them after a crash. \`cancel\`
186
+ * is plain JavaScript: it clears the host timeout and resolves the package's
187
+ * promise. The promise the addon holds is the one below, and once released it
188
+ * never settles, so nothing is handed back to wasm. A sleep armed after that
189
+ * is never armed at all.
190
+ */
191
+ const __currentThreadHostTimerCancels = new Set()
192
+ let __currentThreadHostTimersReleased = false
193
+
194
+ function __scheduleCurrentThreadHostTimer(schedule, cancel, id, ms) {
195
+ if (__currentThreadHostTimersReleased) {
196
+ return new Promise(() => {})
197
+ }
198
+ const scheduled = schedule(id, ms)
199
+ if (!__isThenable(scheduled)) {
200
+ return scheduled
201
+ }
202
+ const release = () => cancel(id)
203
+ __currentThreadHostTimerCancels.add(release)
204
+ const settle = (settleUnreleased) => (outcome) => {
205
+ __currentThreadHostTimerCancels.delete(release)
206
+ return __currentThreadHostTimersReleased
207
+ ? new Promise(() => {})
208
+ : settleUnreleased(outcome)
209
+ }
210
+ return Promise.resolve(scheduled).then(
211
+ settle((value) => value),
212
+ settle((error) => {
213
+ throw error
214
+ }),
215
+ )
216
+ }
217
+
218
+ /**
219
+ * The binding as \`installCurrentThreadHosts\` should see it: every export
220
+ * reads through, and \`registerTimerHost\` hands the addon a \`schedule\` that
221
+ * records its \`cancel\`. A binding without a callable \`registerTimerHost\` is
222
+ * passed through, so the package reports the mismatch as before.
223
+ */
224
+ function __trackCurrentThreadHostTimers(binding) {
225
+ let registerTimerHost
226
+ try {
227
+ registerTimerHost = binding.registerTimerHost
228
+ } catch {
229
+ return binding
230
+ }
231
+ if (typeof registerTimerHost !== 'function') {
232
+ return binding
233
+ }
234
+ const view = Object.create(binding)
235
+ Object.defineProperty(view, 'registerTimerHost', {
236
+ configurable: true,
237
+ enumerable: true,
238
+ writable: true,
239
+ value: function (...args) {
240
+ const [, , schedule, cancel] = args
241
+ if (typeof schedule === 'function' && typeof cancel === 'function') {
242
+ args[2] = (id, ms) =>
243
+ __scheduleCurrentThreadHostTimer(schedule, cancel, id, ms)
244
+ }
245
+ return Reflect.apply(registerTimerHost, this, args)
246
+ },
247
+ })
248
+ return view
249
+ }
250
+
251
+ /**
252
+ * Clears every CurrentThread host timeout without entering wasm. Idempotent;
253
+ * a \`cancel\` that fails has already reported itself.
254
+ */
255
+ function __releaseCurrentThreadHostTimers() {
256
+ __currentThreadHostTimersReleased = true
257
+ const releases = [...__currentThreadHostTimerCancels]
258
+ __currentThreadHostTimerCancels.clear()
259
+ for (const release of releases) {
260
+ try {
261
+ release()
262
+ } catch {}
263
+ }
264
+ }
265
+ `
266
+ : ''
125
267
  const disposeCurrentThreadHosts = asyncRuntime
126
268
  ? ' __disposeCurrentThreadHosts()\n'
127
269
  : ''
270
+ // After the in-flight check: a disposal that was already running when a
271
+ // thread died settles through the crash disposal itself, so every caller of
272
+ // it keeps getting the one promise it handed out.
273
+ const disposeAfterThreadCrash = threadCrashLatch
274
+ ? ` if (!__wasiDisposed && __hasWasiThreadCrashed()) {
275
+ return __disposeWasiBindingAfterThreadCrash()
276
+ }
277
+ `
278
+ : ''
279
+ // A thread can die after the entry check above has passed. The disposal's
280
+ // polls and step boundaries check again, and a chain stopped that way — or
281
+ // failed any other way once a thread is dead — settles through the crash
282
+ // disposal. See `__abortWasiDisposalIfThreadCrashed`.
283
+ const abortDisposalAfterThreadCrash = (indent: string) =>
284
+ threadCrashLatch ? `${indent}__abortWasiDisposalIfThreadCrashed()\n` : ''
285
+ const settleDisposalAfterThreadCrash = (indent: string, exit: string) =>
286
+ threadCrashLatch
287
+ ? `${indent}if (__settleWasiDisposalAfterThreadCrash(resolveDispose, rejectDispose)) {
288
+ ${indent} ${exit}
289
+ ${indent}}
290
+ `
291
+ : ''
292
+ // The barrier's poll ends in `…_finish`, which joins the runtime's work. A
293
+ // dead thread's work never goes idle, so after a crash the join would block
294
+ // this thread for good: leave the barrier parked and stop the disposal.
295
+ const finishWasmEnvCleanupUnlessCrashed = threadCrashLatch
296
+ ? ` const finishCleanupUnlessCrashed = () => {
297
+ try {
298
+ __abortWasiDisposalIfThreadCrashed()
299
+ } catch (error) {
300
+ __finishParkedWasmEnvCleanup = undefined
301
+ throw error
302
+ }
303
+ finishCleanup()
304
+ }
305
+ `
306
+ : ''
307
+ const finishWasmEnvCleanupAfterPoll = threadCrashLatch
308
+ ? 'finishCleanupUnlessCrashed'
309
+ : 'finishCleanup'
310
+ // The initialization rollback runs the same polls and steps. With the latch,
311
+ // its body keeps its own name and `__rollbackWasiInitialization` becomes the
312
+ // wrapper that stops it after a crash. See
313
+ // `__rollbackWasiInitializationAfterThreadCrash`.
314
+ const rollbackStepsName = threadCrashLatch
315
+ ? '__runWasiInitializationRollbackSteps'
316
+ : '__rollbackWasiInitialization'
317
+ const rollbackAfterThreadCrash = threadCrashLatch
318
+ ? `
319
+ /**
320
+ * The rollback above, stopped when a wasm thread has died.
321
+ *
322
+ * Its polls wait for work the dead thread still counts, so they never end, and
323
+ * the barrier's \`…_finish\` and \`Context.destroy()\` would re-enter wasm
324
+ * and wait on that thread for good. While the rollback runs, the poll turns
325
+ * and step boundaries stop it (\`__abortWasiDisposalIfThreadCrashed\`), and a
326
+ * crash seen before, during or after it ends it through
327
+ * \`__rollbackWasiInitializationAfterThreadCrash\`. Without a crash the
328
+ * result — synchronous or not — is passed through unchanged.
329
+ */
330
+ function __rollbackWasiInitialization() {
331
+ if (__hasWasiThreadCrashed()) {
332
+ return __rollbackWasiInitializationAfterThreadCrash()
333
+ }
334
+ __wasiInitializationRollbackActive = true
335
+ let result
336
+ try {
337
+ result = __runWasiInitializationRollbackSteps()
338
+ } catch (error) {
339
+ __wasiInitializationRollbackActive = false
340
+ if (__hasWasiThreadCrashed()) {
341
+ return __rollbackWasiInitializationAfterThreadCrash()
342
+ }
343
+ throw error
344
+ }
345
+ if (!__isThenable(result)) {
346
+ __wasiInitializationRollbackActive = false
347
+ return __hasWasiThreadCrashed()
348
+ ? __rollbackWasiInitializationAfterThreadCrash()
349
+ : result
350
+ }
351
+ return Promise.resolve(result).then(
352
+ (cleanupErrors) => {
353
+ __wasiInitializationRollbackActive = false
354
+ return __hasWasiThreadCrashed()
355
+ ? __rollbackWasiInitializationAfterThreadCrash()
356
+ : cleanupErrors
357
+ },
358
+ (error) => {
359
+ __wasiInitializationRollbackActive = false
360
+ if (__hasWasiThreadCrashed()) {
361
+ return __rollbackWasiInitializationAfterThreadCrash()
362
+ }
363
+ throw error
364
+ },
365
+ )
366
+ }
367
+ `
368
+ : ''
128
369
 
129
370
  return `
130
371
  const __wasiDisposeSymbol = Symbol.for('${WASI_DISPOSE_SYMBOL}')
@@ -188,7 +429,7 @@ let __completeWasiDisposal = function () {}
188
429
  // that stopped short of destroying the context. See
189
430
  // \`__rollbackWasiInitialization\`.
190
431
  let __retainWasiRollbackForRetry = function () {}
191
- ${currentThreadHosts}
432
+ ${currentThreadHosts}${currentThreadHostTimersAfterThreadCrash}
192
433
  function __isThenable(value) {
193
434
  return (
194
435
  value !== null &&
@@ -653,6 +894,7 @@ function __prepareWasmEnvCleanupWithTurns() {
653
894
  // Publish the closer before yielding: from here until \`finishCleanup\` runs,
654
895
  // a caller that cannot yield is entitled to end this handshake itself.
655
896
  __finishParkedWasmEnvCleanup = finishCleanup
897
+ ${finishWasmEnvCleanupUnlessCrashed}\
656
898
  return (async () => {
657
899
  // Unbounded, exactly like the async-work drain below. The wait ends when
658
900
  // the addon reports its runtime work finished; the turns spent here are
@@ -660,6 +902,7 @@ function __prepareWasmEnvCleanupWithTurns() {
660
902
  const pace = __createWasmRuntimePollPace()
661
903
  for (;;) {
662
904
  await __yieldWasmRuntimePollTurn(pace)
905
+ ${abortDisposalAfterThreadCrash(' ')}\
663
906
  try {
664
907
  if (!workPending()) {
665
908
  return
@@ -670,7 +913,7 @@ function __prepareWasmEnvCleanupWithTurns() {
670
913
  return
671
914
  }
672
915
  }
673
- })().then(finishCleanup, finishCleanup)
916
+ })().then(${finishWasmEnvCleanupAfterPoll}, ${finishWasmEnvCleanupAfterPoll})
674
917
  }
675
918
 
676
919
  // Turns to wait for while the addon still reports queued settlements. Reaching
@@ -994,6 +1237,7 @@ function __drainWasiAsyncWork() {
994
1237
  await new Promise((resolve) => {
995
1238
  __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)
996
1239
  })
1240
+ ${abortDisposalAfterThreadCrash(' ')}\
997
1241
  }
998
1242
  })(),
999
1243
  ).then(
@@ -1089,6 +1333,7 @@ function __finishWasiDisposal() {
1089
1333
  }
1090
1334
 
1091
1335
  function __continueWasiDisposal() {
1336
+ ${abortDisposalAfterThreadCrash(' ')}\
1092
1337
  const destroyResult = __destroyEmnapiContext()
1093
1338
  if (__isThenable(destroyResult)) {
1094
1339
  return Promise.resolve(destroyResult).then(__finishWasiDisposal)
@@ -1097,6 +1342,7 @@ function __continueWasiDisposal() {
1097
1342
  }
1098
1343
 
1099
1344
  function __drainWasmEnvForWasiDisposal() {
1345
+ ${abortDisposalAfterThreadCrash(' ')}\
1100
1346
  const drainResult = __drainWasmEnvCleanup()
1101
1347
  if (__isThenable(drainResult)) {
1102
1348
  return Promise.resolve(drainResult).then(__continueWasiDisposal)
@@ -1105,6 +1351,7 @@ function __drainWasmEnvForWasiDisposal() {
1105
1351
  }
1106
1352
 
1107
1353
  function __cleanUpWasmEnvForWasiDisposal() {
1354
+ ${abortDisposalAfterThreadCrash(' ')}\
1108
1355
  // Run the pre-teardown barrier — yielding the turns its two-phase form asks
1109
1356
  // for, when the addon has one — then let the settlements it queued actually
1110
1357
  // reach JavaScript, and only then destroy the environment. Doing any two of
@@ -1141,6 +1388,7 @@ function __disposeWasiBinding() {
1141
1388
  if (__wasiDisposePromise) {
1142
1389
  return __wasiDisposePromise
1143
1390
  }
1391
+ ${disposeAfterThreadCrash}\
1144
1392
  if (__wasiDisposed) {
1145
1393
  return Promise.resolve()
1146
1394
  }
@@ -1157,6 +1405,7 @@ function __disposeWasiBinding() {
1157
1405
  try {
1158
1406
  result = __startWasiDisposal()
1159
1407
  } catch (error) {
1408
+ ${settleDisposalAfterThreadCrash(' ', 'return disposePromise')}\
1160
1409
  __wasiDisposePromise = undefined
1161
1410
  rejectDispose(error)
1162
1411
  return disposePromise
@@ -1168,6 +1417,7 @@ function __disposeWasiBinding() {
1168
1417
  resolveDispose(value)
1169
1418
  },
1170
1419
  (error) => {
1420
+ ${settleDisposalAfterThreadCrash(' ', 'return')}\
1171
1421
  __wasiDisposePromise = undefined
1172
1422
  rejectDispose(error)
1173
1423
  },
@@ -1203,6 +1453,7 @@ function __finishWasiInitializationRollback(cleanupErrors) {
1203
1453
  }
1204
1454
 
1205
1455
  function __destroyContextForWasiRollback(cleanupErrors) {
1456
+ ${abortDisposalAfterThreadCrash(' ')}\
1206
1457
  let destroyResult
1207
1458
  try {
1208
1459
  destroyResult = __destroyEmnapiContext()
@@ -1271,10 +1522,11 @@ function __retainFailedWasiRollback(cleanupErrors) {
1271
1522
  * goes away. That is the deliberate choice: a hung promise is a silent liveness
1272
1523
  * bug with no upper bound, while the retained bookkeeping is bounded by the page.
1273
1524
  */
1274
- function __rollbackWasiInitialization() {
1525
+ function ${rollbackStepsName}() {
1275
1526
  // The environment teardown this rollback performs, kept nested so it cannot
1276
1527
  // be reached without the async-work drain below running first.
1277
1528
  function __rollbackWasmEnvForWasiInitialization() {
1529
+ ${abortDisposalAfterThreadCrash(' ')}\
1278
1530
  const cleanupErrors = []
1279
1531
  let prepareResult
1280
1532
  try {
@@ -1300,6 +1552,7 @@ function __rollbackWasiInitialization() {
1300
1552
  // did not finish never gets here: it retains instead, exactly as a drain that
1301
1553
  // did not finish does.
1302
1554
  function __drainWasmEnvForWasiRollback(cleanupErrors) {
1555
+ ${abortDisposalAfterThreadCrash(' ')}\
1303
1556
  let drainResult
1304
1557
  try {
1305
1558
  drainResult = __drainWasmEnvCleanup()
@@ -1346,6 +1599,7 @@ function __rollbackWasiInitialization() {
1346
1599
  }
1347
1600
  return __rollbackWasmEnvForWasiInitialization()
1348
1601
  }
1602
+ ${rollbackAfterThreadCrash}\
1349
1603
  `
1350
1604
  }
1351
1605
 
@@ -1499,10 +1753,35 @@ const __workerPoolSize = Math.max(
1499
1753
  type: 'module',
1500
1754
  })
1501
1755
  __wasiWorkers.add(worker)
1756
+ __shareWasiAddonCrashFlag(worker)
1502
1757
  ${workerFsHandler}
1503
1758
  ${workerErrorHandler}
1504
1759
  return worker
1505
1760
  },
1761
+ `
1762
+ : ''
1763
+ // The pool is created before the wasm is instantiated, so the workers that
1764
+ // exist by `beforeInit` get the view there, after their 'load' message and
1765
+ // before any 'start'; a worker created later gets it first thing. A message
1766
+ // rather than worker options: a browser Worker has no `workerData`.
1767
+ const addonCrashFlagSharing = threads
1768
+ ? `${ADDON_CRASH_FLAG_CAPTURE}
1769
+ function __shareWasiAddonCrashFlag(worker) {
1770
+ if (__wasiAddonCrashFlag === undefined) {
1771
+ return
1772
+ }
1773
+ try {
1774
+ worker.postMessage({ __napiRsAddonCrashFlag: __wasiAddonCrashFlag })
1775
+ } catch {}
1776
+ }
1777
+
1778
+ `
1779
+ : ''
1780
+ const captureAddonCrashFlag = threads
1781
+ ? ` __captureWasiAddonCrashFlag(instance)
1782
+ for (const worker of __wasiWorkers) {
1783
+ __shareWasiAddonCrashFlag(worker)
1784
+ }
1506
1785
  `
1507
1786
  : ''
1508
1787
 
@@ -1542,6 +1821,7 @@ ${threads ? ' shared: true,\n' : ''}\
1542
1821
  ${workerPoolSizeBinding}\
1543
1822
  let __emnapiContext
1544
1823
  ${createEmnapiContextLifecycle(asyncRuntime)}
1824
+ ${addonCrashFlagSharing}\
1545
1825
  let __wasiModule
1546
1826
  let __napiModule
1547
1827
 
@@ -1575,6 +1855,7 @@ ${workerOption}\
1575
1855
  },
1576
1856
  beforeInit({ instance }) {
1577
1857
  __napiInstance = instance
1858
+ ${captureAddonCrashFlag}\
1578
1859
  for (const name of Object.keys(instance.exports)) {
1579
1860
  if (name.startsWith('__napi_register__')) {
1580
1861
  instance.exports[name]()
@@ -3608,9 +3889,11 @@ export const createWasiBinding = (
3608
3889
  } = require('@napi-rs/async-runtime')
3609
3890
  `
3610
3891
  : ''
3892
+ // The threaded loader has the thread crash latch, whose disposal releases
3893
+ // the host timers it tracks. See \`__trackCurrentThreadHostTimers\`.
3611
3894
  const installAsyncRuntimeHosts = asyncRuntime
3612
3895
  ? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
3613
- __napiModule.exports,
3896
+ ${threads ? '__trackCurrentThreadHostTimers(__napiModule.exports)' : '__napiModule.exports'},
3614
3897
  )
3615
3898
  `
3616
3899
  : ''
@@ -3694,7 +3977,13 @@ function __createWasiWorker(filename) {
3694
3977
  return new Worker(filename, {
3695
3978
  env: process.env,
3696
3979
  execArgv: __workerExecArgv,
3697
- workerData: { hostRoot: __hostRoot, rootDir: __rootDir },
3980
+ workerData: {
3981
+ hostRoot: __hostRoot,
3982
+ rootDir: __rootDir,
3983
+ crashFlag: __wasiThreadCrashFlag,
3984
+ crashReport: __wasiThreadCrashReport,
3985
+ addonCrashFlag: __wasiAddonCrashFlag,
3986
+ },
3698
3987
  })
3699
3988
  } catch (error) {
3700
3989
  if (!error || error.code !== 'ERR_WORKER_INVALID_EXEC_ARGV') {
@@ -3711,6 +4000,505 @@ function __createWasiWorker(filename) {
3711
4000
  }
3712
4001
  `
3713
4002
  : ''
4003
+ // The host timeouts a CurrentThread sleep armed, released without entering
4004
+ // wasm on every crash path. See `__trackCurrentThreadHostTimers`.
4005
+ const releaseCurrentThreadHostTimers = (indent: string) =>
4006
+ threads && asyncRuntime
4007
+ ? `${indent}__releaseCurrentThreadHostTimers()\n`
4008
+ : ''
4009
+ // The threaded context defers emnapi's calls into wasm through
4010
+ // `__wasiSetImmediate`, which a crash disposal closes.
4011
+ const emnapiContextOptions = threads
4012
+ ? '{ autoDestroy: false, features: { setImmediate: __wasiSetImmediate } }'
4013
+ : '{ autoDestroy: false }'
4014
+ // Only the threaded flavor has pool workers whose wasm thread can die under
4015
+ // this one. See `__disposeWasiBindingAtExit`.
4016
+ const threadCrashLatch = threads
4017
+ ? `
4018
+ // Set to 1 by a pool worker (see wasi-worker.mjs) right before it reports that
4019
+ // its wasm thread died. Shared memory, so this thread reads it synchronously even
4020
+ // while the worker's 'error' event is still queued behind the code that is
4021
+ // exiting right now.
4022
+ const __wasiThreadCrashFlag = new Int32Array(new SharedArrayBuffer(4))
4023
+ // Written by the first pool worker whose wasm thread dies, before it raises the
4024
+ // flag: its error and \`threadId\` (layout in wasi-worker.mjs). Read without
4025
+ // waiting for any event, see \`__readWasiThreadCrashReport\`.
4026
+ const __wasiThreadCrashReport = new SharedArrayBuffer(4096)
4027
+ let __wasiThreadCrashReportRead
4028
+ // The first error a pool worker reported through its 'error' event, and that
4029
+ // worker's \`threadId\`. See \`__getWasiThreadCrashError\`.
4030
+ let __wasiThreadCrashWorkerError
4031
+ let __wasiThreadCrashWorkerId
4032
+ let __wasiThreadCrashError
4033
+ // Raised while \`__runWasiInitializationRollbackSteps\` runs, so its polls and
4034
+ // steps stop after a crash the way a public disposal's do.
4035
+ let __wasiInitializationRollbackActive = false
4036
+ let __wasiThreadCrashed = false
4037
+
4038
+ ${ADDON_CRASH_FLAG_CAPTURE}
4039
+ /**
4040
+ * Whether any wasm thread of this binding has died: a trap or an uncaught error
4041
+ * in a pool worker, including one that failed to load after its thread spawn
4042
+ * had already been reported as started.
4043
+ *
4044
+ * After that the shared wasm state cannot be trusted: a lock the dead thread
4045
+ * held stays held, and a join or park that waits on it — the async runtime's
4046
+ * \`finish_shutdown\` waiting for the dead thread's work to go idle — blocks
4047
+ * this thread forever in a raw \`memory.atomic.wait32\`, which neither
4048
+ * emnapi's crash check nor a signal can interrupt.
4049
+ */
4050
+ function __hasWasiThreadCrashed() {
4051
+ if (__wasiThreadCrashed || Atomics.load(__wasiThreadCrashFlag, 0) !== 0) {
4052
+ return true
4053
+ }
4054
+ const manager = __getWasiThreadManager()
4055
+ return Boolean(manager && manager._fatalError)
4056
+ }
4057
+
4058
+ let __wasiThreadCrashDisposePromise
4059
+ // Raised by the crash disposal right before it terminates the workers. See
4060
+ // \`__wasiSetImmediate\`.
4061
+ let __wasiReentryClosed = false
4062
+
4063
+ /**
4064
+ * emnapi's \`features.setImmediate\` for this binding's context. emnapi defers
4065
+ * its calls back into wasm through it: \`_emnapi_set_immediate\` (libuv handle
4066
+ * closes, threadsafe-function finalizers), threadsafe-function dispatch
4067
+ * (\`async-send\`) and the finalizer queue. One queued before a crash disposal
4068
+ * terminates the workers still runs after it, and a worker terminated while it
4069
+ * held a lock in the wasm heap (napi's heap-sync allocator lock spins and never
4070
+ * gives up) leaves that call spinning on this thread for good. The binding is
4071
+ * unusable after a crash disposal, so those calls are dropped.
4072
+ *
4073
+ * Known residual, still able to enter wasm after a crash disposal because emnapi
4074
+ * offers no hook for them: the \`FinalizationRegistry\` callbacks that free
4075
+ * external memory when GC collects a value (\`_free\`, the shared-buffer meta
4076
+ * release), threadsafe-function dispatch of \`async-send\` type 1 and
4077
+ * \`_emnapi_next_tick\` (both \`Promise.resolve().then\`), and every deferred
4078
+ * call under emnapi 1.x, whose \`createContext\` ignores \`features\`.
4079
+ */
4080
+ function __wasiSetImmediate(callback) {
4081
+ return setImmediate(function () {
4082
+ if (!__wasiReentryClosed) {
4083
+ callback()
4084
+ }
4085
+ })
4086
+ }
4087
+
4088
+ /**
4089
+ * Stores the first error a pool worker reported, with the worker's id. Called
4090
+ * from the loader's own 'error' listener, which runs before emnapi's.
4091
+ */
4092
+ function __recordWasiThreadCrashError(error, workerId) {
4093
+ if (__wasiThreadCrashWorkerError !== undefined || error === undefined) {
4094
+ return
4095
+ }
4096
+ __wasiThreadCrashWorkerError = error
4097
+ __wasiThreadCrashWorkerId = workerId
4098
+ __fillWasiThreadCrashError()
4099
+ }
4100
+
4101
+ /**
4102
+ * The error in the shared crash report, rebuilt once it is complete: the
4103
+ * worker writes it before it raises the flag, so it is there as soon as the
4104
+ * crash is seen. Plain JavaScript over shared memory; never enters wasm.
4105
+ */
4106
+ function __readWasiThreadCrashReport() {
4107
+ if (__wasiThreadCrashReportRead !== undefined) {
4108
+ return __wasiThreadCrashReportRead
4109
+ }
4110
+ try {
4111
+ const header = new Int32Array(__wasiThreadCrashReport, 0, 3)
4112
+ if (Atomics.load(header, 0) !== 2) {
4113
+ return
4114
+ }
4115
+ const length = Atomics.load(header, 1)
4116
+ const threadId = Atomics.load(header, 2)
4117
+ let error
4118
+ if (length > 0) {
4119
+ // Copied out of shared memory: TextDecoder does not take a shared view.
4120
+ const bytes = new Uint8Array(__wasiThreadCrashReport, 12, length).slice()
4121
+ const report = JSON.parse(new TextDecoder().decode(bytes))
4122
+ error = new Error(String(report.message))
4123
+ if (typeof report.name === 'string') {
4124
+ error.name = report.name
4125
+ }
4126
+ if (typeof report.stack === 'string') {
4127
+ error.stack = report.stack
4128
+ }
4129
+ }
4130
+ __wasiThreadCrashReportRead = { error, threadId }
4131
+ } catch {
4132
+ __wasiThreadCrashReportRead = { error: undefined, threadId: 0 }
4133
+ }
4134
+ return __wasiThreadCrashReportRead
4135
+ }
4136
+
4137
+ /**
4138
+ * The one error every crash path of this binding reports, created on first
4139
+ * use.
4140
+ *
4141
+ * The shared flag is raised before this thread has processed the worker's
4142
+ * 'error' event, and once the workers are terminated that event is often
4143
+ * never delivered, so neither it nor emnapi's \`_fatalError\` can be counted
4144
+ * on. The cause is filled in from the first source that has it: the error the
4145
+ * 'error' listener kept, else the worker's shared crash report, else
4146
+ * \`_fatalError\` — here, and again from the 'error' listener when it arrives
4147
+ * later. Until then the error carries only its message.
4148
+ */
4149
+ function __getWasiThreadCrashError() {
4150
+ if (__wasiThreadCrashError === undefined) {
4151
+ __wasiThreadCrashError = new Error(
4152
+ 'napi-rs: WASI binding cannot be disposed after a worker thread crashed',
4153
+ )
4154
+ }
4155
+ __fillWasiThreadCrashError()
4156
+ return __wasiThreadCrashError
4157
+ }
4158
+
4159
+ function __fillWasiThreadCrashError() {
4160
+ const crashError = __wasiThreadCrashError
4161
+ if (crashError === undefined) {
4162
+ return
4163
+ }
4164
+ try {
4165
+ if (crashError.cause === undefined) {
4166
+ let cause = __wasiThreadCrashWorkerError
4167
+ if (cause === undefined) {
4168
+ const report = __readWasiThreadCrashReport()
4169
+ cause = report ? report.error : undefined
4170
+ }
4171
+ if (cause === undefined) {
4172
+ const manager = __getWasiThreadManager()
4173
+ cause = manager ? manager._fatalError : undefined
4174
+ }
4175
+ if (cause !== undefined && cause !== null) {
4176
+ crashError.cause = cause
4177
+ }
4178
+ }
4179
+ if (crashError.workerThreadId === undefined) {
4180
+ let workerId = __wasiThreadCrashWorkerId
4181
+ if (workerId === undefined) {
4182
+ const report = __readWasiThreadCrashReport()
4183
+ workerId = report && report.threadId > 0 ? report.threadId : undefined
4184
+ }
4185
+ if (workerId !== undefined) {
4186
+ crashError.workerThreadId = workerId
4187
+ }
4188
+ }
4189
+ } catch {}
4190
+ }
4191
+
4192
+ /**
4193
+ * Let the event loop drain after a wasm thread died, without entering wasm.
4194
+ *
4195
+ * emnapi's \`Context\` keeps a \`NodejsWaitingRequestCounter\` on Node: a
4196
+ * \`MessagePort\` (\`refCounter.refHandle\`) that it refs when the count of
4197
+ * in-flight async work and threadsafe-function requests leaves zero and unrefs
4198
+ * when it comes back. The requests the dead thread held never complete, so the
4199
+ * count never returns to zero and the port holds the process open for good.
4200
+ * \`Context.destroy()\` would not release it either — and it runs cleanup
4201
+ * hooks, which is exactly what must not happen now. So unref the port directly.
4202
+ * The count is left as is: it stays above zero, so a later request never refs
4203
+ * the port again.
4204
+ *
4205
+ * The field is private in emnapi's typings, so read it defensively; a context
4206
+ * without it (a non-Node host, a future emnapi) is left alone.
4207
+ */
4208
+ function __releaseEmnapiWaitingRequestHandle() {
4209
+ try {
4210
+ const refCounter = __emnapiContext && __emnapiContext.refCounter
4211
+ const refHandle = refCounter && refCounter.refHandle
4212
+ if (refHandle && typeof refHandle.unref === 'function') {
4213
+ refHandle.unref()
4214
+ }
4215
+ } catch {}
4216
+ }
4217
+
4218
+ /**
4219
+ * The public disposer after a wasm thread died. The normal chain drains async
4220
+ * work, runs the environment cleanup barrier and destroys the context — every
4221
+ * one of those re-enters wasm, and the barrier's shutdown waits for the dead
4222
+ * thread's work in the same raw atomic wait \`__disposeWasiBindingAtExit\`
4223
+ * avoids. An app that handled the worker's error and then disposes would block
4224
+ * there for good. Only stop the workers, then reject: the binding was not
4225
+ * cleaned up and cannot be, so reporting success would be a lie. The context
4226
+ * is not destroyed — only its waiting-request port is unrefed, so the process
4227
+ * can exit on its own — and the 'exit' listener takes its short path too.
4228
+ *
4229
+ * Latched: every later call returns the same promise.
4230
+ */
4231
+ function __disposeWasiBindingAfterThreadCrash() {
4232
+ if (__wasiThreadCrashDisposePromise) {
4233
+ return __wasiThreadCrashDisposePromise
4234
+ }
4235
+ __releaseEmnapiWaitingRequestHandle()
4236
+ ${releaseCurrentThreadHostTimers(' ')}\
4237
+ // No call into wasm after this: see \`__wasiSetImmediate\`.
4238
+ __wasiReentryClosed = true
4239
+ let workerResult
4240
+ try {
4241
+ workerResult = __terminateWasiWorkers()
4242
+ } catch (terminateError) {
4243
+ workerResult = Promise.reject(terminateError)
4244
+ }
4245
+ // Built at settlement, after the workers stopped: by then the 'error' event
4246
+ // has usually delivered the worker's error.
4247
+ __wasiThreadCrashDisposePromise = Promise.resolve(workerResult).then(
4248
+ () => {
4249
+ throw __getWasiThreadCrashError()
4250
+ },
4251
+ (terminateError) => {
4252
+ throw __attachCleanupErrors(__getWasiThreadCrashError(), [
4253
+ terminateError,
4254
+ ])
4255
+ },
4256
+ )
4257
+ return __wasiThreadCrashDisposePromise
4258
+ }
4259
+
4260
+ /**
4261
+ * Stops a public disposal that a thread crash overtook.
4262
+ *
4263
+ * The check at the top of \`__disposeWasiBinding\` only sees a crash that came
4264
+ * first. A thread that dies once the chain is running leaves it polling for
4265
+ * work the dead thread still counts — the async-work drain for
4266
+ * \`napi_wasm_async_work_pending\`, the barrier's poll for
4267
+ * \`napi_wasm_runtime_work_pending\` — and neither count ever reaches zero, so
4268
+ * the poll's referenced timers keep the process alive forever with the
4269
+ * disposal promise pending. Each poll turn and each step boundary calls this,
4270
+ * and the throw ends the chain before anything re-enters wasm again; the
4271
+ * disposer then settles it through \`__settleWasiDisposalAfterThreadCrash\`.
4272
+ *
4273
+ * The initialization rollback runs the same polls and steps, so they stop while
4274
+ * it runs too; its wrapper, \`__rollbackWasiInitialization\`, then ends it
4275
+ * through \`__rollbackWasiInitializationAfterThreadCrash\`. A bare poll with
4276
+ * neither in flight is left alone.
4277
+ */
4278
+ function __abortWasiDisposalIfThreadCrashed() {
4279
+ if (
4280
+ (__wasiDisposePromise !== undefined ||
4281
+ __wasiInitializationRollbackActive) &&
4282
+ __hasWasiThreadCrashed()
4283
+ ) {
4284
+ throw new Error(
4285
+ 'napi-rs: WASI disposal stopped because a worker thread crashed',
4286
+ )
4287
+ }
4288
+ }
4289
+
4290
+ /**
4291
+ * Settles an in-flight public disposal whose chain failed after a thread
4292
+ * died — stopped by \`__abortWasiDisposalIfThreadCrashed\` or failing any other
4293
+ * way — through the crash disposal: it releases the waiting-request port,
4294
+ * terminates the workers and rejects with the same error an entry-time crash
4295
+ * gets. \`__wasiDisposePromise\` stays set, so this caller, every caller that
4296
+ * joined it and every later one hold the same promise. Returns false when no
4297
+ * thread died, leaving the ordinary failure handling alone.
4298
+ */
4299
+ function __settleWasiDisposalAfterThreadCrash(resolve, reject) {
4300
+ if (!__hasWasiThreadCrashed()) {
4301
+ return false
4302
+ }
4303
+ __disposeWasiBindingAfterThreadCrash().then(resolve, reject)
4304
+ return true
4305
+ }
4306
+
4307
+ /**
4308
+ * Ends an initialization rollback after a wasm thread died, without entering
4309
+ * wasm: the barrier, its finish and \`Context.destroy()\` are skipped, and the
4310
+ * crash disposal releases the waiting-request port and terminates the workers,
4311
+ * once. The initialization error itself is left to propagate: the crash error
4312
+ * is returned as the rollback's cleanup error, so
4313
+ * \`__completeWasiInitializationRollback\` attaches it — as the cause when the
4314
+ * error has none — and keeps the record, since the context was not destroyed.
4315
+ */
4316
+ function __rollbackWasiInitializationAfterThreadCrash() {
4317
+ const crashError = __getWasiThreadCrashError()
4318
+ // Its rejection is \`crashError\` itself, which the caller reports.
4319
+ void __disposeWasiBindingAfterThreadCrash().catch(() => {})
4320
+ return [crashError]
4321
+ }
4322
+ `
4323
+ : ''
4324
+ const skipTeardownAfterThreadCrash = threads
4325
+ ? ` if (__hasWasiThreadCrashed()) {
4326
+ // Never re-enter wasm after a thread died: the environment cleanup joins
4327
+ // the dead thread's work and would hang the exit forever — SIGTERM
4328
+ // included, since its JavaScript listener never gets a turn. Stop the
4329
+ // workers and leave.
4330
+ ${releaseCurrentThreadHostTimers(' ')}\
4331
+ try {
4332
+ void Promise.resolve(__terminateWasiWorkers()).catch(() => {})
4333
+ } catch {}
4334
+ return
4335
+ }
4336
+ `
4337
+ : ''
4338
+ // Threaded flavor only: keeps emnapi's idle reuse pool at the addon's
4339
+ // configured MultiThread worker count. See docs/wasi.md, "Thread pool
4340
+ // preload".
4341
+ const threadPoolReconcile = threads
4342
+ ? `
4343
+ const __wasiThreadPoolReconcileSymbol = Symbol.for('${WASI_THREAD_POOL_RECONCILE_SYMBOL}')
4344
+
4345
+ /**
4346
+ * Takes a Worker out of emnapi's reuse pool, if it is still there. emnapi
4347
+ * terminates a pooled Worker that failed to load but (up to
4348
+ * @emnapi/wasi-threads 2.1.0) leaves it in the pool, where the next thread
4349
+ * spawn would pop it. A newer emnapi removes it itself, so this is a no-op
4350
+ * then.
4351
+ */
4352
+ function __removeWasiPoolWorker(manager, worker) {
4353
+ const index = manager.unusedWorkers.indexOf(worker)
4354
+ if (index !== -1) {
4355
+ manager.unusedWorkers.splice(index, 1)
4356
+ }
4357
+ }
4358
+
4359
+ /**
4360
+ * Takes a Worker the thread manager just terminated out of \`__wasiWorkers\`
4361
+ * once it has exited, not before. \`terminateWorker\` only starts Node's
4362
+ * asynchronous \`worker.terminate()\` and drops its promise, so a disposal that
4363
+ * begins before the exit has to find this Worker in the set and wait for it
4364
+ * like any other. A second \`terminate()\` settles when the Worker has exited,
4365
+ * the way \`__terminateWasiWorkers\` waits.
4366
+ */
4367
+ function __untrackWasiWorkerOnExit(worker) {
4368
+ const terminated = worker.terminate()
4369
+ if (__isThenable(terminated)) {
4370
+ Promise.resolve(terminated).then(
4371
+ () => {
4372
+ __wasiWorkers.delete(worker)
4373
+ },
4374
+ // Left tracked: disposal terminates it again and reports the error.
4375
+ () => {},
4376
+ )
4377
+ } else {
4378
+ __wasiWorkers.delete(worker)
4379
+ }
4380
+ }
4381
+
4382
+ /**
4383
+ * Matches emnapi's idle reuse pool to the addon's configured MultiThread worker
4384
+ * count, which the addon exports as \`napi_wasm_runtime_pool_workers\`
4385
+ * (napi-async-runtime; 0 under CurrentThread). \`reuseWorker: true\` starts the
4386
+ * pool empty, so without this every pool thread the first async call spawns
4387
+ * boots a Worker and loads the wasm into it first. Here each missing Worker is
4388
+ * created and starts loading now; a spawn later pops one that is already
4389
+ * booting. Idle Workers above the count are terminated, last in first out,
4390
+ * the way a spawn takes them. A Worker a spawn already took is not in the pool
4391
+ * and is left alone.
4392
+ *
4393
+ * Runs once after a successful load and, when the loader wraps it, after every
4394
+ * successful \`configureAsyncRuntime\`. Also reachable as
4395
+ * binding[Symbol.for('${WASI_THREAD_POOL_RECONCILE_SYMBOL}')]().
4396
+ * It never throws and never waits on a Worker: a Worker whose load fails is
4397
+ * dropped from the pool when its load rejects. Nothing at all happens after a
4398
+ * thread crash, once disposal started, or for an addon without the export.
4399
+ */
4400
+ function __reconcileWasiThreadPool() {
4401
+ try {
4402
+ if (__wasiDisposed || __wasiDisposePromise || __hasWasiThreadCrashed()) {
4403
+ return
4404
+ }
4405
+ const read = __napiInstance?.exports?.napi_wasm_runtime_pool_workers
4406
+ if (typeof read !== 'function') {
4407
+ return
4408
+ }
4409
+ const count = read() >>> 0
4410
+ const manager = __getWasiThreadManager()
4411
+ if (
4412
+ !manager ||
4413
+ !Array.isArray(manager.unusedWorkers) ||
4414
+ typeof manager.allocateUnusedWorker !== 'function' ||
4415
+ typeof manager.loadWasmModuleToWorker !== 'function'
4416
+ ) {
4417
+ return
4418
+ }
4419
+ // Both loops are bounded by the difference they start from, so a manager
4420
+ // that does not update \`unusedWorkers\` the way emnapi does cannot spin.
4421
+ for (let excess = manager.unusedWorkers.length - count; excess > 0; excess--) {
4422
+ const worker = manager.unusedWorkers[manager.unusedWorkers.length - 1]
4423
+ manager.terminateWorker(worker)
4424
+ __removeWasiPoolWorker(manager, worker)
4425
+ __untrackWasiWorkerOnExit(worker)
4426
+ // Compatibility with @emnapi/wasi-threads 2.1.0 and older:
4427
+ // \`terminateWorker\` installs a reporter that logs every emnapi message
4428
+ // still queued on the port, so a Worker that finished loading just
4429
+ // before it was terminated prints 'received "loaded" command from
4430
+ // terminated worker'. Nothing listens for that Worker any more, and a
4431
+ // newer emnapi ignores the late 'loaded' itself, so this is harmless
4432
+ // there. \`__terminateWasiWorkers\` does the same.
4433
+ worker.onmessage = undefined
4434
+ }
4435
+ for (let missing = count - manager.unusedWorkers.length; missing > 0; missing--) {
4436
+ let worker
4437
+ try {
4438
+ // Through \`onCreateWorker\`: tracked in \`__wasiWorkers\`, unref'd, and
4439
+ // handed the crash flags like any pool Worker.
4440
+ worker = manager.allocateUnusedWorker()
4441
+ manager
4442
+ .loadWasmModuleToWorker(worker)
4443
+ .then(undefined, () => __removeWasiPoolWorker(manager, worker))
4444
+ } catch {
4445
+ if (worker !== undefined) {
4446
+ __removeWasiPoolWorker(manager, worker)
4447
+ try {
4448
+ manager.terminateWorker(worker)
4449
+ __untrackWasiWorkerOnExit(worker)
4450
+ } catch {}
4451
+ }
4452
+ return
4453
+ }
4454
+ }
4455
+ } catch {}
4456
+ }
4457
+
4458
+ function __publishWasiThreadPoolReconcile(exports) {
4459
+ Object.defineProperty(exports, __wasiThreadPoolReconcileSymbol, {
4460
+ configurable: false,
4461
+ enumerable: false,
4462
+ value: __reconcileWasiThreadPool,
4463
+ writable: false,
4464
+ })
4465
+ }
4466
+ ${
4467
+ asyncRuntime
4468
+ ? `
4469
+ /**
4470
+ * Replaces the addon's \`configureAsyncRuntime\` export, if it has one, with a
4471
+ * wrapper that reconciles the pool after every successful call: a configure
4472
+ * changes the count the pool was preloaded for. A configure that throws changed
4473
+ * nothing, so its error propagates and the pool stays as it is. Matched by
4474
+ * name, like the CurrentThread host install: it is the export
4475
+ * napi-async-runtime's adapter defines, and an addon that defines its own under
4476
+ * that name gets the same reconcile. Never fails the load.
4477
+ */
4478
+ function __wrapWasiConfigureAsyncRuntime(binding) {
4479
+ let configure
4480
+ try {
4481
+ configure = binding.configureAsyncRuntime
4482
+ } catch {
4483
+ return
4484
+ }
4485
+ if (typeof configure !== 'function') {
4486
+ return
4487
+ }
4488
+ try {
4489
+ binding.configureAsyncRuntime = function configureAsyncRuntime(...args) {
4490
+ const result = Reflect.apply(configure, this, args)
4491
+ try {
4492
+ __reconcileWasiThreadPool()
4493
+ } catch {}
4494
+ return result
4495
+ }
4496
+ } catch {}
4497
+ }
4498
+ `
4499
+ : ''
4500
+ }`
4501
+ : ''
3714
4502
  const workerRuntimeImport = threads
3715
4503
  ? ` createOnMessage: __wasmCreateOnMessageForFsProxy,\n`
3716
4504
  : ''
@@ -3755,10 +4543,18 @@ function __createWasiWorker(filename) {
3755
4543
  // with threads there is no JavaScript seam at all, so `__drainWasiAsyncWork`
3756
4544
  // asks the addon instead.
3757
4545
  const emnapiPluginRequire = ` emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin: __emnapiTSFNPlugin,\n`
4546
+ const captureAddonCrashFlag = threads
4547
+ ? ' __captureWasiAddonCrashFlag(instance)\n'
4548
+ : ''
3758
4549
  const workerOption = threads
3759
4550
  ? ` onCreateWorker() {
3760
4551
  const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
3761
4552
  __wasiWorkers.add(worker)
4553
+ // Registered before emnapi's own listeners, which rethrow the error.
4554
+ worker.on('error', (error) => {
4555
+ __wasiThreadCrashed = true
4556
+ __recordWasiThreadCrashError(error, worker.threadId)
4557
+ })
3762
4558
  worker.onmessage = ({ data }) => {
3763
4559
  __wasmCreateOnMessageForFsProxy(__nodeFs)(data)
3764
4560
  }
@@ -3813,6 +4609,8 @@ ${workerRuntimeImport}\
3813
4609
  const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
3814
4610
  ${asyncRuntimeImport}\
3815
4611
  ${workerExecArgv}\
4612
+ ${threadCrashLatch}\
4613
+ ${threadPoolReconcile}\
3816
4614
 
3817
4615
  const __cwd = process.cwd()
3818
4616
  const __rootDir = __nodePath.parse(__cwd).root
@@ -3855,7 +4653,7 @@ if (__nodeFs.existsSync(__wasmDebugFilePath)) {
3855
4653
 
3856
4654
  const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
3857
4655
  let __emnapiContext
3858
- ${createEmnapiContextLifecycle(asyncRuntime)}
4656
+ ${createEmnapiContextLifecycle(asyncRuntime, threads)}
3859
4657
  const __wasiRollbackRegistrySymbol = Symbol.for('${WASI_ROLLBACK_REGISTRY_SYMBOL}')
3860
4658
  const __wasiRollbackRegistryKey =
3861
4659
  typeof __filename === 'string' ? __filename : __wasmFilePath
@@ -3959,6 +4757,7 @@ function __removeWasiExitListener() {
3959
4757
 
3960
4758
  function __disposeWasiBindingAtExit() {
3961
4759
  __wasiExitListenerRegistered = false
4760
+ ${skipTeardownAfterThreadCrash}\
3962
4761
  // An 'exit' handler cannot yield, so it cannot wait for queued promise
3963
4762
  // settlements the way __startWasiDisposal does — the process is leaving and
3964
4763
  // those promises have no observer left anyway. Run the synchronous teardown
@@ -4033,7 +4832,7 @@ try {
4033
4832
  const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
4034
4833
  try {
4035
4834
  __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
4036
- __emnapiCreateContext({ autoDestroy: false }),
4835
+ __emnapiCreateContext(${emnapiContextOptions}),
4037
4836
  __prepareWasmEnvCleanup,
4038
4837
  __isPreparingWasmEnvCleanup,
4039
4838
  )
@@ -4068,6 +4867,7 @@ ${workerOption}\
4068
4867
  },
4069
4868
  beforeInit({ instance }) {
4070
4869
  __napiInstance = instance
4870
+ ${captureAddonCrashFlag}\
4071
4871
  for (const name of Object.keys(instance.exports)) {
4072
4872
  if (name.startsWith('__napi_register__')) {
4073
4873
  instance.exports[name]()
@@ -4076,7 +4876,9 @@ ${workerOption}\
4076
4876
  },
4077
4877
  }))
4078
4878
  __publishWasiDispose(__napiModule.exports)
4879
+ ${threads ? ' __publishWasiThreadPoolReconcile(__napiModule.exports)\n' : ''}\
4079
4880
  ${installAsyncRuntimeHosts}\
4881
+ ${threads && asyncRuntime ? ' __wrapWasiConfigureAsyncRuntime(__napiModule.exports)\n' : ''}\
4080
4882
  // The CommonJS tail below aliases \`__napiModule.exports\`; a named module
4081
4883
  // export does not travel with it, so carry the marker on the binding itself
4082
4884
  // too. Three things pin the stamp to exactly this spot:
@@ -4105,5 +4907,14 @@ ${installAsyncRuntimeHosts}\
4105
4907
  __runWasiInitializationRollback(rollback)
4106
4908
  throw rollback.error
4107
4909
  }
4910
+ ${
4911
+ threads
4912
+ ? `// Preload the pool for the count the addon configured during registration.
4913
+ // See \`__reconcileWasiThreadPool\`.
4914
+ try {
4915
+ __reconcileWasiThreadPool()
4916
+ } catch {}
4108
4917
  `
4918
+ : ''
4919
+ }`
4109
4920
  }