@napi-rs/cli 3.10.1 → 3.10.2
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 +342 -25
- package/dist/index.cjs +342 -25
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +342 -25
- package/package.json +1 -1
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +1096 -82
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/templates.spec.ts +118 -0
- package/src/api/templates/load-wasi-template.ts +370 -35
|
Binary file
|
|
@@ -1005,6 +1005,124 @@ function initializationRollbackBody(code: string): string {
|
|
|
1005
1005
|
return code.slice(deferredStart, code.indexOf('throw error', deferredStart))
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* The body of `__startWasiDisposal`, sliced so that "the drain runs on the
|
|
1010
|
+
* disposal path" cannot be satisfied by a call somewhere else in the file.
|
|
1011
|
+
*/
|
|
1012
|
+
function disposalStartBody(code: string): string {
|
|
1013
|
+
const start = code.indexOf(DISPOSAL_START_SIGNATURE)
|
|
1014
|
+
return start === -1 ? '' : code.slice(start, code.indexOf('\n}', start))
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const DISPOSAL_START_SIGNATURE = 'function __startWasiDisposal() {'
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* `napi_async_work` is the one thing the settlement barrier above does not
|
|
1021
|
+
* cover. The eager loaders drain it from the shared prelude; the
|
|
1022
|
+
* deferred/workerd loader carries its own per-instance lifecycle and drains it
|
|
1023
|
+
* there, so both are asserted — just against different function names.
|
|
1024
|
+
*/
|
|
1025
|
+
const eagerWasiLoaderCases = wasiLoaderCases.filter(({ code }) =>
|
|
1026
|
+
code.includes(EAGER_ROLLBACK_SIGNATURE),
|
|
1027
|
+
)
|
|
1028
|
+
const deferredWasiLoaderCases = wasiLoaderCases.filter(
|
|
1029
|
+
({ code }) => !code.includes(EAGER_ROLLBACK_SIGNATURE),
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
test('every WASI loader case is either eager or deferred', (t) => {
|
|
1033
|
+
t.is(
|
|
1034
|
+
eagerWasiLoaderCases.length + deferredWasiLoaderCases.length,
|
|
1035
|
+
wasiLoaderCases.length,
|
|
1036
|
+
)
|
|
1037
|
+
t.true(deferredWasiLoaderCases.length >= 2)
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
for (const { name, code } of deferredWasiLoaderCases) {
|
|
1041
|
+
test(`deferred WASI loader drains outstanding async work before teardown: ${name}`, (t) => {
|
|
1042
|
+
t.true(
|
|
1043
|
+
code.includes('napi_wasm_async_work_pending'),
|
|
1044
|
+
'the deferred loader instantiates the same async-work plugin, so it strands the same work',
|
|
1045
|
+
)
|
|
1046
|
+
t.true(
|
|
1047
|
+
code.includes('napi_wasm_cancel_pending_async_work'),
|
|
1048
|
+
'threadless work is always queued rather than executing, so cancellation is what bounds the wait',
|
|
1049
|
+
)
|
|
1050
|
+
t.regex(
|
|
1051
|
+
code,
|
|
1052
|
+
/typeof __pending !== 'function' \|\|\s*typeof __cancelPending !== 'function'/,
|
|
1053
|
+
'loader must feature-detect both exports',
|
|
1054
|
+
)
|
|
1055
|
+
// Per instance, from that instance's own exports: two instances have
|
|
1056
|
+
// separate registries and must not wait on each other.
|
|
1057
|
+
t.true(
|
|
1058
|
+
code.includes('__drainInstanceAsyncWork(__napiInstance)'),
|
|
1059
|
+
'the drain must read the disposing instance, not a module-global one',
|
|
1060
|
+
)
|
|
1061
|
+
// Both teardown paths destroy the environment those completions need.
|
|
1062
|
+
const disposal = code.slice(
|
|
1063
|
+
code.indexOf('const __runInstanceDisposal'),
|
|
1064
|
+
code.indexOf('let __instanceDisposePromise'),
|
|
1065
|
+
)
|
|
1066
|
+
t.true(
|
|
1067
|
+
disposal.includes('__drainInstanceAsyncWork'),
|
|
1068
|
+
'per-instance disposal must drain outstanding async work',
|
|
1069
|
+
)
|
|
1070
|
+
t.true(
|
|
1071
|
+
initializationRollbackBody(code).includes('__drainInstanceAsyncWork'),
|
|
1072
|
+
'the initialization-failure path must drain it too',
|
|
1073
|
+
)
|
|
1074
|
+
})
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
test('the eager loader cases are the ones that share the disposal prelude', (t) => {
|
|
1078
|
+
// Guards the filter above: a prelude change that stopped emitting the eager
|
|
1079
|
+
// rollback would silently empty the loop below instead of failing.
|
|
1080
|
+
t.true(eagerWasiLoaderCases.length >= 4)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
for (const { name, code } of eagerWasiLoaderCases) {
|
|
1084
|
+
test(`WASI loader drains outstanding async work before teardown: ${name}`, (t) => {
|
|
1085
|
+
t.true(
|
|
1086
|
+
code.includes('napi_wasm_async_work_pending'),
|
|
1087
|
+
'loader must poll the addon for outstanding async work; nothing about it is observable from JavaScript in a threaded build',
|
|
1088
|
+
)
|
|
1089
|
+
t.true(
|
|
1090
|
+
code.includes('napi_wasm_cancel_pending_async_work'),
|
|
1091
|
+
'loader must cancel work that has not started, or disposal waits for the whole queue instead of only what is running',
|
|
1092
|
+
)
|
|
1093
|
+
// Both exports are optional, exactly like the settlement handshake: an
|
|
1094
|
+
// addon built against a napi crate that predates them must keep loading and
|
|
1095
|
+
// disposing as it does today.
|
|
1096
|
+
t.regex(
|
|
1097
|
+
code,
|
|
1098
|
+
/typeof pending !== 'function' \|\| typeof cancelPending !== 'function'/,
|
|
1099
|
+
'loader must feature-detect both exports',
|
|
1100
|
+
)
|
|
1101
|
+
// The wait is a real referenced timer, not a macrotask spin: the addon is
|
|
1102
|
+
// polled, so a zero-delay turn would burn the loop instead of yielding it.
|
|
1103
|
+
t.true(
|
|
1104
|
+
code.includes(
|
|
1105
|
+
'__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)',
|
|
1106
|
+
),
|
|
1107
|
+
'the async-work wait must yield with a real timer',
|
|
1108
|
+
)
|
|
1109
|
+
t.true(
|
|
1110
|
+
disposalStartBody(code).includes('__drainWasiAsyncWork'),
|
|
1111
|
+
'disposal must drain outstanding async work',
|
|
1112
|
+
)
|
|
1113
|
+
// Ordering is the whole point: the completion callbacks run addon code, and
|
|
1114
|
+
// the barrier, `Context.destroy()` and the termination each take that away.
|
|
1115
|
+
t.false(
|
|
1116
|
+
disposalStartBody(code).includes('__prepareWasmEnvCleanup'),
|
|
1117
|
+
'the async-work drain must run before the barrier, not beside it',
|
|
1118
|
+
)
|
|
1119
|
+
t.true(
|
|
1120
|
+
initializationRollbackBody(code).includes('__drainWasiAsyncWork'),
|
|
1121
|
+
'initialization rollback tears down the same things and needs the same drain',
|
|
1122
|
+
)
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1008
1126
|
// The loaders order their own teardown barrier-then-destroy, but the emnapi
|
|
1009
1127
|
// context is a live object: an embedder or test harness holding it, or emnapi's
|
|
1010
1128
|
// own `beforeExit` auto-destroy on a host where `suppressDestroy()` is absent,
|
|
@@ -167,6 +167,7 @@ let __emnapiWasmEnvCleanupRan = false
|
|
|
167
167
|
let __emnapiWasmEnvCleanupDrained = false
|
|
168
168
|
let __emnapiWasmEnvCleanupDrainPromise
|
|
169
169
|
let __wasiDisposed = false
|
|
170
|
+
let __wasiAsyncWorkDrainPromise
|
|
170
171
|
let __wasiDisposePromise
|
|
171
172
|
let __completeWasiDisposal = function () {}
|
|
172
173
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback
|
|
@@ -291,6 +292,24 @@ const __scheduleMacrotask = (function () {
|
|
|
291
292
|
}
|
|
292
293
|
})()
|
|
293
294
|
|
|
295
|
+
// A real, *referenced* timer, for waits that must let the whole host make
|
|
296
|
+
// progress between looks — the async-work drain polls the addon rather than
|
|
297
|
+
// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there
|
|
298
|
+
// would spin the loop instead of yielding it. Falls back to the macrotask
|
|
299
|
+
// scheduler on a host without timers.
|
|
300
|
+
function __scheduleTimer(callback, delay) {
|
|
301
|
+
const setTimer = globalThis.setTimeout
|
|
302
|
+
if (typeof setTimer !== 'function') {
|
|
303
|
+
__scheduleMacrotask(callback)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
setTimer(callback, delay)
|
|
308
|
+
} catch {
|
|
309
|
+
__scheduleMacrotask(callback)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
294
313
|
// Turns to wait for while the addon still reports queued settlements. Reaching
|
|
295
314
|
// zero is the only success. A counter still nonzero at this bound rejects the
|
|
296
315
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than
|
|
@@ -491,6 +510,135 @@ function __keepEventLoopAliveUntil(work) {
|
|
|
491
510
|
)
|
|
492
511
|
}
|
|
493
512
|
|
|
513
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait
|
|
514
|
+
// ends when the addon reports zero, so this only decides how promptly disposal
|
|
515
|
+
// notices — not how long it waits.
|
|
516
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Settles this addon's outstanding \`napi_async_work\` before the teardown that
|
|
520
|
+
* would strand it.
|
|
521
|
+
*
|
|
522
|
+
* \`napi_prepare_wasm_env_cleanup\` does not cover async work, and nothing about
|
|
523
|
+
* it is observable from JavaScript: the threadless archive resolves
|
|
524
|
+
* \`napi_*_async_work\` through the \`@emnapi/core\` plugins, but the threaded one
|
|
525
|
+
* links the C \`async_work.c\` on the uv threadpool, so there the wasm neither
|
|
526
|
+
* imports nor exports those symbols and the only brackets a loader could watch
|
|
527
|
+
* (\`_emnapi_ctx_*_waiting_request_counter\`) are shared with threadsafe
|
|
528
|
+
* functions. The addon is the one place both flavors go through, so it answers
|
|
529
|
+
* for both, through the same kind of handshake the settlement drain uses:
|
|
530
|
+
*
|
|
531
|
+
* - \`napi_wasm_cancel_pending_async_work()\` cancels what no thread has
|
|
532
|
+
* started. Those completion callbacks run with \`napi_cancelled\`, which
|
|
533
|
+
* napi-rs turns into a promise rejected with an \`AbortError\`.
|
|
534
|
+
* - \`napi_wasm_async_work_pending()\` counts what is still owed a completion
|
|
535
|
+
* callback. Work already executing refuses cancellation and stays counted
|
|
536
|
+
* until it finishes normally — which it can, because this runs before the
|
|
537
|
+
* barrier, before \`Context.destroy()\` and before anything is terminated.
|
|
538
|
+
*
|
|
539
|
+
* Both exports are optional: an addon built against a napi crate that predates
|
|
540
|
+
* them drains nothing and keeps the previous behavior, exactly as the
|
|
541
|
+
* \`napi_wasm_env_cleanup_pending\` handshake degrades.
|
|
542
|
+
*
|
|
543
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal
|
|
544
|
+
* synchronous in the common case. The promise it returns otherwise never
|
|
545
|
+
* rejects.
|
|
546
|
+
*
|
|
547
|
+
* The wait has no deadline, and that is the point: giving up would destroy the
|
|
548
|
+
* environment with a completion callback still owed, which is the stranding
|
|
549
|
+
* this exists to prevent. A task whose \`execute\` never returns already keeps an
|
|
550
|
+
* *undisposed* process alive in exactly the same way, so disposal inherits that
|
|
551
|
+
* rather than inventing a bound it cannot honor.
|
|
552
|
+
*
|
|
553
|
+
* Safe to call from inside a completion callback, which is reachable: settling
|
|
554
|
+
* a task runs addon code that can re-enter JavaScript — a setter on the value
|
|
555
|
+
* being handed back, a threadsafe-function callback — and that JavaScript can
|
|
556
|
+
* call \`dispose()\`. Two things make it terminate rather than wait on itself:
|
|
557
|
+
*
|
|
558
|
+
* - The addon keeps a work registered until its completion callback
|
|
559
|
+
* *finishes*, so the count read here is at least one and this takes the
|
|
560
|
+
* polling path instead of declaring the environment drained and tearing it
|
|
561
|
+
* down from inside the frame that is still settling a promise.
|
|
562
|
+
* - The poll is a timer, so it cannot run until the callback has returned to
|
|
563
|
+
* the host — by which time that work has left the registry. The count the
|
|
564
|
+
* next poll reads is the one taken after the callback finished.
|
|
565
|
+
*
|
|
566
|
+
* \`__disposeWasiBinding\` hands every caller the same in-flight promise, so the
|
|
567
|
+
* nested call joins this disposal rather than starting a second one.
|
|
568
|
+
*/
|
|
569
|
+
function __drainWasiAsyncWork() {
|
|
570
|
+
if (__wasiAsyncWorkDrainPromise !== undefined) {
|
|
571
|
+
return __wasiAsyncWorkDrainPromise
|
|
572
|
+
}
|
|
573
|
+
const exports = __napiInstance?.exports
|
|
574
|
+
const pending = exports?.napi_wasm_async_work_pending
|
|
575
|
+
const cancelPending = exports?.napi_wasm_cancel_pending_async_work
|
|
576
|
+
if (typeof pending !== 'function' || typeof cancelPending !== 'function') {
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const readPending = () => {
|
|
581
|
+
try {
|
|
582
|
+
return pending()
|
|
583
|
+
} catch (error) {
|
|
584
|
+
// A trap is the only way this call fails: it reads a counter and cannot
|
|
585
|
+
// allocate or call back into JavaScript. A trapped instance can no longer
|
|
586
|
+
// run anything, so its outstanding work is unreachable by definition —
|
|
587
|
+
// there is nothing left to wait for, and refusing to dispose would only
|
|
588
|
+
// keep a dead instance and its stuck counter alive. Best-effort here is
|
|
589
|
+
// the honest answer, and it is what disposal did before this drain
|
|
590
|
+
// existed.
|
|
591
|
+
//
|
|
592
|
+
// Only a trap. Anything else means the export is not what this loader
|
|
593
|
+
// thinks it is, which is a defect worth surfacing rather than disposing
|
|
594
|
+
// over.
|
|
595
|
+
if (error instanceof globalThis.WebAssembly.RuntimeError) {
|
|
596
|
+
return 0
|
|
597
|
+
}
|
|
598
|
+
throw error
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!readPending()) {
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
cancelPending()
|
|
607
|
+
} catch {
|
|
608
|
+
// Cancellation is an optimization: it bounds the wait by the work already
|
|
609
|
+
// executing. Failing it only means waiting for the whole queue instead.
|
|
610
|
+
}
|
|
611
|
+
if (!readPending()) {
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const drainPromise = __keepEventLoopAliveUntil(
|
|
616
|
+
(async () => {
|
|
617
|
+
while (readPending()) {
|
|
618
|
+
await new Promise((resolve) => {
|
|
619
|
+
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
})(),
|
|
623
|
+
).then(
|
|
624
|
+
() => {
|
|
625
|
+
__wasiAsyncWorkDrainPromise = undefined
|
|
626
|
+
},
|
|
627
|
+
(error) => {
|
|
628
|
+
// A wait that could not run is not a wait that finished. The only way
|
|
629
|
+
// here is a host whose timers and macrotask primitives all refuse, and
|
|
630
|
+
// the work is still outstanding — reporting success would destroy the
|
|
631
|
+
// environment over it, which is the stranding this exists to prevent.
|
|
632
|
+
// Reject instead: disposal stays retryable, and the context is not
|
|
633
|
+
// destroyed. Clearing the memo first is what makes the retry re-run this.
|
|
634
|
+
__wasiAsyncWorkDrainPromise = undefined
|
|
635
|
+
throw error
|
|
636
|
+
},
|
|
637
|
+
)
|
|
638
|
+
__wasiAsyncWorkDrainPromise = drainPromise
|
|
639
|
+
return drainPromise
|
|
640
|
+
}
|
|
641
|
+
|
|
494
642
|
/**
|
|
495
643
|
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own
|
|
496
644
|
* thread manager performed the termination. A bare \`worker.terminate()\` reaches
|
|
@@ -572,7 +720,7 @@ function __continueWasiDisposal() {
|
|
|
572
720
|
return __finishWasiDisposal()
|
|
573
721
|
}
|
|
574
722
|
|
|
575
|
-
function
|
|
723
|
+
function __cleanUpWasmEnvForWasiDisposal() {
|
|
576
724
|
// Run the pre-teardown barrier, then let the settlements it queued actually
|
|
577
725
|
// reach JavaScript, and only then destroy the environment. Doing these two
|
|
578
726
|
// back to back is what strands them.
|
|
@@ -584,6 +732,21 @@ function __startWasiDisposal() {
|
|
|
584
732
|
return __continueWasiDisposal()
|
|
585
733
|
}
|
|
586
734
|
|
|
735
|
+
function __startWasiDisposal() {
|
|
736
|
+
// Outstanding \`napi_async_work\` goes first, while the environment is still
|
|
737
|
+
// completely live: the completion callbacks run addon code, and everything
|
|
738
|
+
// after this point takes that away from them — the barrier shuts the async
|
|
739
|
+
// runtime down, \`Context.destroy()\` stops JavaScript calls, and terminating
|
|
740
|
+
// the pool threads removes what would have reported the work finished.
|
|
741
|
+
const asyncWorkResult = __drainWasiAsyncWork()
|
|
742
|
+
if (__isThenable(asyncWorkResult)) {
|
|
743
|
+
return Promise.resolve(asyncWorkResult).then(
|
|
744
|
+
__cleanUpWasmEnvForWasiDisposal,
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
return __cleanUpWasmEnvForWasiDisposal()
|
|
748
|
+
}
|
|
749
|
+
|
|
587
750
|
/**
|
|
588
751
|
* Disposes this generated WASI binding.
|
|
589
752
|
*
|
|
@@ -725,29 +888,60 @@ function __retainFailedWasiRollback(cleanupErrors) {
|
|
|
725
888
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.
|
|
726
889
|
*/
|
|
727
890
|
function __rollbackWasiInitialization() {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
891
|
+
// The environment teardown this rollback performs, kept nested so it cannot
|
|
892
|
+
// be reached without the async-work drain below running first.
|
|
893
|
+
function __rollbackWasmEnvForWasiInitialization() {
|
|
894
|
+
const cleanupErrors = []
|
|
895
|
+
let drainResult
|
|
896
|
+
let settlementsUnreached = false
|
|
897
|
+
try {
|
|
898
|
+
__prepareWasmEnvCleanup()
|
|
899
|
+
drainResult = __drainWasmEnvCleanup()
|
|
900
|
+
} catch (cleanupError) {
|
|
901
|
+
cleanupErrors.push(cleanupError)
|
|
902
|
+
settlementsUnreached = true
|
|
903
|
+
}
|
|
904
|
+
if (__isThenable(drainResult)) {
|
|
905
|
+
return Promise.resolve(drainResult).then(
|
|
906
|
+
() => __destroyContextForWasiRollback(cleanupErrors),
|
|
907
|
+
(cleanupError) => {
|
|
908
|
+
cleanupErrors.push(cleanupError)
|
|
909
|
+
return __retainFailedWasiRollback(cleanupErrors)
|
|
910
|
+
},
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
if (settlementsUnreached) {
|
|
914
|
+
return __retainFailedWasiRollback(cleanupErrors)
|
|
915
|
+
}
|
|
916
|
+
return __destroyContextForWasiRollback(cleanupErrors)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Same reason as \`__startWasiDisposal\`: a module-init hook can start async
|
|
920
|
+
// work before the load goes on to fail, and this rollback tears down exactly
|
|
921
|
+
// what those completions need. Settle them while everything is still live,
|
|
922
|
+
// before the barrier and the teardown above take that away.
|
|
923
|
+
//
|
|
924
|
+
// A drain that could not finish leaves async work possibly outstanding, and
|
|
925
|
+
// destroying the context over it would strand exactly what this rollback is
|
|
926
|
+
// there to settle. Stop short and retain instead — the same trade
|
|
927
|
+
// \`__rollbackWasmEnvForWasiInitialization\` makes for the settlement drain, so
|
|
928
|
+
// the context stays reclaimable by a retry or by this flavor's own
|
|
929
|
+
// last-resort teardown.
|
|
930
|
+
const __retainAfterAsyncWorkDrainFailure = (cleanupError) =>
|
|
931
|
+
__retainFailedWasiRollback([cleanupError])
|
|
932
|
+
let asyncWorkResult
|
|
731
933
|
try {
|
|
732
|
-
|
|
733
|
-
drainResult = __drainWasmEnvCleanup()
|
|
934
|
+
asyncWorkResult = __drainWasiAsyncWork()
|
|
734
935
|
} catch (cleanupError) {
|
|
735
|
-
|
|
736
|
-
settlementsUnreached = true
|
|
936
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)
|
|
737
937
|
}
|
|
738
|
-
if (__isThenable(
|
|
739
|
-
return Promise.resolve(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
cleanupErrors.push(cleanupError)
|
|
743
|
-
return __retainFailedWasiRollback(cleanupErrors)
|
|
744
|
-
},
|
|
938
|
+
if (__isThenable(asyncWorkResult)) {
|
|
939
|
+
return Promise.resolve(asyncWorkResult).then(
|
|
940
|
+
__rollbackWasmEnvForWasiInitialization,
|
|
941
|
+
__retainAfterAsyncWorkDrainFailure,
|
|
745
942
|
)
|
|
746
943
|
}
|
|
747
|
-
|
|
748
|
-
return __retainFailedWasiRollback(cleanupErrors)
|
|
749
|
-
}
|
|
750
|
-
return __destroyContextForWasiRollback(cleanupErrors)
|
|
944
|
+
return __rollbackWasmEnvForWasiInitialization()
|
|
751
945
|
}
|
|
752
946
|
`
|
|
753
947
|
}
|
|
@@ -875,14 +1069,27 @@ const __workerPoolSize = Math.max(
|
|
|
875
1069
|
const memoryName = threads ? '__sharedMemory' : '__wasmMemory'
|
|
876
1070
|
const asyncWorkPoolOption = ` asyncWorkPoolSize: ${threads ? '__asyncWorkPoolSize' : 0},
|
|
877
1071
|
`
|
|
878
|
-
//
|
|
879
|
-
// threadsafe
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
1072
|
+
// Which archive a build links decides who implements async work and
|
|
1073
|
+
// threadsafe functions — see `emnapi_link_library` in
|
|
1074
|
+
// `crates/build/src/wasi.rs`. Without threads it is `emnapi-basic-napi-rs`,
|
|
1075
|
+
// the "basic" model: both stay wasm imports, and the `@emnapi/core`
|
|
1076
|
+
// JavaScript plugins below are what resolves them; without the plugins
|
|
1077
|
+
// instantiation fails with a LinkError naming the missing import. With
|
|
1078
|
+
// threads it is `emnapi-napi-rs-mt`, the full composition, whose C
|
|
1079
|
+
// `async_work.c` / `threadsafe_function.c` run on the uv threadpool inside
|
|
1080
|
+
// the wasm — it imports none of those symbols, so the plugins are inert
|
|
1081
|
+
// there. They are passed in both modes because only the archive knows which
|
|
1082
|
+
// applies, and a plugin nothing imports costs nothing.
|
|
1083
|
+
//
|
|
1084
|
+
// This is why the async-work drain on the disposal path cannot live here:
|
|
1085
|
+
// with threads there is no JavaScript seam at all, so `__drainWasiAsyncWork`
|
|
1086
|
+
// asks the addon instead.
|
|
884
1087
|
const emnapiPluginImport = ` emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin as __emnapiTSFNPlugin,\n`
|
|
885
|
-
const emnapiPluginOption = ` plugins: [
|
|
1088
|
+
const emnapiPluginOption = ` plugins: [
|
|
1089
|
+
__captureWasiThreadManager,
|
|
1090
|
+
__emnapiAsyncWorkPlugin,
|
|
1091
|
+
__emnapiTSFNPlugin,
|
|
1092
|
+
],\n`
|
|
886
1093
|
const workerOption = threads
|
|
887
1094
|
? ` onCreateWorker() {
|
|
888
1095
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
|
|
@@ -1492,6 +1699,101 @@ function __drainWasmEnvCleanup(__instance) {
|
|
|
1492
1699
|
})()
|
|
1493
1700
|
}
|
|
1494
1701
|
|
|
1702
|
+
// A real, *referenced* timer for the async-work wait below, which polls the
|
|
1703
|
+
// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay
|
|
1704
|
+
// macrotask there would spin the loop instead of yielding it. Falls back to the
|
|
1705
|
+
// macrotask scheduler on a host without timers.
|
|
1706
|
+
function __scheduleTimer(__callback, __delay) {
|
|
1707
|
+
const __setTimer = globalThis.setTimeout
|
|
1708
|
+
if (typeof __setTimer !== 'function') {
|
|
1709
|
+
__scheduleMacrotask(__callback)
|
|
1710
|
+
return
|
|
1711
|
+
}
|
|
1712
|
+
try {
|
|
1713
|
+
__setTimer(__callback, __delay)
|
|
1714
|
+
} catch {
|
|
1715
|
+
__scheduleMacrotask(__callback)
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait
|
|
1720
|
+
// ends when the addon reports zero, so this only decides how promptly disposal
|
|
1721
|
+
// notices — not how long it waits.
|
|
1722
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* Settles this instance's outstanding \`napi_async_work\` before the teardown
|
|
1726
|
+
* that would strand it.
|
|
1727
|
+
*
|
|
1728
|
+
* The same hole the eager loaders had, and the same fix: the environment
|
|
1729
|
+
* cleanup barrier covers promise settlements queued on the threadsafe-function
|
|
1730
|
+
* queue and says nothing about \`napi_async_work\`, so destroying the context
|
|
1731
|
+
* with a work still outstanding leaves its completion callback with nowhere to
|
|
1732
|
+
* run and its promise unsettled forever.
|
|
1733
|
+
*
|
|
1734
|
+
* This flavor is threadless, so \`compute\` runs on the JavaScript thread inside
|
|
1735
|
+
* the macrotask that dequeued it: while this is running, any outstanding work
|
|
1736
|
+
* is queued rather than executing, and \`napi_wasm_cancel_pending_async_work\`
|
|
1737
|
+
* can take all of it. The poll is still what decides when the drain is done —
|
|
1738
|
+
* cancellation delivers those completions from a later macrotask, not
|
|
1739
|
+
* synchronously.
|
|
1740
|
+
*
|
|
1741
|
+
* Per instance, from that instance's own exports: two instances of this loader
|
|
1742
|
+
* have separate registries and must not wait on each other.
|
|
1743
|
+
*
|
|
1744
|
+
* Both exports are optional, so an addon built against a napi crate that
|
|
1745
|
+
* predates them keeps the previous behavior.
|
|
1746
|
+
*
|
|
1747
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal
|
|
1748
|
+
* synchronous in the common case. No deadline: giving up would destroy the
|
|
1749
|
+
* environment with a completion callback still owed.
|
|
1750
|
+
*/
|
|
1751
|
+
function __drainInstanceAsyncWork(__instance) {
|
|
1752
|
+
const __exports = __instance?.exports
|
|
1753
|
+
const __pending = __exports?.napi_wasm_async_work_pending
|
|
1754
|
+
const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work
|
|
1755
|
+
if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') {
|
|
1756
|
+
return
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const __readPending = () => {
|
|
1760
|
+
try {
|
|
1761
|
+
return __pending()
|
|
1762
|
+
} catch (__error) {
|
|
1763
|
+
// A trap is the only way this call fails: it reads a counter and cannot
|
|
1764
|
+
// allocate or call back into JavaScript. A trapped instance can no longer
|
|
1765
|
+
// run anything, so its outstanding work is unreachable by definition and
|
|
1766
|
+
// best-effort is the honest answer. Anything else means the export is not
|
|
1767
|
+
// what this loader thinks it is — a defect worth surfacing.
|
|
1768
|
+
if (__error instanceof globalThis.WebAssembly.RuntimeError) {
|
|
1769
|
+
return 0
|
|
1770
|
+
}
|
|
1771
|
+
throw __error
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (!__readPending()) {
|
|
1776
|
+
return
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
1779
|
+
__cancelPending()
|
|
1780
|
+
} catch {
|
|
1781
|
+
// Cancellation only bounds the wait. Failing it means waiting for the queue
|
|
1782
|
+
// to run instead, which the poll below already does.
|
|
1783
|
+
}
|
|
1784
|
+
if (!__readPending()) {
|
|
1785
|
+
return
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
return (async () => {
|
|
1789
|
+
while (__readPending()) {
|
|
1790
|
+
await new Promise((__resolve) => {
|
|
1791
|
+
__scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)
|
|
1792
|
+
})
|
|
1793
|
+
}
|
|
1794
|
+
})()
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1495
1797
|
function __createLifecycleReentryError(__operation) {
|
|
1496
1798
|
const __error = new Error(
|
|
1497
1799
|
__operation +
|
|
@@ -1988,7 +2290,15 @@ ${instanceHostState}\
|
|
|
1988
2290
|
if (__lifecycleState !== 'failed') {
|
|
1989
2291
|
__lifecycleState = 'disposal'
|
|
1990
2292
|
}
|
|
1991
|
-
//
|
|
2293
|
+
// Outstanding async work first, while the environment is still completely
|
|
2294
|
+
// live: its completion callbacks run addon code, and the barrier and
|
|
2295
|
+
// \`Context.destroy()\` below each take that away. Undefined unless work is
|
|
2296
|
+
// outstanding.
|
|
2297
|
+
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)
|
|
2298
|
+
if (__asyncWorkDrained) {
|
|
2299
|
+
await __asyncWorkDrained
|
|
2300
|
+
}
|
|
2301
|
+
// Then settle what the barrier cancelled, before the environment stops
|
|
1992
2302
|
// accepting JavaScript calls. Undefined unless something is queued, so
|
|
1993
2303
|
// an idle disposal is not delayed by a single turn.
|
|
1994
2304
|
const __drained = __prepareForDisposal()
|
|
@@ -2145,6 +2455,14 @@ ${installInstanceHosts}\
|
|
|
2145
2455
|
// so a failure before beforeInit costs no extra turn.
|
|
2146
2456
|
let __settlementsUnreached = false
|
|
2147
2457
|
try {
|
|
2458
|
+
// Registration can start async work too, and this path destroys the same
|
|
2459
|
+
// environment its completions need. A drain that cannot finish leaves the
|
|
2460
|
+
// work outstanding, so it counts as settlements unreached and stops the
|
|
2461
|
+
// rollback short of destroying, exactly like a failed barrier drain.
|
|
2462
|
+
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)
|
|
2463
|
+
if (__asyncWorkDrained) {
|
|
2464
|
+
await __asyncWorkDrained
|
|
2465
|
+
}
|
|
2148
2466
|
const __drained = __prepareForDisposal()
|
|
2149
2467
|
if (__drained) {
|
|
2150
2468
|
await __drained
|
|
@@ -2628,17 +2946,34 @@ function __createWasiWorker(filename) {
|
|
|
2628
2946
|
}
|
|
2629
2947
|
})(),
|
|
2630
2948
|
reuseWorker: true,
|
|
2631
|
-
plugins: [
|
|
2949
|
+
plugins: [
|
|
2950
|
+
__captureWasiThreadManager,
|
|
2951
|
+
__emnapiAsyncWorkPlugin,
|
|
2952
|
+
__emnapiTSFNPlugin,
|
|
2953
|
+
],
|
|
2632
2954
|
`
|
|
2633
2955
|
: ` asyncWorkPoolSize: 0,
|
|
2634
|
-
plugins: [
|
|
2956
|
+
plugins: [
|
|
2957
|
+
__captureWasiThreadManager,
|
|
2958
|
+
__emnapiAsyncWorkPlugin,
|
|
2959
|
+
__emnapiTSFNPlugin,
|
|
2960
|
+
],
|
|
2635
2961
|
`
|
|
2636
|
-
//
|
|
2637
|
-
// threadsafe
|
|
2638
|
-
//
|
|
2639
|
-
//
|
|
2640
|
-
//
|
|
2641
|
-
//
|
|
2962
|
+
// Which archive a build links decides who implements async work and
|
|
2963
|
+
// threadsafe functions — see `emnapi_link_library` in
|
|
2964
|
+
// `crates/build/src/wasi.rs`. Without threads it is `emnapi-basic-napi-rs`,
|
|
2965
|
+
// the "basic" model: both stay wasm imports, and the `@emnapi/core`
|
|
2966
|
+
// JavaScript plugins below are what resolves them; without the plugins
|
|
2967
|
+
// instantiation fails with a LinkError naming the missing import. With
|
|
2968
|
+
// threads it is `emnapi-napi-rs-mt`, the full composition, whose C
|
|
2969
|
+
// `async_work.c` / `threadsafe_function.c` run on the uv threadpool inside
|
|
2970
|
+
// the wasm — it imports none of those symbols, so the plugins are inert
|
|
2971
|
+
// there. They are passed in both modes because only the archive knows which
|
|
2972
|
+
// applies, and a plugin nothing imports costs nothing.
|
|
2973
|
+
//
|
|
2974
|
+
// This is why the async-work drain on the disposal path cannot live here:
|
|
2975
|
+
// with threads there is no JavaScript seam at all, so `__drainWasiAsyncWork`
|
|
2976
|
+
// asks the addon instead.
|
|
2642
2977
|
const emnapiPluginRequire = ` emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin: __emnapiTSFNPlugin,\n`
|
|
2643
2978
|
const workerOption = threads
|
|
2644
2979
|
? ` onCreateWorker() {
|