@napi-rs/cli 3.10.0 → 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.
@@ -129,6 +129,35 @@ function __disposeCurrentThreadHosts() {
129
129
  return `
130
130
  const __wasiDisposeSymbol = Symbol.for('${WASI_DISPOSE_SYMBOL}')
131
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
+ }
132
161
  let __napiInstance
133
162
  let __emnapiContextDestroyed = false
134
163
  let __emnapiContextDestroyPromise
@@ -419,13 +448,80 @@ ${disposeCurrentThreadHosts}\
419
448
  return destroyPromise
420
449
  }
421
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
+ */
422
510
  function __terminateWasiWorkers() {
423
511
  const cleanupErrors = []
424
512
  const pending = []
513
+ const threadManager = __getWasiThreadManager()
425
514
 
426
515
  for (const worker of __wasiWorkers) {
427
516
  let result
428
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
+ }
429
525
  result = worker.terminate()
430
526
  } catch (error) {
431
527
  cleanupErrors.push(error)
@@ -455,7 +551,9 @@ function __terminateWasiWorkers() {
455
551
  )
456
552
  }
457
553
  }
458
- return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
554
+ return pending.length > 0
555
+ ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
556
+ : finish()
459
557
  }
460
558
 
461
559
  function __finishWasiDisposal() {
@@ -784,7 +882,7 @@ const __workerPoolSize = Math.max(
784
882
  // `napi_generic_failure` stubs; with threads it would shadow the
785
883
  // `@emnapi/core` threaded TSFN/async-work protocol the plugins implement.
786
884
  const emnapiPluginImport = ` emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin as __emnapiTSFNPlugin,\n`
787
- const emnapiPluginOption = ` plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],\n`
885
+ const emnapiPluginOption = ` plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],\n`
788
886
  const workerOption = threads
789
887
  ? ` onCreateWorker() {
790
888
  const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -2530,10 +2628,10 @@ function __createWasiWorker(filename) {
2530
2628
  }
2531
2629
  })(),
2532
2630
  reuseWorker: true,
2533
- plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
2631
+ plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
2534
2632
  `
2535
2633
  : ` asyncWorkPoolSize: 0,
2536
- plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
2634
+ plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
2537
2635
  `
2538
2636
  // Every build links a "basic" emnapi archive without the C async-work and
2539
2637
  // threadsafe-function implementations (the `emnapi-napi-rs(-mt)` archives shipped by the emnapi package), so the
@@ -2571,6 +2669,10 @@ function __createWasiWorker(filename) {
2571
2669
  }
2572
2670
 
2573
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.
2574
2676
  }
2575
2677
  return worker
2576
2678
  },
@@ -117,7 +117,11 @@ test('resolveProcessExecutionIdentityForLocking re-probes until the identity is
117
117
  reads += 1
118
118
  return reads < 3 ? incompleteIdentity : completeIdentity
119
119
  },
120
- { expiresAt: performance.now() + 150, timeout: 150 },
120
+ // A sub-second budget flakes on loaded CI runners: two 20ms retry delays
121
+ // plus event-loop lag can exceed it before the third read (seen on
122
+ // windows-msvc with ~300ms timer starvation). The nominal runtime stays
123
+ // ~40ms; the budget only bounds how long failures are ridden out.
124
+ { expiresAt: performance.now() + 2_000, timeout: 2_000 },
121
125
  )
122
126
 
123
127
  t.true(resolution.complete)
@@ -132,7 +136,10 @@ test('resolveProcessExecutionIdentityForLocking resolves incomplete past the wai
132
136
  reads += 1
133
137
  return incompleteIdentity
134
138
  },
135
- { expiresAt: performance.now() + 150, timeout: 150 },
139
+ // Reads past the first happen on 20ms retry delays; the budget must
140
+ // tolerate event-loop lag on loaded CI runners so at least one re-probe
141
+ // (reads > 1 below) is guaranteed.
142
+ { expiresAt: performance.now() + 1_000, timeout: 1_000 },
136
143
  )
137
144
 
138
145
  t.false(resolution.complete)