@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.
- package/dist/cli.js +108 -6
- package/dist/index.cjs +108 -6
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +108 -6
- package/docs/wasi.md +17 -0
- package/package.json +1 -1
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +400 -8
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/templates.spec.ts +105 -0
- package/src/api/templates/load-wasi-template.ts +106 -4
- package/src/utils/__tests__/reconciliation.spec.ts +9 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|