@napi-rs/cli 3.10.1 → 3.10.3
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 +419 -40
- package/dist/index.cjs +419 -40
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +419 -40
- 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
- package/src/utils/__tests__/misc.spec.ts +339 -0
- package/src/utils/misc.ts +178 -36
|
@@ -139,6 +139,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
139
139
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
140
140
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
141
141
|
let __wasiDisposed = false␊
|
|
142
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
142
143
|
let __wasiDisposePromise␊
|
|
143
144
|
let __completeWasiDisposal = function () {}␊
|
|
144
145
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -296,6 +297,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
296
297
|
}␊
|
|
297
298
|
})()␊
|
|
298
299
|
␊
|
|
300
|
+
// A real, *referenced* timer, for waits that must let the whole host make␊
|
|
301
|
+
// progress between looks — the async-work drain polls the addon rather than␊
|
|
302
|
+
// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there␊
|
|
303
|
+
// would spin the loop instead of yielding it. Falls back to the macrotask␊
|
|
304
|
+
// scheduler on a host without timers.␊
|
|
305
|
+
function __scheduleTimer(callback, delay) {␊
|
|
306
|
+
const setTimer = globalThis.setTimeout␊
|
|
307
|
+
if (typeof setTimer !== 'function') {␊
|
|
308
|
+
__scheduleMacrotask(callback)␊
|
|
309
|
+
return␊
|
|
310
|
+
}␊
|
|
311
|
+
try {␊
|
|
312
|
+
setTimer(callback, delay)␊
|
|
313
|
+
} catch {␊
|
|
314
|
+
__scheduleMacrotask(callback)␊
|
|
315
|
+
}␊
|
|
316
|
+
}␊
|
|
317
|
+
␊
|
|
299
318
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
300
319
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
301
320
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -495,6 +514,135 @@ Generated by [AVA](https://avajs.dev).
|
|
|
495
514
|
)␊
|
|
496
515
|
}␊
|
|
497
516
|
␊
|
|
517
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait␊
|
|
518
|
+
// ends when the addon reports zero, so this only decides how promptly disposal␊
|
|
519
|
+
// notices — not how long it waits.␊
|
|
520
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1␊
|
|
521
|
+
␊
|
|
522
|
+
/**␊
|
|
523
|
+
* Settles this addon's outstanding \`napi_async_work\` before the teardown that␊
|
|
524
|
+
* would strand it.␊
|
|
525
|
+
*␊
|
|
526
|
+
* \`napi_prepare_wasm_env_cleanup\` does not cover async work, and nothing about␊
|
|
527
|
+
* it is observable from JavaScript: the threadless archive resolves␊
|
|
528
|
+
* \`napi_*_async_work\` through the \`@emnapi/core\` plugins, but the threaded one␊
|
|
529
|
+
* links the C \`async_work.c\` on the uv threadpool, so there the wasm neither␊
|
|
530
|
+
* imports nor exports those symbols and the only brackets a loader could watch␊
|
|
531
|
+
* (\`_emnapi_ctx_*_waiting_request_counter\`) are shared with threadsafe␊
|
|
532
|
+
* functions. The addon is the one place both flavors go through, so it answers␊
|
|
533
|
+
* for both, through the same kind of handshake the settlement drain uses:␊
|
|
534
|
+
*␊
|
|
535
|
+
* - \`napi_wasm_cancel_pending_async_work()\` cancels what no thread has␊
|
|
536
|
+
* started. Those completion callbacks run with \`napi_cancelled\`, which␊
|
|
537
|
+
* napi-rs turns into a promise rejected with an \`AbortError\`.␊
|
|
538
|
+
* - \`napi_wasm_async_work_pending()\` counts what is still owed a completion␊
|
|
539
|
+
* callback. Work already executing refuses cancellation and stays counted␊
|
|
540
|
+
* until it finishes normally — which it can, because this runs before the␊
|
|
541
|
+
* barrier, before \`Context.destroy()\` and before anything is terminated.␊
|
|
542
|
+
*␊
|
|
543
|
+
* Both exports are optional: an addon built against a napi crate that predates␊
|
|
544
|
+
* them drains nothing and keeps the previous behavior, exactly as the␊
|
|
545
|
+
* \`napi_wasm_env_cleanup_pending\` handshake degrades.␊
|
|
546
|
+
*␊
|
|
547
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal␊
|
|
548
|
+
* synchronous in the common case. The promise it returns otherwise never␊
|
|
549
|
+
* rejects.␊
|
|
550
|
+
*␊
|
|
551
|
+
* The wait has no deadline, and that is the point: giving up would destroy the␊
|
|
552
|
+
* environment with a completion callback still owed, which is the stranding␊
|
|
553
|
+
* this exists to prevent. A task whose \`execute\` never returns already keeps an␊
|
|
554
|
+
* *undisposed* process alive in exactly the same way, so disposal inherits that␊
|
|
555
|
+
* rather than inventing a bound it cannot honor.␊
|
|
556
|
+
*␊
|
|
557
|
+
* Safe to call from inside a completion callback, which is reachable: settling␊
|
|
558
|
+
* a task runs addon code that can re-enter JavaScript — a setter on the value␊
|
|
559
|
+
* being handed back, a threadsafe-function callback — and that JavaScript can␊
|
|
560
|
+
* call \`dispose()\`. Two things make it terminate rather than wait on itself:␊
|
|
561
|
+
*␊
|
|
562
|
+
* - The addon keeps a work registered until its completion callback␊
|
|
563
|
+
* *finishes*, so the count read here is at least one and this takes the␊
|
|
564
|
+
* polling path instead of declaring the environment drained and tearing it␊
|
|
565
|
+
* down from inside the frame that is still settling a promise.␊
|
|
566
|
+
* - The poll is a timer, so it cannot run until the callback has returned to␊
|
|
567
|
+
* the host — by which time that work has left the registry. The count the␊
|
|
568
|
+
* next poll reads is the one taken after the callback finished.␊
|
|
569
|
+
*␊
|
|
570
|
+
* \`__disposeWasiBinding\` hands every caller the same in-flight promise, so the␊
|
|
571
|
+
* nested call joins this disposal rather than starting a second one.␊
|
|
572
|
+
*/␊
|
|
573
|
+
function __drainWasiAsyncWork() {␊
|
|
574
|
+
if (__wasiAsyncWorkDrainPromise !== undefined) {␊
|
|
575
|
+
return __wasiAsyncWorkDrainPromise␊
|
|
576
|
+
}␊
|
|
577
|
+
const exports = __napiInstance?.exports␊
|
|
578
|
+
const pending = exports?.napi_wasm_async_work_pending␊
|
|
579
|
+
const cancelPending = exports?.napi_wasm_cancel_pending_async_work␊
|
|
580
|
+
if (typeof pending !== 'function' || typeof cancelPending !== 'function') {␊
|
|
581
|
+
return␊
|
|
582
|
+
}␊
|
|
583
|
+
␊
|
|
584
|
+
const readPending = () => {␊
|
|
585
|
+
try {␊
|
|
586
|
+
return pending()␊
|
|
587
|
+
} catch (error) {␊
|
|
588
|
+
// A trap is the only way this call fails: it reads a counter and cannot␊
|
|
589
|
+
// allocate or call back into JavaScript. A trapped instance can no longer␊
|
|
590
|
+
// run anything, so its outstanding work is unreachable by definition —␊
|
|
591
|
+
// there is nothing left to wait for, and refusing to dispose would only␊
|
|
592
|
+
// keep a dead instance and its stuck counter alive. Best-effort here is␊
|
|
593
|
+
// the honest answer, and it is what disposal did before this drain␊
|
|
594
|
+
// existed.␊
|
|
595
|
+
//␊
|
|
596
|
+
// Only a trap. Anything else means the export is not what this loader␊
|
|
597
|
+
// thinks it is, which is a defect worth surfacing rather than disposing␊
|
|
598
|
+
// over.␊
|
|
599
|
+
if (error instanceof globalThis.WebAssembly.RuntimeError) {␊
|
|
600
|
+
return 0␊
|
|
601
|
+
}␊
|
|
602
|
+
throw error␊
|
|
603
|
+
}␊
|
|
604
|
+
}␊
|
|
605
|
+
␊
|
|
606
|
+
if (!readPending()) {␊
|
|
607
|
+
return␊
|
|
608
|
+
}␊
|
|
609
|
+
try {␊
|
|
610
|
+
cancelPending()␊
|
|
611
|
+
} catch {␊
|
|
612
|
+
// Cancellation is an optimization: it bounds the wait by the work already␊
|
|
613
|
+
// executing. Failing it only means waiting for the whole queue instead.␊
|
|
614
|
+
}␊
|
|
615
|
+
if (!readPending()) {␊
|
|
616
|
+
return␊
|
|
617
|
+
}␊
|
|
618
|
+
␊
|
|
619
|
+
const drainPromise = __keepEventLoopAliveUntil(␊
|
|
620
|
+
(async () => {␊
|
|
621
|
+
while (readPending()) {␊
|
|
622
|
+
await new Promise((resolve) => {␊
|
|
623
|
+
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)␊
|
|
624
|
+
})␊
|
|
625
|
+
}␊
|
|
626
|
+
})(),␊
|
|
627
|
+
).then(␊
|
|
628
|
+
() => {␊
|
|
629
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
630
|
+
},␊
|
|
631
|
+
(error) => {␊
|
|
632
|
+
// A wait that could not run is not a wait that finished. The only way␊
|
|
633
|
+
// here is a host whose timers and macrotask primitives all refuse, and␊
|
|
634
|
+
// the work is still outstanding — reporting success would destroy the␊
|
|
635
|
+
// environment over it, which is the stranding this exists to prevent.␊
|
|
636
|
+
// Reject instead: disposal stays retryable, and the context is not␊
|
|
637
|
+
// destroyed. Clearing the memo first is what makes the retry re-run this.␊
|
|
638
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
639
|
+
throw error␊
|
|
640
|
+
},␊
|
|
641
|
+
)␊
|
|
642
|
+
__wasiAsyncWorkDrainPromise = drainPromise␊
|
|
643
|
+
return drainPromise␊
|
|
644
|
+
}␊
|
|
645
|
+
␊
|
|
498
646
|
/**␊
|
|
499
647
|
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
500
648
|
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
@@ -576,7 +724,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
576
724
|
return __finishWasiDisposal()␊
|
|
577
725
|
}␊
|
|
578
726
|
␊
|
|
579
|
-
function
|
|
727
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
580
728
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
581
729
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
582
730
|
// back to back is what strands them.␊
|
|
@@ -588,6 +736,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
588
736
|
return __continueWasiDisposal()␊
|
|
589
737
|
}␊
|
|
590
738
|
␊
|
|
739
|
+
function __startWasiDisposal() {␊
|
|
740
|
+
// Outstanding \`napi_async_work\` goes first, while the environment is still␊
|
|
741
|
+
// completely live: the completion callbacks run addon code, and everything␊
|
|
742
|
+
// after this point takes that away from them — the barrier shuts the async␊
|
|
743
|
+
// runtime down, \`Context.destroy()\` stops JavaScript calls, and terminating␊
|
|
744
|
+
// the pool threads removes what would have reported the work finished.␊
|
|
745
|
+
const asyncWorkResult = __drainWasiAsyncWork()␊
|
|
746
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
747
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
748
|
+
__cleanUpWasmEnvForWasiDisposal,␊
|
|
749
|
+
)␊
|
|
750
|
+
}␊
|
|
751
|
+
return __cleanUpWasmEnvForWasiDisposal()␊
|
|
752
|
+
}␊
|
|
753
|
+
␊
|
|
591
754
|
/**␊
|
|
592
755
|
* Disposes this generated WASI binding.␊
|
|
593
756
|
*␊
|
|
@@ -729,29 +892,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
729
892
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
730
893
|
*/␊
|
|
731
894
|
function __rollbackWasiInitialization() {␊
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
895
|
+
// The environment teardown this rollback performs, kept nested so it cannot␊
|
|
896
|
+
// be reached without the async-work drain below running first.␊
|
|
897
|
+
function __rollbackWasmEnvForWasiInitialization() {␊
|
|
898
|
+
const cleanupErrors = []␊
|
|
899
|
+
let drainResult␊
|
|
900
|
+
let settlementsUnreached = false␊
|
|
901
|
+
try {␊
|
|
902
|
+
__prepareWasmEnvCleanup()␊
|
|
903
|
+
drainResult = __drainWasmEnvCleanup()␊
|
|
904
|
+
} catch (cleanupError) {␊
|
|
905
|
+
cleanupErrors.push(cleanupError)␊
|
|
906
|
+
settlementsUnreached = true␊
|
|
907
|
+
}␊
|
|
908
|
+
if (__isThenable(drainResult)) {␊
|
|
909
|
+
return Promise.resolve(drainResult).then(␊
|
|
910
|
+
() => __destroyContextForWasiRollback(cleanupErrors),␊
|
|
911
|
+
(cleanupError) => {␊
|
|
912
|
+
cleanupErrors.push(cleanupError)␊
|
|
913
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
914
|
+
},␊
|
|
915
|
+
)␊
|
|
916
|
+
}␊
|
|
917
|
+
if (settlementsUnreached) {␊
|
|
918
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
919
|
+
}␊
|
|
920
|
+
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
921
|
+
}␊
|
|
922
|
+
␊
|
|
923
|
+
// Same reason as \`__startWasiDisposal\`: a module-init hook can start async␊
|
|
924
|
+
// work before the load goes on to fail, and this rollback tears down exactly␊
|
|
925
|
+
// what those completions need. Settle them while everything is still live,␊
|
|
926
|
+
// before the barrier and the teardown above take that away.␊
|
|
927
|
+
//␊
|
|
928
|
+
// A drain that could not finish leaves async work possibly outstanding, and␊
|
|
929
|
+
// destroying the context over it would strand exactly what this rollback is␊
|
|
930
|
+
// there to settle. Stop short and retain instead — the same trade␊
|
|
931
|
+
// \`__rollbackWasmEnvForWasiInitialization\` makes for the settlement drain, so␊
|
|
932
|
+
// the context stays reclaimable by a retry or by this flavor's own␊
|
|
933
|
+
// last-resort teardown.␊
|
|
934
|
+
const __retainAfterAsyncWorkDrainFailure = (cleanupError) =>␊
|
|
935
|
+
__retainFailedWasiRollback([cleanupError])␊
|
|
936
|
+
let asyncWorkResult␊
|
|
735
937
|
try {␊
|
|
736
|
-
|
|
737
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
938
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
738
939
|
} catch (cleanupError) {␊
|
|
739
|
-
|
|
740
|
-
settlementsUnreached = true␊
|
|
940
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
741
941
|
}␊
|
|
742
|
-
if (__isThenable(
|
|
743
|
-
return Promise.resolve(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
cleanupErrors.push(cleanupError)␊
|
|
747
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
748
|
-
},␊
|
|
942
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
943
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
944
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
945
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
749
946
|
)␊
|
|
750
947
|
}␊
|
|
751
|
-
|
|
752
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
753
|
-
}␊
|
|
754
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
948
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
755
949
|
}␊
|
|
756
950
|
␊
|
|
757
951
|
let __wasiModule␊
|
|
@@ -773,7 +967,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
773
967
|
context: __emnapiContext,␊
|
|
774
968
|
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
775
969
|
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
776
|
-
plugins: [
|
|
970
|
+
plugins: [␊
|
|
971
|
+
__captureWasiThreadManager,␊
|
|
972
|
+
__emnapiAsyncWorkPlugin,␊
|
|
973
|
+
__emnapiTSFNPlugin,␊
|
|
974
|
+
],␊
|
|
777
975
|
wasi: __wasi,␊
|
|
778
976
|
onCreateWorker() {␊
|
|
779
977
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {␊
|
|
@@ -950,6 +1148,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
950
1148
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
951
1149
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
952
1150
|
let __wasiDisposed = false␊
|
|
1151
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
953
1152
|
let __wasiDisposePromise␊
|
|
954
1153
|
let __completeWasiDisposal = function () {}␊
|
|
955
1154
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -1107,6 +1306,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1107
1306
|
}␊
|
|
1108
1307
|
})()␊
|
|
1109
1308
|
␊
|
|
1309
|
+
// A real, *referenced* timer, for waits that must let the whole host make␊
|
|
1310
|
+
// progress between looks — the async-work drain polls the addon rather than␊
|
|
1311
|
+
// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there␊
|
|
1312
|
+
// would spin the loop instead of yielding it. Falls back to the macrotask␊
|
|
1313
|
+
// scheduler on a host without timers.␊
|
|
1314
|
+
function __scheduleTimer(callback, delay) {␊
|
|
1315
|
+
const setTimer = globalThis.setTimeout␊
|
|
1316
|
+
if (typeof setTimer !== 'function') {␊
|
|
1317
|
+
__scheduleMacrotask(callback)␊
|
|
1318
|
+
return␊
|
|
1319
|
+
}␊
|
|
1320
|
+
try {␊
|
|
1321
|
+
setTimer(callback, delay)␊
|
|
1322
|
+
} catch {␊
|
|
1323
|
+
__scheduleMacrotask(callback)␊
|
|
1324
|
+
}␊
|
|
1325
|
+
}␊
|
|
1326
|
+
␊
|
|
1110
1327
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
1111
1328
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
1112
1329
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -1306,6 +1523,135 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1306
1523
|
)␊
|
|
1307
1524
|
}␊
|
|
1308
1525
|
␊
|
|
1526
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait␊
|
|
1527
|
+
// ends when the addon reports zero, so this only decides how promptly disposal␊
|
|
1528
|
+
// notices — not how long it waits.␊
|
|
1529
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1␊
|
|
1530
|
+
␊
|
|
1531
|
+
/**␊
|
|
1532
|
+
* Settles this addon's outstanding \`napi_async_work\` before the teardown that␊
|
|
1533
|
+
* would strand it.␊
|
|
1534
|
+
*␊
|
|
1535
|
+
* \`napi_prepare_wasm_env_cleanup\` does not cover async work, and nothing about␊
|
|
1536
|
+
* it is observable from JavaScript: the threadless archive resolves␊
|
|
1537
|
+
* \`napi_*_async_work\` through the \`@emnapi/core\` plugins, but the threaded one␊
|
|
1538
|
+
* links the C \`async_work.c\` on the uv threadpool, so there the wasm neither␊
|
|
1539
|
+
* imports nor exports those symbols and the only brackets a loader could watch␊
|
|
1540
|
+
* (\`_emnapi_ctx_*_waiting_request_counter\`) are shared with threadsafe␊
|
|
1541
|
+
* functions. The addon is the one place both flavors go through, so it answers␊
|
|
1542
|
+
* for both, through the same kind of handshake the settlement drain uses:␊
|
|
1543
|
+
*␊
|
|
1544
|
+
* - \`napi_wasm_cancel_pending_async_work()\` cancels what no thread has␊
|
|
1545
|
+
* started. Those completion callbacks run with \`napi_cancelled\`, which␊
|
|
1546
|
+
* napi-rs turns into a promise rejected with an \`AbortError\`.␊
|
|
1547
|
+
* - \`napi_wasm_async_work_pending()\` counts what is still owed a completion␊
|
|
1548
|
+
* callback. Work already executing refuses cancellation and stays counted␊
|
|
1549
|
+
* until it finishes normally — which it can, because this runs before the␊
|
|
1550
|
+
* barrier, before \`Context.destroy()\` and before anything is terminated.␊
|
|
1551
|
+
*␊
|
|
1552
|
+
* Both exports are optional: an addon built against a napi crate that predates␊
|
|
1553
|
+
* them drains nothing and keeps the previous behavior, exactly as the␊
|
|
1554
|
+
* \`napi_wasm_env_cleanup_pending\` handshake degrades.␊
|
|
1555
|
+
*␊
|
|
1556
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal␊
|
|
1557
|
+
* synchronous in the common case. The promise it returns otherwise never␊
|
|
1558
|
+
* rejects.␊
|
|
1559
|
+
*␊
|
|
1560
|
+
* The wait has no deadline, and that is the point: giving up would destroy the␊
|
|
1561
|
+
* environment with a completion callback still owed, which is the stranding␊
|
|
1562
|
+
* this exists to prevent. A task whose \`execute\` never returns already keeps an␊
|
|
1563
|
+
* *undisposed* process alive in exactly the same way, so disposal inherits that␊
|
|
1564
|
+
* rather than inventing a bound it cannot honor.␊
|
|
1565
|
+
*␊
|
|
1566
|
+
* Safe to call from inside a completion callback, which is reachable: settling␊
|
|
1567
|
+
* a task runs addon code that can re-enter JavaScript — a setter on the value␊
|
|
1568
|
+
* being handed back, a threadsafe-function callback — and that JavaScript can␊
|
|
1569
|
+
* call \`dispose()\`. Two things make it terminate rather than wait on itself:␊
|
|
1570
|
+
*␊
|
|
1571
|
+
* - The addon keeps a work registered until its completion callback␊
|
|
1572
|
+
* *finishes*, so the count read here is at least one and this takes the␊
|
|
1573
|
+
* polling path instead of declaring the environment drained and tearing it␊
|
|
1574
|
+
* down from inside the frame that is still settling a promise.␊
|
|
1575
|
+
* - The poll is a timer, so it cannot run until the callback has returned to␊
|
|
1576
|
+
* the host — by which time that work has left the registry. The count the␊
|
|
1577
|
+
* next poll reads is the one taken after the callback finished.␊
|
|
1578
|
+
*␊
|
|
1579
|
+
* \`__disposeWasiBinding\` hands every caller the same in-flight promise, so the␊
|
|
1580
|
+
* nested call joins this disposal rather than starting a second one.␊
|
|
1581
|
+
*/␊
|
|
1582
|
+
function __drainWasiAsyncWork() {␊
|
|
1583
|
+
if (__wasiAsyncWorkDrainPromise !== undefined) {␊
|
|
1584
|
+
return __wasiAsyncWorkDrainPromise␊
|
|
1585
|
+
}␊
|
|
1586
|
+
const exports = __napiInstance?.exports␊
|
|
1587
|
+
const pending = exports?.napi_wasm_async_work_pending␊
|
|
1588
|
+
const cancelPending = exports?.napi_wasm_cancel_pending_async_work␊
|
|
1589
|
+
if (typeof pending !== 'function' || typeof cancelPending !== 'function') {␊
|
|
1590
|
+
return␊
|
|
1591
|
+
}␊
|
|
1592
|
+
␊
|
|
1593
|
+
const readPending = () => {␊
|
|
1594
|
+
try {␊
|
|
1595
|
+
return pending()␊
|
|
1596
|
+
} catch (error) {␊
|
|
1597
|
+
// A trap is the only way this call fails: it reads a counter and cannot␊
|
|
1598
|
+
// allocate or call back into JavaScript. A trapped instance can no longer␊
|
|
1599
|
+
// run anything, so its outstanding work is unreachable by definition —␊
|
|
1600
|
+
// there is nothing left to wait for, and refusing to dispose would only␊
|
|
1601
|
+
// keep a dead instance and its stuck counter alive. Best-effort here is␊
|
|
1602
|
+
// the honest answer, and it is what disposal did before this drain␊
|
|
1603
|
+
// existed.␊
|
|
1604
|
+
//␊
|
|
1605
|
+
// Only a trap. Anything else means the export is not what this loader␊
|
|
1606
|
+
// thinks it is, which is a defect worth surfacing rather than disposing␊
|
|
1607
|
+
// over.␊
|
|
1608
|
+
if (error instanceof globalThis.WebAssembly.RuntimeError) {␊
|
|
1609
|
+
return 0␊
|
|
1610
|
+
}␊
|
|
1611
|
+
throw error␊
|
|
1612
|
+
}␊
|
|
1613
|
+
}␊
|
|
1614
|
+
␊
|
|
1615
|
+
if (!readPending()) {␊
|
|
1616
|
+
return␊
|
|
1617
|
+
}␊
|
|
1618
|
+
try {␊
|
|
1619
|
+
cancelPending()␊
|
|
1620
|
+
} catch {␊
|
|
1621
|
+
// Cancellation is an optimization: it bounds the wait by the work already␊
|
|
1622
|
+
// executing. Failing it only means waiting for the whole queue instead.␊
|
|
1623
|
+
}␊
|
|
1624
|
+
if (!readPending()) {␊
|
|
1625
|
+
return␊
|
|
1626
|
+
}␊
|
|
1627
|
+
␊
|
|
1628
|
+
const drainPromise = __keepEventLoopAliveUntil(␊
|
|
1629
|
+
(async () => {␊
|
|
1630
|
+
while (readPending()) {␊
|
|
1631
|
+
await new Promise((resolve) => {␊
|
|
1632
|
+
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)␊
|
|
1633
|
+
})␊
|
|
1634
|
+
}␊
|
|
1635
|
+
})(),␊
|
|
1636
|
+
).then(␊
|
|
1637
|
+
() => {␊
|
|
1638
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
1639
|
+
},␊
|
|
1640
|
+
(error) => {␊
|
|
1641
|
+
// A wait that could not run is not a wait that finished. The only way␊
|
|
1642
|
+
// here is a host whose timers and macrotask primitives all refuse, and␊
|
|
1643
|
+
// the work is still outstanding — reporting success would destroy the␊
|
|
1644
|
+
// environment over it, which is the stranding this exists to prevent.␊
|
|
1645
|
+
// Reject instead: disposal stays retryable, and the context is not␊
|
|
1646
|
+
// destroyed. Clearing the memo first is what makes the retry re-run this.␊
|
|
1647
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
1648
|
+
throw error␊
|
|
1649
|
+
},␊
|
|
1650
|
+
)␊
|
|
1651
|
+
__wasiAsyncWorkDrainPromise = drainPromise␊
|
|
1652
|
+
return drainPromise␊
|
|
1653
|
+
}␊
|
|
1654
|
+
␊
|
|
1309
1655
|
/**␊
|
|
1310
1656
|
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
1311
1657
|
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
@@ -1387,7 +1733,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1387
1733
|
return __finishWasiDisposal()␊
|
|
1388
1734
|
}␊
|
|
1389
1735
|
␊
|
|
1390
|
-
function
|
|
1736
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
1391
1737
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
1392
1738
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
1393
1739
|
// back to back is what strands them.␊
|
|
@@ -1399,6 +1745,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1399
1745
|
return __continueWasiDisposal()␊
|
|
1400
1746
|
}␊
|
|
1401
1747
|
␊
|
|
1748
|
+
function __startWasiDisposal() {␊
|
|
1749
|
+
// Outstanding \`napi_async_work\` goes first, while the environment is still␊
|
|
1750
|
+
// completely live: the completion callbacks run addon code, and everything␊
|
|
1751
|
+
// after this point takes that away from them — the barrier shuts the async␊
|
|
1752
|
+
// runtime down, \`Context.destroy()\` stops JavaScript calls, and terminating␊
|
|
1753
|
+
// the pool threads removes what would have reported the work finished.␊
|
|
1754
|
+
const asyncWorkResult = __drainWasiAsyncWork()␊
|
|
1755
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
1756
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
1757
|
+
__cleanUpWasmEnvForWasiDisposal,␊
|
|
1758
|
+
)␊
|
|
1759
|
+
}␊
|
|
1760
|
+
return __cleanUpWasmEnvForWasiDisposal()␊
|
|
1761
|
+
}␊
|
|
1762
|
+
␊
|
|
1402
1763
|
/**␊
|
|
1403
1764
|
* Disposes this generated WASI binding.␊
|
|
1404
1765
|
*␊
|
|
@@ -1540,29 +1901,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1540
1901
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
1541
1902
|
*/␊
|
|
1542
1903
|
function __rollbackWasiInitialization() {␊
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1904
|
+
// The environment teardown this rollback performs, kept nested so it cannot␊
|
|
1905
|
+
// be reached without the async-work drain below running first.␊
|
|
1906
|
+
function __rollbackWasmEnvForWasiInitialization() {␊
|
|
1907
|
+
const cleanupErrors = []␊
|
|
1908
|
+
let drainResult␊
|
|
1909
|
+
let settlementsUnreached = false␊
|
|
1910
|
+
try {␊
|
|
1911
|
+
__prepareWasmEnvCleanup()␊
|
|
1912
|
+
drainResult = __drainWasmEnvCleanup()␊
|
|
1913
|
+
} catch (cleanupError) {␊
|
|
1914
|
+
cleanupErrors.push(cleanupError)␊
|
|
1915
|
+
settlementsUnreached = true␊
|
|
1916
|
+
}␊
|
|
1917
|
+
if (__isThenable(drainResult)) {␊
|
|
1918
|
+
return Promise.resolve(drainResult).then(␊
|
|
1919
|
+
() => __destroyContextForWasiRollback(cleanupErrors),␊
|
|
1920
|
+
(cleanupError) => {␊
|
|
1921
|
+
cleanupErrors.push(cleanupError)␊
|
|
1922
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
1923
|
+
},␊
|
|
1924
|
+
)␊
|
|
1925
|
+
}␊
|
|
1926
|
+
if (settlementsUnreached) {␊
|
|
1927
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
1928
|
+
}␊
|
|
1929
|
+
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
1930
|
+
}␊
|
|
1931
|
+
␊
|
|
1932
|
+
// Same reason as \`__startWasiDisposal\`: a module-init hook can start async␊
|
|
1933
|
+
// work before the load goes on to fail, and this rollback tears down exactly␊
|
|
1934
|
+
// what those completions need. Settle them while everything is still live,␊
|
|
1935
|
+
// before the barrier and the teardown above take that away.␊
|
|
1936
|
+
//␊
|
|
1937
|
+
// A drain that could not finish leaves async work possibly outstanding, and␊
|
|
1938
|
+
// destroying the context over it would strand exactly what this rollback is␊
|
|
1939
|
+
// there to settle. Stop short and retain instead — the same trade␊
|
|
1940
|
+
// \`__rollbackWasmEnvForWasiInitialization\` makes for the settlement drain, so␊
|
|
1941
|
+
// the context stays reclaimable by a retry or by this flavor's own␊
|
|
1942
|
+
// last-resort teardown.␊
|
|
1943
|
+
const __retainAfterAsyncWorkDrainFailure = (cleanupError) =>␊
|
|
1944
|
+
__retainFailedWasiRollback([cleanupError])␊
|
|
1945
|
+
let asyncWorkResult␊
|
|
1546
1946
|
try {␊
|
|
1547
|
-
|
|
1548
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
1947
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
1549
1948
|
} catch (cleanupError) {␊
|
|
1550
|
-
|
|
1551
|
-
settlementsUnreached = true␊
|
|
1949
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
1552
1950
|
}␊
|
|
1553
|
-
if (__isThenable(
|
|
1554
|
-
return Promise.resolve(
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
cleanupErrors.push(cleanupError)␊
|
|
1558
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
1559
|
-
},␊
|
|
1951
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
1952
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
1953
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
1954
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
1560
1955
|
)␊
|
|
1561
1956
|
}␊
|
|
1562
|
-
|
|
1563
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
1564
|
-
}␊
|
|
1565
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
1957
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
1566
1958
|
}␊
|
|
1567
1959
|
␊
|
|
1568
1960
|
let __wasiModule␊
|
|
@@ -1584,7 +1976,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1584
1976
|
context: __emnapiContext,␊
|
|
1585
1977
|
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
1586
1978
|
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
1587
|
-
plugins: [
|
|
1979
|
+
plugins: [␊
|
|
1980
|
+
__captureWasiThreadManager,␊
|
|
1981
|
+
__emnapiAsyncWorkPlugin,␊
|
|
1982
|
+
__emnapiTSFNPlugin,␊
|
|
1983
|
+
],␊
|
|
1588
1984
|
wasi: __wasi,␊
|
|
1589
1985
|
onCreateWorker() {␊
|
|
1590
1986
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {␊
|
|
@@ -1780,6 +2176,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1780
2176
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
1781
2177
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
1782
2178
|
let __wasiDisposed = false␊
|
|
2179
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
1783
2180
|
let __wasiDisposePromise␊
|
|
1784
2181
|
let __completeWasiDisposal = function () {}␊
|
|
1785
2182
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -1937,6 +2334,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1937
2334
|
}␊
|
|
1938
2335
|
})()␊
|
|
1939
2336
|
␊
|
|
2337
|
+
// A real, *referenced* timer, for waits that must let the whole host make␊
|
|
2338
|
+
// progress between looks — the async-work drain polls the addon rather than␊
|
|
2339
|
+
// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there␊
|
|
2340
|
+
// would spin the loop instead of yielding it. Falls back to the macrotask␊
|
|
2341
|
+
// scheduler on a host without timers.␊
|
|
2342
|
+
function __scheduleTimer(callback, delay) {␊
|
|
2343
|
+
const setTimer = globalThis.setTimeout␊
|
|
2344
|
+
if (typeof setTimer !== 'function') {␊
|
|
2345
|
+
__scheduleMacrotask(callback)␊
|
|
2346
|
+
return␊
|
|
2347
|
+
}␊
|
|
2348
|
+
try {␊
|
|
2349
|
+
setTimer(callback, delay)␊
|
|
2350
|
+
} catch {␊
|
|
2351
|
+
__scheduleMacrotask(callback)␊
|
|
2352
|
+
}␊
|
|
2353
|
+
}␊
|
|
2354
|
+
␊
|
|
1940
2355
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
1941
2356
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
1942
2357
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -2136,6 +2551,135 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2136
2551
|
)␊
|
|
2137
2552
|
}␊
|
|
2138
2553
|
␊
|
|
2554
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait␊
|
|
2555
|
+
// ends when the addon reports zero, so this only decides how promptly disposal␊
|
|
2556
|
+
// notices — not how long it waits.␊
|
|
2557
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1␊
|
|
2558
|
+
␊
|
|
2559
|
+
/**␊
|
|
2560
|
+
* Settles this addon's outstanding \`napi_async_work\` before the teardown that␊
|
|
2561
|
+
* would strand it.␊
|
|
2562
|
+
*␊
|
|
2563
|
+
* \`napi_prepare_wasm_env_cleanup\` does not cover async work, and nothing about␊
|
|
2564
|
+
* it is observable from JavaScript: the threadless archive resolves␊
|
|
2565
|
+
* \`napi_*_async_work\` through the \`@emnapi/core\` plugins, but the threaded one␊
|
|
2566
|
+
* links the C \`async_work.c\` on the uv threadpool, so there the wasm neither␊
|
|
2567
|
+
* imports nor exports those symbols and the only brackets a loader could watch␊
|
|
2568
|
+
* (\`_emnapi_ctx_*_waiting_request_counter\`) are shared with threadsafe␊
|
|
2569
|
+
* functions. The addon is the one place both flavors go through, so it answers␊
|
|
2570
|
+
* for both, through the same kind of handshake the settlement drain uses:␊
|
|
2571
|
+
*␊
|
|
2572
|
+
* - \`napi_wasm_cancel_pending_async_work()\` cancels what no thread has␊
|
|
2573
|
+
* started. Those completion callbacks run with \`napi_cancelled\`, which␊
|
|
2574
|
+
* napi-rs turns into a promise rejected with an \`AbortError\`.␊
|
|
2575
|
+
* - \`napi_wasm_async_work_pending()\` counts what is still owed a completion␊
|
|
2576
|
+
* callback. Work already executing refuses cancellation and stays counted␊
|
|
2577
|
+
* until it finishes normally — which it can, because this runs before the␊
|
|
2578
|
+
* barrier, before \`Context.destroy()\` and before anything is terminated.␊
|
|
2579
|
+
*␊
|
|
2580
|
+
* Both exports are optional: an addon built against a napi crate that predates␊
|
|
2581
|
+
* them drains nothing and keeps the previous behavior, exactly as the␊
|
|
2582
|
+
* \`napi_wasm_env_cleanup_pending\` handshake degrades.␊
|
|
2583
|
+
*␊
|
|
2584
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal␊
|
|
2585
|
+
* synchronous in the common case. The promise it returns otherwise never␊
|
|
2586
|
+
* rejects.␊
|
|
2587
|
+
*␊
|
|
2588
|
+
* The wait has no deadline, and that is the point: giving up would destroy the␊
|
|
2589
|
+
* environment with a completion callback still owed, which is the stranding␊
|
|
2590
|
+
* this exists to prevent. A task whose \`execute\` never returns already keeps an␊
|
|
2591
|
+
* *undisposed* process alive in exactly the same way, so disposal inherits that␊
|
|
2592
|
+
* rather than inventing a bound it cannot honor.␊
|
|
2593
|
+
*␊
|
|
2594
|
+
* Safe to call from inside a completion callback, which is reachable: settling␊
|
|
2595
|
+
* a task runs addon code that can re-enter JavaScript — a setter on the value␊
|
|
2596
|
+
* being handed back, a threadsafe-function callback — and that JavaScript can␊
|
|
2597
|
+
* call \`dispose()\`. Two things make it terminate rather than wait on itself:␊
|
|
2598
|
+
*␊
|
|
2599
|
+
* - The addon keeps a work registered until its completion callback␊
|
|
2600
|
+
* *finishes*, so the count read here is at least one and this takes the␊
|
|
2601
|
+
* polling path instead of declaring the environment drained and tearing it␊
|
|
2602
|
+
* down from inside the frame that is still settling a promise.␊
|
|
2603
|
+
* - The poll is a timer, so it cannot run until the callback has returned to␊
|
|
2604
|
+
* the host — by which time that work has left the registry. The count the␊
|
|
2605
|
+
* next poll reads is the one taken after the callback finished.␊
|
|
2606
|
+
*␊
|
|
2607
|
+
* \`__disposeWasiBinding\` hands every caller the same in-flight promise, so the␊
|
|
2608
|
+
* nested call joins this disposal rather than starting a second one.␊
|
|
2609
|
+
*/␊
|
|
2610
|
+
function __drainWasiAsyncWork() {␊
|
|
2611
|
+
if (__wasiAsyncWorkDrainPromise !== undefined) {␊
|
|
2612
|
+
return __wasiAsyncWorkDrainPromise␊
|
|
2613
|
+
}␊
|
|
2614
|
+
const exports = __napiInstance?.exports␊
|
|
2615
|
+
const pending = exports?.napi_wasm_async_work_pending␊
|
|
2616
|
+
const cancelPending = exports?.napi_wasm_cancel_pending_async_work␊
|
|
2617
|
+
if (typeof pending !== 'function' || typeof cancelPending !== 'function') {␊
|
|
2618
|
+
return␊
|
|
2619
|
+
}␊
|
|
2620
|
+
␊
|
|
2621
|
+
const readPending = () => {␊
|
|
2622
|
+
try {␊
|
|
2623
|
+
return pending()␊
|
|
2624
|
+
} catch (error) {␊
|
|
2625
|
+
// A trap is the only way this call fails: it reads a counter and cannot␊
|
|
2626
|
+
// allocate or call back into JavaScript. A trapped instance can no longer␊
|
|
2627
|
+
// run anything, so its outstanding work is unreachable by definition —␊
|
|
2628
|
+
// there is nothing left to wait for, and refusing to dispose would only␊
|
|
2629
|
+
// keep a dead instance and its stuck counter alive. Best-effort here is␊
|
|
2630
|
+
// the honest answer, and it is what disposal did before this drain␊
|
|
2631
|
+
// existed.␊
|
|
2632
|
+
//␊
|
|
2633
|
+
// Only a trap. Anything else means the export is not what this loader␊
|
|
2634
|
+
// thinks it is, which is a defect worth surfacing rather than disposing␊
|
|
2635
|
+
// over.␊
|
|
2636
|
+
if (error instanceof globalThis.WebAssembly.RuntimeError) {␊
|
|
2637
|
+
return 0␊
|
|
2638
|
+
}␊
|
|
2639
|
+
throw error␊
|
|
2640
|
+
}␊
|
|
2641
|
+
}␊
|
|
2642
|
+
␊
|
|
2643
|
+
if (!readPending()) {␊
|
|
2644
|
+
return␊
|
|
2645
|
+
}␊
|
|
2646
|
+
try {␊
|
|
2647
|
+
cancelPending()␊
|
|
2648
|
+
} catch {␊
|
|
2649
|
+
// Cancellation is an optimization: it bounds the wait by the work already␊
|
|
2650
|
+
// executing. Failing it only means waiting for the whole queue instead.␊
|
|
2651
|
+
}␊
|
|
2652
|
+
if (!readPending()) {␊
|
|
2653
|
+
return␊
|
|
2654
|
+
}␊
|
|
2655
|
+
␊
|
|
2656
|
+
const drainPromise = __keepEventLoopAliveUntil(␊
|
|
2657
|
+
(async () => {␊
|
|
2658
|
+
while (readPending()) {␊
|
|
2659
|
+
await new Promise((resolve) => {␊
|
|
2660
|
+
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)␊
|
|
2661
|
+
})␊
|
|
2662
|
+
}␊
|
|
2663
|
+
})(),␊
|
|
2664
|
+
).then(␊
|
|
2665
|
+
() => {␊
|
|
2666
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
2667
|
+
},␊
|
|
2668
|
+
(error) => {␊
|
|
2669
|
+
// A wait that could not run is not a wait that finished. The only way␊
|
|
2670
|
+
// here is a host whose timers and macrotask primitives all refuse, and␊
|
|
2671
|
+
// the work is still outstanding — reporting success would destroy the␊
|
|
2672
|
+
// environment over it, which is the stranding this exists to prevent.␊
|
|
2673
|
+
// Reject instead: disposal stays retryable, and the context is not␊
|
|
2674
|
+
// destroyed. Clearing the memo first is what makes the retry re-run this.␊
|
|
2675
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
2676
|
+
throw error␊
|
|
2677
|
+
},␊
|
|
2678
|
+
)␊
|
|
2679
|
+
__wasiAsyncWorkDrainPromise = drainPromise␊
|
|
2680
|
+
return drainPromise␊
|
|
2681
|
+
}␊
|
|
2682
|
+
␊
|
|
2139
2683
|
/**␊
|
|
2140
2684
|
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
2141
2685
|
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
@@ -2217,7 +2761,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2217
2761
|
return __finishWasiDisposal()␊
|
|
2218
2762
|
}␊
|
|
2219
2763
|
␊
|
|
2220
|
-
function
|
|
2764
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
2221
2765
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
2222
2766
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
2223
2767
|
// back to back is what strands them.␊
|
|
@@ -2229,6 +2773,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2229
2773
|
return __continueWasiDisposal()␊
|
|
2230
2774
|
}␊
|
|
2231
2775
|
␊
|
|
2776
|
+
function __startWasiDisposal() {␊
|
|
2777
|
+
// Outstanding \`napi_async_work\` goes first, while the environment is still␊
|
|
2778
|
+
// completely live: the completion callbacks run addon code, and everything␊
|
|
2779
|
+
// after this point takes that away from them — the barrier shuts the async␊
|
|
2780
|
+
// runtime down, \`Context.destroy()\` stops JavaScript calls, and terminating␊
|
|
2781
|
+
// the pool threads removes what would have reported the work finished.␊
|
|
2782
|
+
const asyncWorkResult = __drainWasiAsyncWork()␊
|
|
2783
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
2784
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
2785
|
+
__cleanUpWasmEnvForWasiDisposal,␊
|
|
2786
|
+
)␊
|
|
2787
|
+
}␊
|
|
2788
|
+
return __cleanUpWasmEnvForWasiDisposal()␊
|
|
2789
|
+
}␊
|
|
2790
|
+
␊
|
|
2232
2791
|
/**␊
|
|
2233
2792
|
* Disposes this generated WASI binding.␊
|
|
2234
2793
|
*␊
|
|
@@ -2370,29 +2929,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2370
2929
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
2371
2930
|
*/␊
|
|
2372
2931
|
function __rollbackWasiInitialization() {␊
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2932
|
+
// The environment teardown this rollback performs, kept nested so it cannot␊
|
|
2933
|
+
// be reached without the async-work drain below running first.␊
|
|
2934
|
+
function __rollbackWasmEnvForWasiInitialization() {␊
|
|
2935
|
+
const cleanupErrors = []␊
|
|
2936
|
+
let drainResult␊
|
|
2937
|
+
let settlementsUnreached = false␊
|
|
2938
|
+
try {␊
|
|
2939
|
+
__prepareWasmEnvCleanup()␊
|
|
2940
|
+
drainResult = __drainWasmEnvCleanup()␊
|
|
2941
|
+
} catch (cleanupError) {␊
|
|
2942
|
+
cleanupErrors.push(cleanupError)␊
|
|
2943
|
+
settlementsUnreached = true␊
|
|
2944
|
+
}␊
|
|
2945
|
+
if (__isThenable(drainResult)) {␊
|
|
2946
|
+
return Promise.resolve(drainResult).then(␊
|
|
2947
|
+
() => __destroyContextForWasiRollback(cleanupErrors),␊
|
|
2948
|
+
(cleanupError) => {␊
|
|
2949
|
+
cleanupErrors.push(cleanupError)␊
|
|
2950
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2951
|
+
},␊
|
|
2952
|
+
)␊
|
|
2953
|
+
}␊
|
|
2954
|
+
if (settlementsUnreached) {␊
|
|
2955
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2956
|
+
}␊
|
|
2957
|
+
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
2958
|
+
}␊
|
|
2959
|
+
␊
|
|
2960
|
+
// Same reason as \`__startWasiDisposal\`: a module-init hook can start async␊
|
|
2961
|
+
// work before the load goes on to fail, and this rollback tears down exactly␊
|
|
2962
|
+
// what those completions need. Settle them while everything is still live,␊
|
|
2963
|
+
// before the barrier and the teardown above take that away.␊
|
|
2964
|
+
//␊
|
|
2965
|
+
// A drain that could not finish leaves async work possibly outstanding, and␊
|
|
2966
|
+
// destroying the context over it would strand exactly what this rollback is␊
|
|
2967
|
+
// there to settle. Stop short and retain instead — the same trade␊
|
|
2968
|
+
// \`__rollbackWasmEnvForWasiInitialization\` makes for the settlement drain, so␊
|
|
2969
|
+
// the context stays reclaimable by a retry or by this flavor's own␊
|
|
2970
|
+
// last-resort teardown.␊
|
|
2971
|
+
const __retainAfterAsyncWorkDrainFailure = (cleanupError) =>␊
|
|
2972
|
+
__retainFailedWasiRollback([cleanupError])␊
|
|
2973
|
+
let asyncWorkResult␊
|
|
2376
2974
|
try {␊
|
|
2377
|
-
|
|
2378
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
2975
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
2379
2976
|
} catch (cleanupError) {␊
|
|
2380
|
-
|
|
2381
|
-
settlementsUnreached = true␊
|
|
2977
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
2382
2978
|
}␊
|
|
2383
|
-
if (__isThenable(
|
|
2384
|
-
return Promise.resolve(
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
cleanupErrors.push(cleanupError)␊
|
|
2388
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2389
|
-
},␊
|
|
2979
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
2980
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
2981
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
2982
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
2390
2983
|
)␊
|
|
2391
2984
|
}␊
|
|
2392
|
-
|
|
2393
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2394
|
-
}␊
|
|
2395
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
2985
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
2396
2986
|
}␊
|
|
2397
2987
|
␊
|
|
2398
2988
|
let __wasiModule␊
|
|
@@ -2414,7 +3004,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2414
3004
|
context: __emnapiContext,␊
|
|
2415
3005
|
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
2416
3006
|
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
2417
|
-
plugins: [
|
|
3007
|
+
plugins: [␊
|
|
3008
|
+
__captureWasiThreadManager,␊
|
|
3009
|
+
__emnapiAsyncWorkPlugin,␊
|
|
3010
|
+
__emnapiTSFNPlugin,␊
|
|
3011
|
+
],␊
|
|
2418
3012
|
wasi: __wasi,␊
|
|
2419
3013
|
onCreateWorker() {␊
|
|
2420
3014
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {␊
|
|
@@ -2598,6 +3192,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2598
3192
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
2599
3193
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
2600
3194
|
let __wasiDisposed = false␊
|
|
3195
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
2601
3196
|
let __wasiDisposePromise␊
|
|
2602
3197
|
let __completeWasiDisposal = function () {}␊
|
|
2603
3198
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -2785,6 +3380,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2785
3380
|
}␊
|
|
2786
3381
|
})()␊
|
|
2787
3382
|
␊
|
|
3383
|
+
// A real, *referenced* timer, for waits that must let the whole host make␊
|
|
3384
|
+
// progress between looks — the async-work drain polls the addon rather than␊
|
|
3385
|
+
// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there␊
|
|
3386
|
+
// would spin the loop instead of yielding it. Falls back to the macrotask␊
|
|
3387
|
+
// scheduler on a host without timers.␊
|
|
3388
|
+
function __scheduleTimer(callback, delay) {␊
|
|
3389
|
+
const setTimer = globalThis.setTimeout␊
|
|
3390
|
+
if (typeof setTimer !== 'function') {␊
|
|
3391
|
+
__scheduleMacrotask(callback)␊
|
|
3392
|
+
return␊
|
|
3393
|
+
}␊
|
|
3394
|
+
try {␊
|
|
3395
|
+
setTimer(callback, delay)␊
|
|
3396
|
+
} catch {␊
|
|
3397
|
+
__scheduleMacrotask(callback)␊
|
|
3398
|
+
}␊
|
|
3399
|
+
}␊
|
|
3400
|
+
␊
|
|
2788
3401
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
2789
3402
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
2790
3403
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -2985,6 +3598,135 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2985
3598
|
)␊
|
|
2986
3599
|
}␊
|
|
2987
3600
|
␊
|
|
3601
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait␊
|
|
3602
|
+
// ends when the addon reports zero, so this only decides how promptly disposal␊
|
|
3603
|
+
// notices — not how long it waits.␊
|
|
3604
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1␊
|
|
3605
|
+
␊
|
|
3606
|
+
/**␊
|
|
3607
|
+
* Settles this addon's outstanding \`napi_async_work\` before the teardown that␊
|
|
3608
|
+
* would strand it.␊
|
|
3609
|
+
*␊
|
|
3610
|
+
* \`napi_prepare_wasm_env_cleanup\` does not cover async work, and nothing about␊
|
|
3611
|
+
* it is observable from JavaScript: the threadless archive resolves␊
|
|
3612
|
+
* \`napi_*_async_work\` through the \`@emnapi/core\` plugins, but the threaded one␊
|
|
3613
|
+
* links the C \`async_work.c\` on the uv threadpool, so there the wasm neither␊
|
|
3614
|
+
* imports nor exports those symbols and the only brackets a loader could watch␊
|
|
3615
|
+
* (\`_emnapi_ctx_*_waiting_request_counter\`) are shared with threadsafe␊
|
|
3616
|
+
* functions. The addon is the one place both flavors go through, so it answers␊
|
|
3617
|
+
* for both, through the same kind of handshake the settlement drain uses:␊
|
|
3618
|
+
*␊
|
|
3619
|
+
* - \`napi_wasm_cancel_pending_async_work()\` cancels what no thread has␊
|
|
3620
|
+
* started. Those completion callbacks run with \`napi_cancelled\`, which␊
|
|
3621
|
+
* napi-rs turns into a promise rejected with an \`AbortError\`.␊
|
|
3622
|
+
* - \`napi_wasm_async_work_pending()\` counts what is still owed a completion␊
|
|
3623
|
+
* callback. Work already executing refuses cancellation and stays counted␊
|
|
3624
|
+
* until it finishes normally — which it can, because this runs before the␊
|
|
3625
|
+
* barrier, before \`Context.destroy()\` and before anything is terminated.␊
|
|
3626
|
+
*␊
|
|
3627
|
+
* Both exports are optional: an addon built against a napi crate that predates␊
|
|
3628
|
+
* them drains nothing and keeps the previous behavior, exactly as the␊
|
|
3629
|
+
* \`napi_wasm_env_cleanup_pending\` handshake degrades.␊
|
|
3630
|
+
*␊
|
|
3631
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal␊
|
|
3632
|
+
* synchronous in the common case. The promise it returns otherwise never␊
|
|
3633
|
+
* rejects.␊
|
|
3634
|
+
*␊
|
|
3635
|
+
* The wait has no deadline, and that is the point: giving up would destroy the␊
|
|
3636
|
+
* environment with a completion callback still owed, which is the stranding␊
|
|
3637
|
+
* this exists to prevent. A task whose \`execute\` never returns already keeps an␊
|
|
3638
|
+
* *undisposed* process alive in exactly the same way, so disposal inherits that␊
|
|
3639
|
+
* rather than inventing a bound it cannot honor.␊
|
|
3640
|
+
*␊
|
|
3641
|
+
* Safe to call from inside a completion callback, which is reachable: settling␊
|
|
3642
|
+
* a task runs addon code that can re-enter JavaScript — a setter on the value␊
|
|
3643
|
+
* being handed back, a threadsafe-function callback — and that JavaScript can␊
|
|
3644
|
+
* call \`dispose()\`. Two things make it terminate rather than wait on itself:␊
|
|
3645
|
+
*␊
|
|
3646
|
+
* - The addon keeps a work registered until its completion callback␊
|
|
3647
|
+
* *finishes*, so the count read here is at least one and this takes the␊
|
|
3648
|
+
* polling path instead of declaring the environment drained and tearing it␊
|
|
3649
|
+
* down from inside the frame that is still settling a promise.␊
|
|
3650
|
+
* - The poll is a timer, so it cannot run until the callback has returned to␊
|
|
3651
|
+
* the host — by which time that work has left the registry. The count the␊
|
|
3652
|
+
* next poll reads is the one taken after the callback finished.␊
|
|
3653
|
+
*␊
|
|
3654
|
+
* \`__disposeWasiBinding\` hands every caller the same in-flight promise, so the␊
|
|
3655
|
+
* nested call joins this disposal rather than starting a second one.␊
|
|
3656
|
+
*/␊
|
|
3657
|
+
function __drainWasiAsyncWork() {␊
|
|
3658
|
+
if (__wasiAsyncWorkDrainPromise !== undefined) {␊
|
|
3659
|
+
return __wasiAsyncWorkDrainPromise␊
|
|
3660
|
+
}␊
|
|
3661
|
+
const exports = __napiInstance?.exports␊
|
|
3662
|
+
const pending = exports?.napi_wasm_async_work_pending␊
|
|
3663
|
+
const cancelPending = exports?.napi_wasm_cancel_pending_async_work␊
|
|
3664
|
+
if (typeof pending !== 'function' || typeof cancelPending !== 'function') {␊
|
|
3665
|
+
return␊
|
|
3666
|
+
}␊
|
|
3667
|
+
␊
|
|
3668
|
+
const readPending = () => {␊
|
|
3669
|
+
try {␊
|
|
3670
|
+
return pending()␊
|
|
3671
|
+
} catch (error) {␊
|
|
3672
|
+
// A trap is the only way this call fails: it reads a counter and cannot␊
|
|
3673
|
+
// allocate or call back into JavaScript. A trapped instance can no longer␊
|
|
3674
|
+
// run anything, so its outstanding work is unreachable by definition —␊
|
|
3675
|
+
// there is nothing left to wait for, and refusing to dispose would only␊
|
|
3676
|
+
// keep a dead instance and its stuck counter alive. Best-effort here is␊
|
|
3677
|
+
// the honest answer, and it is what disposal did before this drain␊
|
|
3678
|
+
// existed.␊
|
|
3679
|
+
//␊
|
|
3680
|
+
// Only a trap. Anything else means the export is not what this loader␊
|
|
3681
|
+
// thinks it is, which is a defect worth surfacing rather than disposing␊
|
|
3682
|
+
// over.␊
|
|
3683
|
+
if (error instanceof globalThis.WebAssembly.RuntimeError) {␊
|
|
3684
|
+
return 0␊
|
|
3685
|
+
}␊
|
|
3686
|
+
throw error␊
|
|
3687
|
+
}␊
|
|
3688
|
+
}␊
|
|
3689
|
+
␊
|
|
3690
|
+
if (!readPending()) {␊
|
|
3691
|
+
return␊
|
|
3692
|
+
}␊
|
|
3693
|
+
try {␊
|
|
3694
|
+
cancelPending()␊
|
|
3695
|
+
} catch {␊
|
|
3696
|
+
// Cancellation is an optimization: it bounds the wait by the work already␊
|
|
3697
|
+
// executing. Failing it only means waiting for the whole queue instead.␊
|
|
3698
|
+
}␊
|
|
3699
|
+
if (!readPending()) {␊
|
|
3700
|
+
return␊
|
|
3701
|
+
}␊
|
|
3702
|
+
␊
|
|
3703
|
+
const drainPromise = __keepEventLoopAliveUntil(␊
|
|
3704
|
+
(async () => {␊
|
|
3705
|
+
while (readPending()) {␊
|
|
3706
|
+
await new Promise((resolve) => {␊
|
|
3707
|
+
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)␊
|
|
3708
|
+
})␊
|
|
3709
|
+
}␊
|
|
3710
|
+
})(),␊
|
|
3711
|
+
).then(␊
|
|
3712
|
+
() => {␊
|
|
3713
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
3714
|
+
},␊
|
|
3715
|
+
(error) => {␊
|
|
3716
|
+
// A wait that could not run is not a wait that finished. The only way␊
|
|
3717
|
+
// here is a host whose timers and macrotask primitives all refuse, and␊
|
|
3718
|
+
// the work is still outstanding — reporting success would destroy the␊
|
|
3719
|
+
// environment over it, which is the stranding this exists to prevent.␊
|
|
3720
|
+
// Reject instead: disposal stays retryable, and the context is not␊
|
|
3721
|
+
// destroyed. Clearing the memo first is what makes the retry re-run this.␊
|
|
3722
|
+
__wasiAsyncWorkDrainPromise = undefined␊
|
|
3723
|
+
throw error␊
|
|
3724
|
+
},␊
|
|
3725
|
+
)␊
|
|
3726
|
+
__wasiAsyncWorkDrainPromise = drainPromise␊
|
|
3727
|
+
return drainPromise␊
|
|
3728
|
+
}␊
|
|
3729
|
+
␊
|
|
2988
3730
|
/**␊
|
|
2989
3731
|
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
2990
3732
|
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
@@ -3066,7 +3808,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3066
3808
|
return __finishWasiDisposal()␊
|
|
3067
3809
|
}␊
|
|
3068
3810
|
␊
|
|
3069
|
-
function
|
|
3811
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
3070
3812
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
3071
3813
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
3072
3814
|
// back to back is what strands them.␊
|
|
@@ -3078,6 +3820,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3078
3820
|
return __continueWasiDisposal()␊
|
|
3079
3821
|
}␊
|
|
3080
3822
|
␊
|
|
3823
|
+
function __startWasiDisposal() {␊
|
|
3824
|
+
// Outstanding \`napi_async_work\` goes first, while the environment is still␊
|
|
3825
|
+
// completely live: the completion callbacks run addon code, and everything␊
|
|
3826
|
+
// after this point takes that away from them — the barrier shuts the async␊
|
|
3827
|
+
// runtime down, \`Context.destroy()\` stops JavaScript calls, and terminating␊
|
|
3828
|
+
// the pool threads removes what would have reported the work finished.␊
|
|
3829
|
+
const asyncWorkResult = __drainWasiAsyncWork()␊
|
|
3830
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
3831
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
3832
|
+
__cleanUpWasmEnvForWasiDisposal,␊
|
|
3833
|
+
)␊
|
|
3834
|
+
}␊
|
|
3835
|
+
return __cleanUpWasmEnvForWasiDisposal()␊
|
|
3836
|
+
}␊
|
|
3837
|
+
␊
|
|
3081
3838
|
/**␊
|
|
3082
3839
|
* Disposes this generated WASI binding.␊
|
|
3083
3840
|
*␊
|
|
@@ -3219,29 +3976,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3219
3976
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
3220
3977
|
*/␊
|
|
3221
3978
|
function __rollbackWasiInitialization() {␊
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3979
|
+
// The environment teardown this rollback performs, kept nested so it cannot␊
|
|
3980
|
+
// be reached without the async-work drain below running first.␊
|
|
3981
|
+
function __rollbackWasmEnvForWasiInitialization() {␊
|
|
3982
|
+
const cleanupErrors = []␊
|
|
3983
|
+
let drainResult␊
|
|
3984
|
+
let settlementsUnreached = false␊
|
|
3985
|
+
try {␊
|
|
3986
|
+
__prepareWasmEnvCleanup()␊
|
|
3987
|
+
drainResult = __drainWasmEnvCleanup()␊
|
|
3988
|
+
} catch (cleanupError) {␊
|
|
3989
|
+
cleanupErrors.push(cleanupError)␊
|
|
3990
|
+
settlementsUnreached = true␊
|
|
3991
|
+
}␊
|
|
3992
|
+
if (__isThenable(drainResult)) {␊
|
|
3993
|
+
return Promise.resolve(drainResult).then(␊
|
|
3994
|
+
() => __destroyContextForWasiRollback(cleanupErrors),␊
|
|
3995
|
+
(cleanupError) => {␊
|
|
3996
|
+
cleanupErrors.push(cleanupError)␊
|
|
3997
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
3998
|
+
},␊
|
|
3999
|
+
)␊
|
|
4000
|
+
}␊
|
|
4001
|
+
if (settlementsUnreached) {␊
|
|
4002
|
+
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
4003
|
+
}␊
|
|
4004
|
+
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
4005
|
+
}␊
|
|
4006
|
+
␊
|
|
4007
|
+
// Same reason as \`__startWasiDisposal\`: a module-init hook can start async␊
|
|
4008
|
+
// work before the load goes on to fail, and this rollback tears down exactly␊
|
|
4009
|
+
// what those completions need. Settle them while everything is still live,␊
|
|
4010
|
+
// before the barrier and the teardown above take that away.␊
|
|
4011
|
+
//␊
|
|
4012
|
+
// A drain that could not finish leaves async work possibly outstanding, and␊
|
|
4013
|
+
// destroying the context over it would strand exactly what this rollback is␊
|
|
4014
|
+
// there to settle. Stop short and retain instead — the same trade␊
|
|
4015
|
+
// \`__rollbackWasmEnvForWasiInitialization\` makes for the settlement drain, so␊
|
|
4016
|
+
// the context stays reclaimable by a retry or by this flavor's own␊
|
|
4017
|
+
// last-resort teardown.␊
|
|
4018
|
+
const __retainAfterAsyncWorkDrainFailure = (cleanupError) =>␊
|
|
4019
|
+
__retainFailedWasiRollback([cleanupError])␊
|
|
4020
|
+
let asyncWorkResult␊
|
|
3225
4021
|
try {␊
|
|
3226
|
-
|
|
3227
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
4022
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
3228
4023
|
} catch (cleanupError) {␊
|
|
3229
|
-
|
|
3230
|
-
settlementsUnreached = true␊
|
|
4024
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
3231
4025
|
}␊
|
|
3232
|
-
if (__isThenable(
|
|
3233
|
-
return Promise.resolve(
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
cleanupErrors.push(cleanupError)␊
|
|
3237
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
3238
|
-
},␊
|
|
4026
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
4027
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
4028
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
4029
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
3239
4030
|
)␊
|
|
3240
4031
|
}␊
|
|
3241
|
-
|
|
3242
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
3243
|
-
}␊
|
|
3244
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
4032
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
3245
4033
|
}␊
|
|
3246
4034
|
␊
|
|
3247
4035
|
let __wasiModule␊
|
|
@@ -3262,7 +4050,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3262
4050
|
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {␊
|
|
3263
4051
|
context: __emnapiContext,␊
|
|
3264
4052
|
asyncWorkPoolSize: 0,␊
|
|
3265
|
-
plugins: [
|
|
4053
|
+
plugins: [␊
|
|
4054
|
+
__captureWasiThreadManager,␊
|
|
4055
|
+
__emnapiAsyncWorkPlugin,␊
|
|
4056
|
+
__emnapiTSFNPlugin,␊
|
|
4057
|
+
],␊
|
|
3266
4058
|
wasi: __wasi,␊
|
|
3267
4059
|
overwriteImports(importObject) {␊
|
|
3268
4060
|
importObject.env = {␊
|
|
@@ -3775,6 +4567,101 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3775
4567
|
})()␊
|
|
3776
4568
|
}␊
|
|
3777
4569
|
␊
|
|
4570
|
+
// A real, *referenced* timer for the async-work wait below, which polls the␊
|
|
4571
|
+
// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay␊
|
|
4572
|
+
// macrotask there would spin the loop instead of yielding it. Falls back to the␊
|
|
4573
|
+
// macrotask scheduler on a host without timers.␊
|
|
4574
|
+
function __scheduleTimer(__callback, __delay) {␊
|
|
4575
|
+
const __setTimer = globalThis.setTimeout␊
|
|
4576
|
+
if (typeof __setTimer !== 'function') {␊
|
|
4577
|
+
__scheduleMacrotask(__callback)␊
|
|
4578
|
+
return␊
|
|
4579
|
+
}␊
|
|
4580
|
+
try {␊
|
|
4581
|
+
__setTimer(__callback, __delay)␊
|
|
4582
|
+
} catch {␊
|
|
4583
|
+
__scheduleMacrotask(__callback)␊
|
|
4584
|
+
}␊
|
|
4585
|
+
}␊
|
|
4586
|
+
␊
|
|
4587
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait␊
|
|
4588
|
+
// ends when the addon reports zero, so this only decides how promptly disposal␊
|
|
4589
|
+
// notices — not how long it waits.␊
|
|
4590
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1␊
|
|
4591
|
+
␊
|
|
4592
|
+
/**␊
|
|
4593
|
+
* Settles this instance's outstanding \`napi_async_work\` before the teardown␊
|
|
4594
|
+
* that would strand it.␊
|
|
4595
|
+
*␊
|
|
4596
|
+
* The same hole the eager loaders had, and the same fix: the environment␊
|
|
4597
|
+
* cleanup barrier covers promise settlements queued on the threadsafe-function␊
|
|
4598
|
+
* queue and says nothing about \`napi_async_work\`, so destroying the context␊
|
|
4599
|
+
* with a work still outstanding leaves its completion callback with nowhere to␊
|
|
4600
|
+
* run and its promise unsettled forever.␊
|
|
4601
|
+
*␊
|
|
4602
|
+
* This flavor is threadless, so \`compute\` runs on the JavaScript thread inside␊
|
|
4603
|
+
* the macrotask that dequeued it: while this is running, any outstanding work␊
|
|
4604
|
+
* is queued rather than executing, and \`napi_wasm_cancel_pending_async_work\`␊
|
|
4605
|
+
* can take all of it. The poll is still what decides when the drain is done —␊
|
|
4606
|
+
* cancellation delivers those completions from a later macrotask, not␊
|
|
4607
|
+
* synchronously.␊
|
|
4608
|
+
*␊
|
|
4609
|
+
* Per instance, from that instance's own exports: two instances of this loader␊
|
|
4610
|
+
* have separate registries and must not wait on each other.␊
|
|
4611
|
+
*␊
|
|
4612
|
+
* Both exports are optional, so an addon built against a napi crate that␊
|
|
4613
|
+
* predates them keeps the previous behavior.␊
|
|
4614
|
+
*␊
|
|
4615
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal␊
|
|
4616
|
+
* synchronous in the common case. No deadline: giving up would destroy the␊
|
|
4617
|
+
* environment with a completion callback still owed.␊
|
|
4618
|
+
*/␊
|
|
4619
|
+
function __drainInstanceAsyncWork(__instance) {␊
|
|
4620
|
+
const __exports = __instance?.exports␊
|
|
4621
|
+
const __pending = __exports?.napi_wasm_async_work_pending␊
|
|
4622
|
+
const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work␊
|
|
4623
|
+
if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') {␊
|
|
4624
|
+
return␊
|
|
4625
|
+
}␊
|
|
4626
|
+
␊
|
|
4627
|
+
const __readPending = () => {␊
|
|
4628
|
+
try {␊
|
|
4629
|
+
return __pending()␊
|
|
4630
|
+
} catch (__error) {␊
|
|
4631
|
+
// A trap is the only way this call fails: it reads a counter and cannot␊
|
|
4632
|
+
// allocate or call back into JavaScript. A trapped instance can no longer␊
|
|
4633
|
+
// run anything, so its outstanding work is unreachable by definition and␊
|
|
4634
|
+
// best-effort is the honest answer. Anything else means the export is not␊
|
|
4635
|
+
// what this loader thinks it is — a defect worth surfacing.␊
|
|
4636
|
+
if (__error instanceof globalThis.WebAssembly.RuntimeError) {␊
|
|
4637
|
+
return 0␊
|
|
4638
|
+
}␊
|
|
4639
|
+
throw __error␊
|
|
4640
|
+
}␊
|
|
4641
|
+
}␊
|
|
4642
|
+
␊
|
|
4643
|
+
if (!__readPending()) {␊
|
|
4644
|
+
return␊
|
|
4645
|
+
}␊
|
|
4646
|
+
try {␊
|
|
4647
|
+
__cancelPending()␊
|
|
4648
|
+
} catch {␊
|
|
4649
|
+
// Cancellation only bounds the wait. Failing it means waiting for the queue␊
|
|
4650
|
+
// to run instead, which the poll below already does.␊
|
|
4651
|
+
}␊
|
|
4652
|
+
if (!__readPending()) {␊
|
|
4653
|
+
return␊
|
|
4654
|
+
}␊
|
|
4655
|
+
␊
|
|
4656
|
+
return (async () => {␊
|
|
4657
|
+
while (__readPending()) {␊
|
|
4658
|
+
await new Promise((__resolve) => {␊
|
|
4659
|
+
__scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)␊
|
|
4660
|
+
})␊
|
|
4661
|
+
}␊
|
|
4662
|
+
})()␊
|
|
4663
|
+
}␊
|
|
4664
|
+
␊
|
|
3778
4665
|
function __createLifecycleReentryError(__operation) {␊
|
|
3779
4666
|
const __error = new Error(␊
|
|
3780
4667
|
__operation +␊
|
|
@@ -4269,7 +5156,15 @@ Generated by [AVA](https://avajs.dev).
|
|
|
4269
5156
|
if (__lifecycleState !== 'failed') {␊
|
|
4270
5157
|
__lifecycleState = 'disposal'␊
|
|
4271
5158
|
}␊
|
|
4272
|
-
//
|
|
5159
|
+
// Outstanding async work first, while the environment is still completely␊
|
|
5160
|
+
// live: its completion callbacks run addon code, and the barrier and␊
|
|
5161
|
+
// \`Context.destroy()\` below each take that away. Undefined unless work is␊
|
|
5162
|
+
// outstanding.␊
|
|
5163
|
+
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)␊
|
|
5164
|
+
if (__asyncWorkDrained) {␊
|
|
5165
|
+
await __asyncWorkDrained␊
|
|
5166
|
+
}␊
|
|
5167
|
+
// Then settle what the barrier cancelled, before the environment stops␊
|
|
4273
5168
|
// accepting JavaScript calls. Undefined unless something is queued, so␊
|
|
4274
5169
|
// an idle disposal is not delayed by a single turn.␊
|
|
4275
5170
|
const __drained = __prepareForDisposal()␊
|
|
@@ -4424,6 +5319,14 @@ Generated by [AVA](https://avajs.dev).
|
|
|
4424
5319
|
// so a failure before beforeInit costs no extra turn.␊
|
|
4425
5320
|
let __settlementsUnreached = false␊
|
|
4426
5321
|
try {␊
|
|
5322
|
+
// Registration can start async work too, and this path destroys the same␊
|
|
5323
|
+
// environment its completions need. A drain that cannot finish leaves the␊
|
|
5324
|
+
// work outstanding, so it counts as settlements unreached and stops the␊
|
|
5325
|
+
// rollback short of destroying, exactly like a failed barrier drain.␊
|
|
5326
|
+
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)␊
|
|
5327
|
+
if (__asyncWorkDrained) {␊
|
|
5328
|
+
await __asyncWorkDrained␊
|
|
5329
|
+
}␊
|
|
4427
5330
|
const __drained = __prepareForDisposal()␊
|
|
4428
5331
|
if (__drained) {␊
|
|
4429
5332
|
await __drained␊
|
|
@@ -5162,6 +6065,101 @@ Generated by [AVA](https://avajs.dev).
|
|
|
5162
6065
|
})()␊
|
|
5163
6066
|
}␊
|
|
5164
6067
|
␊
|
|
6068
|
+
// A real, *referenced* timer for the async-work wait below, which polls the␊
|
|
6069
|
+
// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay␊
|
|
6070
|
+
// macrotask there would spin the loop instead of yielding it. Falls back to the␊
|
|
6071
|
+
// macrotask scheduler on a host without timers.␊
|
|
6072
|
+
function __scheduleTimer(__callback, __delay) {␊
|
|
6073
|
+
const __setTimer = globalThis.setTimeout␊
|
|
6074
|
+
if (typeof __setTimer !== 'function') {␊
|
|
6075
|
+
__scheduleMacrotask(__callback)␊
|
|
6076
|
+
return␊
|
|
6077
|
+
}␊
|
|
6078
|
+
try {␊
|
|
6079
|
+
__setTimer(__callback, __delay)␊
|
|
6080
|
+
} catch {␊
|
|
6081
|
+
__scheduleMacrotask(__callback)␊
|
|
6082
|
+
}␊
|
|
6083
|
+
}␊
|
|
6084
|
+
␊
|
|
6085
|
+
// How often to re-read \`napi_wasm_async_work_pending\` while waiting. The wait␊
|
|
6086
|
+
// ends when the addon reports zero, so this only decides how promptly disposal␊
|
|
6087
|
+
// notices — not how long it waits.␊
|
|
6088
|
+
const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1␊
|
|
6089
|
+
␊
|
|
6090
|
+
/**␊
|
|
6091
|
+
* Settles this instance's outstanding \`napi_async_work\` before the teardown␊
|
|
6092
|
+
* that would strand it.␊
|
|
6093
|
+
*␊
|
|
6094
|
+
* The same hole the eager loaders had, and the same fix: the environment␊
|
|
6095
|
+
* cleanup barrier covers promise settlements queued on the threadsafe-function␊
|
|
6096
|
+
* queue and says nothing about \`napi_async_work\`, so destroying the context␊
|
|
6097
|
+
* with a work still outstanding leaves its completion callback with nowhere to␊
|
|
6098
|
+
* run and its promise unsettled forever.␊
|
|
6099
|
+
*␊
|
|
6100
|
+
* This flavor is threadless, so \`compute\` runs on the JavaScript thread inside␊
|
|
6101
|
+
* the macrotask that dequeued it: while this is running, any outstanding work␊
|
|
6102
|
+
* is queued rather than executing, and \`napi_wasm_cancel_pending_async_work\`␊
|
|
6103
|
+
* can take all of it. The poll is still what decides when the drain is done —␊
|
|
6104
|
+
* cancellation delivers those completions from a later macrotask, not␊
|
|
6105
|
+
* synchronously.␊
|
|
6106
|
+
*␊
|
|
6107
|
+
* Per instance, from that instance's own exports: two instances of this loader␊
|
|
6108
|
+
* have separate registries and must not wait on each other.␊
|
|
6109
|
+
*␊
|
|
6110
|
+
* Both exports are optional, so an addon built against a napi crate that␊
|
|
6111
|
+
* predates them keeps the previous behavior.␊
|
|
6112
|
+
*␊
|
|
6113
|
+
* Returns nothing when there is nothing outstanding, which keeps disposal␊
|
|
6114
|
+
* synchronous in the common case. No deadline: giving up would destroy the␊
|
|
6115
|
+
* environment with a completion callback still owed.␊
|
|
6116
|
+
*/␊
|
|
6117
|
+
function __drainInstanceAsyncWork(__instance) {␊
|
|
6118
|
+
const __exports = __instance?.exports␊
|
|
6119
|
+
const __pending = __exports?.napi_wasm_async_work_pending␊
|
|
6120
|
+
const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work␊
|
|
6121
|
+
if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') {␊
|
|
6122
|
+
return␊
|
|
6123
|
+
}␊
|
|
6124
|
+
␊
|
|
6125
|
+
const __readPending = () => {␊
|
|
6126
|
+
try {␊
|
|
6127
|
+
return __pending()␊
|
|
6128
|
+
} catch (__error) {␊
|
|
6129
|
+
// A trap is the only way this call fails: it reads a counter and cannot␊
|
|
6130
|
+
// allocate or call back into JavaScript. A trapped instance can no longer␊
|
|
6131
|
+
// run anything, so its outstanding work is unreachable by definition and␊
|
|
6132
|
+
// best-effort is the honest answer. Anything else means the export is not␊
|
|
6133
|
+
// what this loader thinks it is — a defect worth surfacing.␊
|
|
6134
|
+
if (__error instanceof globalThis.WebAssembly.RuntimeError) {␊
|
|
6135
|
+
return 0␊
|
|
6136
|
+
}␊
|
|
6137
|
+
throw __error␊
|
|
6138
|
+
}␊
|
|
6139
|
+
}␊
|
|
6140
|
+
␊
|
|
6141
|
+
if (!__readPending()) {␊
|
|
6142
|
+
return␊
|
|
6143
|
+
}␊
|
|
6144
|
+
try {␊
|
|
6145
|
+
__cancelPending()␊
|
|
6146
|
+
} catch {␊
|
|
6147
|
+
// Cancellation only bounds the wait. Failing it means waiting for the queue␊
|
|
6148
|
+
// to run instead, which the poll below already does.␊
|
|
6149
|
+
}␊
|
|
6150
|
+
if (!__readPending()) {␊
|
|
6151
|
+
return␊
|
|
6152
|
+
}␊
|
|
6153
|
+
␊
|
|
6154
|
+
return (async () => {␊
|
|
6155
|
+
while (__readPending()) {␊
|
|
6156
|
+
await new Promise((__resolve) => {␊
|
|
6157
|
+
__scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)␊
|
|
6158
|
+
})␊
|
|
6159
|
+
}␊
|
|
6160
|
+
})()␊
|
|
6161
|
+
}␊
|
|
6162
|
+
␊
|
|
5165
6163
|
function __createLifecycleReentryError(__operation) {␊
|
|
5166
6164
|
const __error = new Error(␊
|
|
5167
6165
|
__operation +␊
|
|
@@ -5677,7 +6675,15 @@ Generated by [AVA](https://avajs.dev).
|
|
|
5677
6675
|
if (__lifecycleState !== 'failed') {␊
|
|
5678
6676
|
__lifecycleState = 'disposal'␊
|
|
5679
6677
|
}␊
|
|
5680
|
-
//
|
|
6678
|
+
// Outstanding async work first, while the environment is still completely␊
|
|
6679
|
+
// live: its completion callbacks run addon code, and the barrier and␊
|
|
6680
|
+
// \`Context.destroy()\` below each take that away. Undefined unless work is␊
|
|
6681
|
+
// outstanding.␊
|
|
6682
|
+
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)␊
|
|
6683
|
+
if (__asyncWorkDrained) {␊
|
|
6684
|
+
await __asyncWorkDrained␊
|
|
6685
|
+
}␊
|
|
6686
|
+
// Then settle what the barrier cancelled, before the environment stops␊
|
|
5681
6687
|
// accepting JavaScript calls. Undefined unless something is queued, so␊
|
|
5682
6688
|
// an idle disposal is not delayed by a single turn.␊
|
|
5683
6689
|
const __drained = __prepareForDisposal()␊
|
|
@@ -5861,6 +6867,14 @@ Generated by [AVA](https://avajs.dev).
|
|
|
5861
6867
|
// so a failure before beforeInit costs no extra turn.␊
|
|
5862
6868
|
let __settlementsUnreached = false␊
|
|
5863
6869
|
try {␊
|
|
6870
|
+
// Registration can start async work too, and this path destroys the same␊
|
|
6871
|
+
// environment its completions need. A drain that cannot finish leaves the␊
|
|
6872
|
+
// work outstanding, so it counts as settlements unreached and stops the␊
|
|
6873
|
+
// rollback short of destroying, exactly like a failed barrier drain.␊
|
|
6874
|
+
const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance)␊
|
|
6875
|
+
if (__asyncWorkDrained) {␊
|
|
6876
|
+
await __asyncWorkDrained␊
|
|
6877
|
+
}␊
|
|
5864
6878
|
const __drained = __prepareForDisposal()␊
|
|
5865
6879
|
if (__drained) {␊
|
|
5866
6880
|
await __drained␊
|