@napi-rs/cli 3.10.0 → 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 +445 -26
- package/dist/index.cjs +445 -26
- package/dist/index.d.cts +1 -1
- package/dist/index.js +445 -26
- package/docs/wasi.md +17 -0
- package/package.json +1 -1
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +1492 -86
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/templates.spec.ts +223 -0
- package/src/api/templates/load-wasi-template.ts +473 -36
- package/src/utils/__tests__/reconciliation.spec.ts +9 -2
|
@@ -101,6 +101,35 @@ Generated by [AVA](https://avajs.dev).
|
|
|
101
101
|
␊
|
|
102
102
|
const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose')␊
|
|
103
103
|
const __wasiWorkers = new Set()␊
|
|
104
|
+
// The thread manager has to be reachable *before* anything that can throw␊
|
|
105
|
+
// during load or registration. Initialization can fail after the pool has␊
|
|
106
|
+
// already spawned workers, and the rollback still has to mark their␊
|
|
107
|
+
// terminations as expected — but \`__napiModule\` is assigned only when␊
|
|
108
|
+
// instantiation RETURNS, so on exactly that path it is still undefined. A␊
|
|
109
|
+
// plugin factory runs while the emnapi module is being created, before the␊
|
|
110
|
+
// wasm is loaded and before any registration function runs, and its context␊
|
|
111
|
+
// carries the very same manager instance.␊
|
|
112
|
+
let __wasiThreadManager␊
|
|
113
|
+
␊
|
|
114
|
+
function __captureWasiThreadManager(context) {␊
|
|
115
|
+
if (context && context.PThread) {␊
|
|
116
|
+
__wasiThreadManager = context.PThread␊
|
|
117
|
+
}␊
|
|
118
|
+
return {}␊
|
|
119
|
+
}␊
|
|
120
|
+
␊
|
|
121
|
+
function __getWasiThreadManager() {␊
|
|
122
|
+
const manager =␊
|
|
123
|
+
__wasiThreadManager !== undefined␊
|
|
124
|
+
? __wasiThreadManager␊
|
|
125
|
+
: __napiModule␊
|
|
126
|
+
? __napiModule.PThread␊
|
|
127
|
+
: undefined␊
|
|
128
|
+
if (manager && typeof manager.terminateWorker === 'function') {␊
|
|
129
|
+
return manager␊
|
|
130
|
+
}␊
|
|
131
|
+
return undefined␊
|
|
132
|
+
}␊
|
|
104
133
|
let __napiInstance␊
|
|
105
134
|
let __emnapiContextDestroyed = false␊
|
|
106
135
|
let __emnapiContextDestroyPromise␊
|
|
@@ -110,6 +139,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
110
139
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
111
140
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
112
141
|
let __wasiDisposed = false␊
|
|
142
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
113
143
|
let __wasiDisposePromise␊
|
|
114
144
|
let __completeWasiDisposal = function () {}␊
|
|
115
145
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -267,6 +297,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
267
297
|
}␊
|
|
268
298
|
})()␊
|
|
269
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
|
+
␊
|
|
270
318
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
271
319
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
272
320
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -423,13 +471,209 @@ Generated by [AVA](https://avajs.dev).
|
|
|
423
471
|
return destroyPromise␊
|
|
424
472
|
}␊
|
|
425
473
|
␊
|
|
474
|
+
/**␊
|
|
475
|
+
* Holds the event loop open until \`work\` settles.␊
|
|
476
|
+
*␊
|
|
477
|
+
* Nothing else can: the pool workers are deliberately unreferenced so an idle␊
|
|
478
|
+
* binding cannot keep a process alive, and referencing them again for the␊
|
|
479
|
+
* termination does not hold either — emnapi unreferences a worker the moment it␊
|
|
480
|
+
* reports \`async-thread-ready\`, which for a worker that was still starting␊
|
|
481
|
+
* lands *after* the termination began. Without a handle of its own, an␊
|
|
482
|
+
* \`await dispose()\` with nothing else pending exits the process with its␊
|
|
483
|
+
* promise unsettled, and everything after the \`await\` is skipped.␊
|
|
484
|
+
*␊
|
|
485
|
+
* The timer is cleared as soon as the work settles, so this never outlives the␊
|
|
486
|
+
* disposal that asked for it.␊
|
|
487
|
+
*/␊
|
|
488
|
+
function __keepEventLoopAliveUntil(work) {␊
|
|
489
|
+
const setTimer = globalThis.setInterval␊
|
|
490
|
+
const clearTimer = globalThis.clearInterval␊
|
|
491
|
+
if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') {␊
|
|
492
|
+
return work␊
|
|
493
|
+
}␊
|
|
494
|
+
let timer␊
|
|
495
|
+
try {␊
|
|
496
|
+
timer = setTimer(function () {}, 50)␊
|
|
497
|
+
} catch {␊
|
|
498
|
+
return work␊
|
|
499
|
+
}␊
|
|
500
|
+
const release = function () {␊
|
|
501
|
+
try {␊
|
|
502
|
+
clearTimer(timer)␊
|
|
503
|
+
} catch {}␊
|
|
504
|
+
}␊
|
|
505
|
+
return work.then(␊
|
|
506
|
+
(value) => {␊
|
|
507
|
+
release()␊
|
|
508
|
+
return value␊
|
|
509
|
+
},␊
|
|
510
|
+
(error) => {␊
|
|
511
|
+
release()␊
|
|
512
|
+
throw error␊
|
|
513
|
+
},␊
|
|
514
|
+
)␊
|
|
515
|
+
}␊
|
|
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
|
+
␊
|
|
646
|
+
/**␊
|
|
647
|
+
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
648
|
+
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
649
|
+
* the manager's \`exit\` listener instead, which reports␊
|
|
650
|
+
* \`worker (tid = N) sent an error! ... stopped with exit code 1\` and rethrows␊
|
|
651
|
+
* inside the emit — aborting the \`once('exit')\` that backs the terminate␊
|
|
652
|
+
* promise, so disposal never settles and the process dies with an uncaught␊
|
|
653
|
+
* exception. Mark the termination through the manager first.␊
|
|
654
|
+
*␊
|
|
655
|
+
* The manager comes from \`__getWasiThreadManager\`, not from \`__napiModule\`:␊
|
|
656
|
+
* the initialization rollback runs on the one path where instantiation never␊
|
|
657
|
+
* returned, so \`__napiModule\` is still undefined there while the workers it␊
|
|
658
|
+
* spawned are already registered and loaded.␊
|
|
659
|
+
*␊
|
|
660
|
+
* Not \`terminateAllThreads()\`: that one recreates the pool it just shut down.␊
|
|
661
|
+
*/␊
|
|
426
662
|
function __terminateWasiWorkers() {␊
|
|
427
663
|
const cleanupErrors = []␊
|
|
428
664
|
const pending = []␊
|
|
665
|
+
const threadManager = __getWasiThreadManager()␊
|
|
429
666
|
␊
|
|
430
667
|
for (const worker of __wasiWorkers) {␊
|
|
431
668
|
let result␊
|
|
432
669
|
try {␊
|
|
670
|
+
if (threadManager) {␊
|
|
671
|
+
threadManager.terminateWorker(worker)␊
|
|
672
|
+
// \`terminateWorker\` leaves behind a reporter that logs every message␊
|
|
673
|
+
// still queued on the port, which Node flushes on exit. Nothing is␊
|
|
674
|
+
// listening for those any more.␊
|
|
675
|
+
worker.onmessage = undefined␊
|
|
676
|
+
}␊
|
|
433
677
|
result = worker.terminate()␊
|
|
434
678
|
} catch (error) {␊
|
|
435
679
|
cleanupErrors.push(error)␊
|
|
@@ -459,7 +703,9 @@ Generated by [AVA](https://avajs.dev).
|
|
|
459
703
|
)␊
|
|
460
704
|
}␊
|
|
461
705
|
}␊
|
|
462
|
-
return pending.length > 0
|
|
706
|
+
return pending.length > 0␊
|
|
707
|
+
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)␊
|
|
708
|
+
: finish()␊
|
|
463
709
|
}␊
|
|
464
710
|
␊
|
|
465
711
|
function __finishWasiDisposal() {␊
|
|
@@ -478,7 +724,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
478
724
|
return __finishWasiDisposal()␊
|
|
479
725
|
}␊
|
|
480
726
|
␊
|
|
481
|
-
function
|
|
727
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
482
728
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
483
729
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
484
730
|
// back to back is what strands them.␊
|
|
@@ -490,6 +736,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
490
736
|
return __continueWasiDisposal()␊
|
|
491
737
|
}␊
|
|
492
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
|
+
␊
|
|
493
754
|
/**␊
|
|
494
755
|
* Disposes this generated WASI binding.␊
|
|
495
756
|
*␊
|
|
@@ -631,29 +892,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
631
892
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
632
893
|
*/␊
|
|
633
894
|
function __rollbackWasiInitialization() {␊
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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␊
|
|
637
937
|
try {␊
|
|
638
|
-
|
|
639
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
938
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
640
939
|
} catch (cleanupError) {␊
|
|
641
|
-
|
|
642
|
-
settlementsUnreached = true␊
|
|
940
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
643
941
|
}␊
|
|
644
|
-
if (__isThenable(
|
|
645
|
-
return Promise.resolve(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
cleanupErrors.push(cleanupError)␊
|
|
649
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
650
|
-
},␊
|
|
942
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
943
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
944
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
945
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
651
946
|
)␊
|
|
652
947
|
}␊
|
|
653
|
-
|
|
654
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
655
|
-
}␊
|
|
656
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
948
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
657
949
|
}␊
|
|
658
950
|
␊
|
|
659
951
|
let __wasiModule␊
|
|
@@ -675,7 +967,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
675
967
|
context: __emnapiContext,␊
|
|
676
968
|
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
677
969
|
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
678
|
-
plugins: [
|
|
970
|
+
plugins: [␊
|
|
971
|
+
__captureWasiThreadManager,␊
|
|
972
|
+
__emnapiAsyncWorkPlugin,␊
|
|
973
|
+
__emnapiTSFNPlugin,␊
|
|
974
|
+
],␊
|
|
679
975
|
wasi: __wasi,␊
|
|
680
976
|
onCreateWorker() {␊
|
|
681
977
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {␊
|
|
@@ -814,6 +1110,35 @@ Generated by [AVA](https://avajs.dev).
|
|
|
814
1110
|
␊
|
|
815
1111
|
const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose')␊
|
|
816
1112
|
const __wasiWorkers = new Set()␊
|
|
1113
|
+
// The thread manager has to be reachable *before* anything that can throw␊
|
|
1114
|
+
// during load or registration. Initialization can fail after the pool has␊
|
|
1115
|
+
// already spawned workers, and the rollback still has to mark their␊
|
|
1116
|
+
// terminations as expected — but \`__napiModule\` is assigned only when␊
|
|
1117
|
+
// instantiation RETURNS, so on exactly that path it is still undefined. A␊
|
|
1118
|
+
// plugin factory runs while the emnapi module is being created, before the␊
|
|
1119
|
+
// wasm is loaded and before any registration function runs, and its context␊
|
|
1120
|
+
// carries the very same manager instance.␊
|
|
1121
|
+
let __wasiThreadManager␊
|
|
1122
|
+
␊
|
|
1123
|
+
function __captureWasiThreadManager(context) {␊
|
|
1124
|
+
if (context && context.PThread) {␊
|
|
1125
|
+
__wasiThreadManager = context.PThread␊
|
|
1126
|
+
}␊
|
|
1127
|
+
return {}␊
|
|
1128
|
+
}␊
|
|
1129
|
+
␊
|
|
1130
|
+
function __getWasiThreadManager() {␊
|
|
1131
|
+
const manager =␊
|
|
1132
|
+
__wasiThreadManager !== undefined␊
|
|
1133
|
+
? __wasiThreadManager␊
|
|
1134
|
+
: __napiModule␊
|
|
1135
|
+
? __napiModule.PThread␊
|
|
1136
|
+
: undefined␊
|
|
1137
|
+
if (manager && typeof manager.terminateWorker === 'function') {␊
|
|
1138
|
+
return manager␊
|
|
1139
|
+
}␊
|
|
1140
|
+
return undefined␊
|
|
1141
|
+
}␊
|
|
817
1142
|
let __napiInstance␊
|
|
818
1143
|
let __emnapiContextDestroyed = false␊
|
|
819
1144
|
let __emnapiContextDestroyPromise␊
|
|
@@ -823,6 +1148,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
823
1148
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
824
1149
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
825
1150
|
let __wasiDisposed = false␊
|
|
1151
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
826
1152
|
let __wasiDisposePromise␊
|
|
827
1153
|
let __completeWasiDisposal = function () {}␊
|
|
828
1154
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -980,6 +1306,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
980
1306
|
}␊
|
|
981
1307
|
})()␊
|
|
982
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
|
+
␊
|
|
983
1327
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
984
1328
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
985
1329
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -1136,13 +1480,209 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1136
1480
|
return destroyPromise␊
|
|
1137
1481
|
}␊
|
|
1138
1482
|
␊
|
|
1483
|
+
/**␊
|
|
1484
|
+
* Holds the event loop open until \`work\` settles.␊
|
|
1485
|
+
*␊
|
|
1486
|
+
* Nothing else can: the pool workers are deliberately unreferenced so an idle␊
|
|
1487
|
+
* binding cannot keep a process alive, and referencing them again for the␊
|
|
1488
|
+
* termination does not hold either — emnapi unreferences a worker the moment it␊
|
|
1489
|
+
* reports \`async-thread-ready\`, which for a worker that was still starting␊
|
|
1490
|
+
* lands *after* the termination began. Without a handle of its own, an␊
|
|
1491
|
+
* \`await dispose()\` with nothing else pending exits the process with its␊
|
|
1492
|
+
* promise unsettled, and everything after the \`await\` is skipped.␊
|
|
1493
|
+
*␊
|
|
1494
|
+
* The timer is cleared as soon as the work settles, so this never outlives the␊
|
|
1495
|
+
* disposal that asked for it.␊
|
|
1496
|
+
*/␊
|
|
1497
|
+
function __keepEventLoopAliveUntil(work) {␊
|
|
1498
|
+
const setTimer = globalThis.setInterval␊
|
|
1499
|
+
const clearTimer = globalThis.clearInterval␊
|
|
1500
|
+
if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') {␊
|
|
1501
|
+
return work␊
|
|
1502
|
+
}␊
|
|
1503
|
+
let timer␊
|
|
1504
|
+
try {␊
|
|
1505
|
+
timer = setTimer(function () {}, 50)␊
|
|
1506
|
+
} catch {␊
|
|
1507
|
+
return work␊
|
|
1508
|
+
}␊
|
|
1509
|
+
const release = function () {␊
|
|
1510
|
+
try {␊
|
|
1511
|
+
clearTimer(timer)␊
|
|
1512
|
+
} catch {}␊
|
|
1513
|
+
}␊
|
|
1514
|
+
return work.then(␊
|
|
1515
|
+
(value) => {␊
|
|
1516
|
+
release()␊
|
|
1517
|
+
return value␊
|
|
1518
|
+
},␊
|
|
1519
|
+
(error) => {␊
|
|
1520
|
+
release()␊
|
|
1521
|
+
throw error␊
|
|
1522
|
+
},␊
|
|
1523
|
+
)␊
|
|
1524
|
+
}␊
|
|
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
|
+
␊
|
|
1655
|
+
/**␊
|
|
1656
|
+
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
1657
|
+
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
1658
|
+
* the manager's \`exit\` listener instead, which reports␊
|
|
1659
|
+
* \`worker (tid = N) sent an error! ... stopped with exit code 1\` and rethrows␊
|
|
1660
|
+
* inside the emit — aborting the \`once('exit')\` that backs the terminate␊
|
|
1661
|
+
* promise, so disposal never settles and the process dies with an uncaught␊
|
|
1662
|
+
* exception. Mark the termination through the manager first.␊
|
|
1663
|
+
*␊
|
|
1664
|
+
* The manager comes from \`__getWasiThreadManager\`, not from \`__napiModule\`:␊
|
|
1665
|
+
* the initialization rollback runs on the one path where instantiation never␊
|
|
1666
|
+
* returned, so \`__napiModule\` is still undefined there while the workers it␊
|
|
1667
|
+
* spawned are already registered and loaded.␊
|
|
1668
|
+
*␊
|
|
1669
|
+
* Not \`terminateAllThreads()\`: that one recreates the pool it just shut down.␊
|
|
1670
|
+
*/␊
|
|
1139
1671
|
function __terminateWasiWorkers() {␊
|
|
1140
1672
|
const cleanupErrors = []␊
|
|
1141
1673
|
const pending = []␊
|
|
1674
|
+
const threadManager = __getWasiThreadManager()␊
|
|
1142
1675
|
␊
|
|
1143
1676
|
for (const worker of __wasiWorkers) {␊
|
|
1144
1677
|
let result␊
|
|
1145
1678
|
try {␊
|
|
1679
|
+
if (threadManager) {␊
|
|
1680
|
+
threadManager.terminateWorker(worker)␊
|
|
1681
|
+
// \`terminateWorker\` leaves behind a reporter that logs every message␊
|
|
1682
|
+
// still queued on the port, which Node flushes on exit. Nothing is␊
|
|
1683
|
+
// listening for those any more.␊
|
|
1684
|
+
worker.onmessage = undefined␊
|
|
1685
|
+
}␊
|
|
1146
1686
|
result = worker.terminate()␊
|
|
1147
1687
|
} catch (error) {␊
|
|
1148
1688
|
cleanupErrors.push(error)␊
|
|
@@ -1172,7 +1712,9 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1172
1712
|
)␊
|
|
1173
1713
|
}␊
|
|
1174
1714
|
}␊
|
|
1175
|
-
return pending.length > 0
|
|
1715
|
+
return pending.length > 0␊
|
|
1716
|
+
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)␊
|
|
1717
|
+
: finish()␊
|
|
1176
1718
|
}␊
|
|
1177
1719
|
␊
|
|
1178
1720
|
function __finishWasiDisposal() {␊
|
|
@@ -1191,7 +1733,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1191
1733
|
return __finishWasiDisposal()␊
|
|
1192
1734
|
}␊
|
|
1193
1735
|
␊
|
|
1194
|
-
function
|
|
1736
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
1195
1737
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
1196
1738
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
1197
1739
|
// back to back is what strands them.␊
|
|
@@ -1203,6 +1745,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1203
1745
|
return __continueWasiDisposal()␊
|
|
1204
1746
|
}␊
|
|
1205
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
|
+
␊
|
|
1206
1763
|
/**␊
|
|
1207
1764
|
* Disposes this generated WASI binding.␊
|
|
1208
1765
|
*␊
|
|
@@ -1344,29 +1901,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1344
1901
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
1345
1902
|
*/␊
|
|
1346
1903
|
function __rollbackWasiInitialization() {␊
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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␊
|
|
1350
1946
|
try {␊
|
|
1351
|
-
|
|
1352
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
1947
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
1353
1948
|
} catch (cleanupError) {␊
|
|
1354
|
-
|
|
1355
|
-
settlementsUnreached = true␊
|
|
1949
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
1356
1950
|
}␊
|
|
1357
|
-
if (__isThenable(
|
|
1358
|
-
return Promise.resolve(
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
cleanupErrors.push(cleanupError)␊
|
|
1362
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
1363
|
-
},␊
|
|
1951
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
1952
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
1953
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
1954
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
1364
1955
|
)␊
|
|
1365
1956
|
}␊
|
|
1366
|
-
|
|
1367
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
1368
|
-
}␊
|
|
1369
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
1957
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
1370
1958
|
}␊
|
|
1371
1959
|
␊
|
|
1372
1960
|
let __wasiModule␊
|
|
@@ -1388,7 +1976,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1388
1976
|
context: __emnapiContext,␊
|
|
1389
1977
|
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
1390
1978
|
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
1391
|
-
plugins: [
|
|
1979
|
+
plugins: [␊
|
|
1980
|
+
__captureWasiThreadManager,␊
|
|
1981
|
+
__emnapiAsyncWorkPlugin,␊
|
|
1982
|
+
__emnapiTSFNPlugin,␊
|
|
1983
|
+
],␊
|
|
1392
1984
|
wasi: __wasi,␊
|
|
1393
1985
|
onCreateWorker() {␊
|
|
1394
1986
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {␊
|
|
@@ -1546,6 +2138,35 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1546
2138
|
␊
|
|
1547
2139
|
const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose')␊
|
|
1548
2140
|
const __wasiWorkers = new Set()␊
|
|
2141
|
+
// The thread manager has to be reachable *before* anything that can throw␊
|
|
2142
|
+
// during load or registration. Initialization can fail after the pool has␊
|
|
2143
|
+
// already spawned workers, and the rollback still has to mark their␊
|
|
2144
|
+
// terminations as expected — but \`__napiModule\` is assigned only when␊
|
|
2145
|
+
// instantiation RETURNS, so on exactly that path it is still undefined. A␊
|
|
2146
|
+
// plugin factory runs while the emnapi module is being created, before the␊
|
|
2147
|
+
// wasm is loaded and before any registration function runs, and its context␊
|
|
2148
|
+
// carries the very same manager instance.␊
|
|
2149
|
+
let __wasiThreadManager␊
|
|
2150
|
+
␊
|
|
2151
|
+
function __captureWasiThreadManager(context) {␊
|
|
2152
|
+
if (context && context.PThread) {␊
|
|
2153
|
+
__wasiThreadManager = context.PThread␊
|
|
2154
|
+
}␊
|
|
2155
|
+
return {}␊
|
|
2156
|
+
}␊
|
|
2157
|
+
␊
|
|
2158
|
+
function __getWasiThreadManager() {␊
|
|
2159
|
+
const manager =␊
|
|
2160
|
+
__wasiThreadManager !== undefined␊
|
|
2161
|
+
? __wasiThreadManager␊
|
|
2162
|
+
: __napiModule␊
|
|
2163
|
+
? __napiModule.PThread␊
|
|
2164
|
+
: undefined␊
|
|
2165
|
+
if (manager && typeof manager.terminateWorker === 'function') {␊
|
|
2166
|
+
return manager␊
|
|
2167
|
+
}␊
|
|
2168
|
+
return undefined␊
|
|
2169
|
+
}␊
|
|
1549
2170
|
let __napiInstance␊
|
|
1550
2171
|
let __emnapiContextDestroyed = false␊
|
|
1551
2172
|
let __emnapiContextDestroyPromise␊
|
|
@@ -1555,6 +2176,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1555
2176
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
1556
2177
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
1557
2178
|
let __wasiDisposed = false␊
|
|
2179
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
1558
2180
|
let __wasiDisposePromise␊
|
|
1559
2181
|
let __completeWasiDisposal = function () {}␊
|
|
1560
2182
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -1712,6 +2334,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1712
2334
|
}␊
|
|
1713
2335
|
})()␊
|
|
1714
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
|
+
␊
|
|
1715
2355
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
1716
2356
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
1717
2357
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -1868,13 +2508,209 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1868
2508
|
return destroyPromise␊
|
|
1869
2509
|
}␊
|
|
1870
2510
|
␊
|
|
2511
|
+
/**␊
|
|
2512
|
+
* Holds the event loop open until \`work\` settles.␊
|
|
2513
|
+
*␊
|
|
2514
|
+
* Nothing else can: the pool workers are deliberately unreferenced so an idle␊
|
|
2515
|
+
* binding cannot keep a process alive, and referencing them again for the␊
|
|
2516
|
+
* termination does not hold either — emnapi unreferences a worker the moment it␊
|
|
2517
|
+
* reports \`async-thread-ready\`, which for a worker that was still starting␊
|
|
2518
|
+
* lands *after* the termination began. Without a handle of its own, an␊
|
|
2519
|
+
* \`await dispose()\` with nothing else pending exits the process with its␊
|
|
2520
|
+
* promise unsettled, and everything after the \`await\` is skipped.␊
|
|
2521
|
+
*␊
|
|
2522
|
+
* The timer is cleared as soon as the work settles, so this never outlives the␊
|
|
2523
|
+
* disposal that asked for it.␊
|
|
2524
|
+
*/␊
|
|
2525
|
+
function __keepEventLoopAliveUntil(work) {␊
|
|
2526
|
+
const setTimer = globalThis.setInterval␊
|
|
2527
|
+
const clearTimer = globalThis.clearInterval␊
|
|
2528
|
+
if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') {␊
|
|
2529
|
+
return work␊
|
|
2530
|
+
}␊
|
|
2531
|
+
let timer␊
|
|
2532
|
+
try {␊
|
|
2533
|
+
timer = setTimer(function () {}, 50)␊
|
|
2534
|
+
} catch {␊
|
|
2535
|
+
return work␊
|
|
2536
|
+
}␊
|
|
2537
|
+
const release = function () {␊
|
|
2538
|
+
try {␊
|
|
2539
|
+
clearTimer(timer)␊
|
|
2540
|
+
} catch {}␊
|
|
2541
|
+
}␊
|
|
2542
|
+
return work.then(␊
|
|
2543
|
+
(value) => {␊
|
|
2544
|
+
release()␊
|
|
2545
|
+
return value␊
|
|
2546
|
+
},␊
|
|
2547
|
+
(error) => {␊
|
|
2548
|
+
release()␊
|
|
2549
|
+
throw error␊
|
|
2550
|
+
},␊
|
|
2551
|
+
)␊
|
|
2552
|
+
}␊
|
|
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
|
+
␊
|
|
2683
|
+
/**␊
|
|
2684
|
+
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
2685
|
+
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
2686
|
+
* the manager's \`exit\` listener instead, which reports␊
|
|
2687
|
+
* \`worker (tid = N) sent an error! ... stopped with exit code 1\` and rethrows␊
|
|
2688
|
+
* inside the emit — aborting the \`once('exit')\` that backs the terminate␊
|
|
2689
|
+
* promise, so disposal never settles and the process dies with an uncaught␊
|
|
2690
|
+
* exception. Mark the termination through the manager first.␊
|
|
2691
|
+
*␊
|
|
2692
|
+
* The manager comes from \`__getWasiThreadManager\`, not from \`__napiModule\`:␊
|
|
2693
|
+
* the initialization rollback runs on the one path where instantiation never␊
|
|
2694
|
+
* returned, so \`__napiModule\` is still undefined there while the workers it␊
|
|
2695
|
+
* spawned are already registered and loaded.␊
|
|
2696
|
+
*␊
|
|
2697
|
+
* Not \`terminateAllThreads()\`: that one recreates the pool it just shut down.␊
|
|
2698
|
+
*/␊
|
|
1871
2699
|
function __terminateWasiWorkers() {␊
|
|
1872
2700
|
const cleanupErrors = []␊
|
|
1873
2701
|
const pending = []␊
|
|
2702
|
+
const threadManager = __getWasiThreadManager()␊
|
|
1874
2703
|
␊
|
|
1875
2704
|
for (const worker of __wasiWorkers) {␊
|
|
1876
2705
|
let result␊
|
|
1877
2706
|
try {␊
|
|
2707
|
+
if (threadManager) {␊
|
|
2708
|
+
threadManager.terminateWorker(worker)␊
|
|
2709
|
+
// \`terminateWorker\` leaves behind a reporter that logs every message␊
|
|
2710
|
+
// still queued on the port, which Node flushes on exit. Nothing is␊
|
|
2711
|
+
// listening for those any more.␊
|
|
2712
|
+
worker.onmessage = undefined␊
|
|
2713
|
+
}␊
|
|
1878
2714
|
result = worker.terminate()␊
|
|
1879
2715
|
} catch (error) {␊
|
|
1880
2716
|
cleanupErrors.push(error)␊
|
|
@@ -1904,7 +2740,9 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1904
2740
|
)␊
|
|
1905
2741
|
}␊
|
|
1906
2742
|
}␊
|
|
1907
|
-
return pending.length > 0
|
|
2743
|
+
return pending.length > 0␊
|
|
2744
|
+
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)␊
|
|
2745
|
+
: finish()␊
|
|
1908
2746
|
}␊
|
|
1909
2747
|
␊
|
|
1910
2748
|
function __finishWasiDisposal() {␊
|
|
@@ -1923,7 +2761,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1923
2761
|
return __finishWasiDisposal()␊
|
|
1924
2762
|
}␊
|
|
1925
2763
|
␊
|
|
1926
|
-
function
|
|
2764
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
1927
2765
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
1928
2766
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
1929
2767
|
// back to back is what strands them.␊
|
|
@@ -1935,6 +2773,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1935
2773
|
return __continueWasiDisposal()␊
|
|
1936
2774
|
}␊
|
|
1937
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
|
+
␊
|
|
1938
2791
|
/**␊
|
|
1939
2792
|
* Disposes this generated WASI binding.␊
|
|
1940
2793
|
*␊
|
|
@@ -2076,29 +2929,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2076
2929
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
2077
2930
|
*/␊
|
|
2078
2931
|
function __rollbackWasiInitialization() {␊
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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␊
|
|
2082
2974
|
try {␊
|
|
2083
|
-
|
|
2084
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
2975
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
2085
2976
|
} catch (cleanupError) {␊
|
|
2086
|
-
|
|
2087
|
-
settlementsUnreached = true␊
|
|
2977
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
2088
2978
|
}␊
|
|
2089
|
-
if (__isThenable(
|
|
2090
|
-
return Promise.resolve(
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
cleanupErrors.push(cleanupError)␊
|
|
2094
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2095
|
-
},␊
|
|
2979
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
2980
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
2981
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
2982
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
2096
2983
|
)␊
|
|
2097
2984
|
}␊
|
|
2098
|
-
|
|
2099
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2100
|
-
}␊
|
|
2101
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
2985
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
2102
2986
|
}␊
|
|
2103
2987
|
␊
|
|
2104
2988
|
let __wasiModule␊
|
|
@@ -2120,7 +3004,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2120
3004
|
context: __emnapiContext,␊
|
|
2121
3005
|
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
2122
3006
|
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
2123
|
-
plugins: [
|
|
3007
|
+
plugins: [␊
|
|
3008
|
+
__captureWasiThreadManager,␊
|
|
3009
|
+
__emnapiAsyncWorkPlugin,␊
|
|
3010
|
+
__emnapiTSFNPlugin,␊
|
|
3011
|
+
],␊
|
|
2124
3012
|
wasi: __wasi,␊
|
|
2125
3013
|
onCreateWorker() {␊
|
|
2126
3014
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {␊
|
|
@@ -2266,6 +3154,35 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2266
3154
|
␊
|
|
2267
3155
|
const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose')␊
|
|
2268
3156
|
const __wasiWorkers = new Set()␊
|
|
3157
|
+
// The thread manager has to be reachable *before* anything that can throw␊
|
|
3158
|
+
// during load or registration. Initialization can fail after the pool has␊
|
|
3159
|
+
// already spawned workers, and the rollback still has to mark their␊
|
|
3160
|
+
// terminations as expected — but \`__napiModule\` is assigned only when␊
|
|
3161
|
+
// instantiation RETURNS, so on exactly that path it is still undefined. A␊
|
|
3162
|
+
// plugin factory runs while the emnapi module is being created, before the␊
|
|
3163
|
+
// wasm is loaded and before any registration function runs, and its context␊
|
|
3164
|
+
// carries the very same manager instance.␊
|
|
3165
|
+
let __wasiThreadManager␊
|
|
3166
|
+
␊
|
|
3167
|
+
function __captureWasiThreadManager(context) {␊
|
|
3168
|
+
if (context && context.PThread) {␊
|
|
3169
|
+
__wasiThreadManager = context.PThread␊
|
|
3170
|
+
}␊
|
|
3171
|
+
return {}␊
|
|
3172
|
+
}␊
|
|
3173
|
+
␊
|
|
3174
|
+
function __getWasiThreadManager() {␊
|
|
3175
|
+
const manager =␊
|
|
3176
|
+
__wasiThreadManager !== undefined␊
|
|
3177
|
+
? __wasiThreadManager␊
|
|
3178
|
+
: __napiModule␊
|
|
3179
|
+
? __napiModule.PThread␊
|
|
3180
|
+
: undefined␊
|
|
3181
|
+
if (manager && typeof manager.terminateWorker === 'function') {␊
|
|
3182
|
+
return manager␊
|
|
3183
|
+
}␊
|
|
3184
|
+
return undefined␊
|
|
3185
|
+
}␊
|
|
2269
3186
|
let __napiInstance␊
|
|
2270
3187
|
let __emnapiContextDestroyed = false␊
|
|
2271
3188
|
let __emnapiContextDestroyPromise␊
|
|
@@ -2275,6 +3192,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2275
3192
|
let __emnapiWasmEnvCleanupDrained = false␊
|
|
2276
3193
|
let __emnapiWasmEnvCleanupDrainPromise␊
|
|
2277
3194
|
let __wasiDisposed = false␊
|
|
3195
|
+
let __wasiAsyncWorkDrainPromise␊
|
|
2278
3196
|
let __wasiDisposePromise␊
|
|
2279
3197
|
let __completeWasiDisposal = function () {}␊
|
|
2280
3198
|
// Overridden by loader flavors that have a last-resort reclaim for a rollback␊
|
|
@@ -2462,6 +3380,24 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2462
3380
|
}␊
|
|
2463
3381
|
})()␊
|
|
2464
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
|
+
␊
|
|
2465
3401
|
// Turns to wait for while the addon still reports queued settlements. Reaching␊
|
|
2466
3402
|
// zero is the only success. A counter still nonzero at this bound rejects the␊
|
|
2467
3403
|
// disposal as retryable (\`ERR_NAPI_WASI_CLEANUP_PENDING\`) rather than␊
|
|
@@ -2619,13 +3555,209 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2619
3555
|
return destroyPromise␊
|
|
2620
3556
|
}␊
|
|
2621
3557
|
␊
|
|
3558
|
+
/**␊
|
|
3559
|
+
* Holds the event loop open until \`work\` settles.␊
|
|
3560
|
+
*␊
|
|
3561
|
+
* Nothing else can: the pool workers are deliberately unreferenced so an idle␊
|
|
3562
|
+
* binding cannot keep a process alive, and referencing them again for the␊
|
|
3563
|
+
* termination does not hold either — emnapi unreferences a worker the moment it␊
|
|
3564
|
+
* reports \`async-thread-ready\`, which for a worker that was still starting␊
|
|
3565
|
+
* lands *after* the termination began. Without a handle of its own, an␊
|
|
3566
|
+
* \`await dispose()\` with nothing else pending exits the process with its␊
|
|
3567
|
+
* promise unsettled, and everything after the \`await\` is skipped.␊
|
|
3568
|
+
*␊
|
|
3569
|
+
* The timer is cleared as soon as the work settles, so this never outlives the␊
|
|
3570
|
+
* disposal that asked for it.␊
|
|
3571
|
+
*/␊
|
|
3572
|
+
function __keepEventLoopAliveUntil(work) {␊
|
|
3573
|
+
const setTimer = globalThis.setInterval␊
|
|
3574
|
+
const clearTimer = globalThis.clearInterval␊
|
|
3575
|
+
if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') {␊
|
|
3576
|
+
return work␊
|
|
3577
|
+
}␊
|
|
3578
|
+
let timer␊
|
|
3579
|
+
try {␊
|
|
3580
|
+
timer = setTimer(function () {}, 50)␊
|
|
3581
|
+
} catch {␊
|
|
3582
|
+
return work␊
|
|
3583
|
+
}␊
|
|
3584
|
+
const release = function () {␊
|
|
3585
|
+
try {␊
|
|
3586
|
+
clearTimer(timer)␊
|
|
3587
|
+
} catch {}␊
|
|
3588
|
+
}␊
|
|
3589
|
+
return work.then(␊
|
|
3590
|
+
(value) => {␊
|
|
3591
|
+
release()␊
|
|
3592
|
+
return value␊
|
|
3593
|
+
},␊
|
|
3594
|
+
(error) => {␊
|
|
3595
|
+
release()␊
|
|
3596
|
+
throw error␊
|
|
3597
|
+
},␊
|
|
3598
|
+
)␊
|
|
3599
|
+
}␊
|
|
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
|
+
␊
|
|
3730
|
+
/**␊
|
|
3731
|
+
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own␊
|
|
3732
|
+
* thread manager performed the termination. A bare \`worker.terminate()\` reaches␊
|
|
3733
|
+
* the manager's \`exit\` listener instead, which reports␊
|
|
3734
|
+
* \`worker (tid = N) sent an error! ... stopped with exit code 1\` and rethrows␊
|
|
3735
|
+
* inside the emit — aborting the \`once('exit')\` that backs the terminate␊
|
|
3736
|
+
* promise, so disposal never settles and the process dies with an uncaught␊
|
|
3737
|
+
* exception. Mark the termination through the manager first.␊
|
|
3738
|
+
*␊
|
|
3739
|
+
* The manager comes from \`__getWasiThreadManager\`, not from \`__napiModule\`:␊
|
|
3740
|
+
* the initialization rollback runs on the one path where instantiation never␊
|
|
3741
|
+
* returned, so \`__napiModule\` is still undefined there while the workers it␊
|
|
3742
|
+
* spawned are already registered and loaded.␊
|
|
3743
|
+
*␊
|
|
3744
|
+
* Not \`terminateAllThreads()\`: that one recreates the pool it just shut down.␊
|
|
3745
|
+
*/␊
|
|
2622
3746
|
function __terminateWasiWorkers() {␊
|
|
2623
3747
|
const cleanupErrors = []␊
|
|
2624
3748
|
const pending = []␊
|
|
3749
|
+
const threadManager = __getWasiThreadManager()␊
|
|
2625
3750
|
␊
|
|
2626
3751
|
for (const worker of __wasiWorkers) {␊
|
|
2627
3752
|
let result␊
|
|
2628
3753
|
try {␊
|
|
3754
|
+
if (threadManager) {␊
|
|
3755
|
+
threadManager.terminateWorker(worker)␊
|
|
3756
|
+
// \`terminateWorker\` leaves behind a reporter that logs every message␊
|
|
3757
|
+
// still queued on the port, which Node flushes on exit. Nothing is␊
|
|
3758
|
+
// listening for those any more.␊
|
|
3759
|
+
worker.onmessage = undefined␊
|
|
3760
|
+
}␊
|
|
2629
3761
|
result = worker.terminate()␊
|
|
2630
3762
|
} catch (error) {␊
|
|
2631
3763
|
cleanupErrors.push(error)␊
|
|
@@ -2655,7 +3787,9 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2655
3787
|
)␊
|
|
2656
3788
|
}␊
|
|
2657
3789
|
}␊
|
|
2658
|
-
return pending.length > 0
|
|
3790
|
+
return pending.length > 0␊
|
|
3791
|
+
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)␊
|
|
3792
|
+
: finish()␊
|
|
2659
3793
|
}␊
|
|
2660
3794
|
␊
|
|
2661
3795
|
function __finishWasiDisposal() {␊
|
|
@@ -2674,7 +3808,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2674
3808
|
return __finishWasiDisposal()␊
|
|
2675
3809
|
}␊
|
|
2676
3810
|
␊
|
|
2677
|
-
function
|
|
3811
|
+
function __cleanUpWasmEnvForWasiDisposal() {␊
|
|
2678
3812
|
// Run the pre-teardown barrier, then let the settlements it queued actually␊
|
|
2679
3813
|
// reach JavaScript, and only then destroy the environment. Doing these two␊
|
|
2680
3814
|
// back to back is what strands them.␊
|
|
@@ -2686,6 +3820,21 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2686
3820
|
return __continueWasiDisposal()␊
|
|
2687
3821
|
}␊
|
|
2688
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
|
+
␊
|
|
2689
3838
|
/**␊
|
|
2690
3839
|
* Disposes this generated WASI binding.␊
|
|
2691
3840
|
*␊
|
|
@@ -2827,29 +3976,60 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2827
3976
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.␊
|
|
2828
3977
|
*/␊
|
|
2829
3978
|
function __rollbackWasiInitialization() {␊
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
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␊
|
|
2833
4021
|
try {␊
|
|
2834
|
-
|
|
2835
|
-
drainResult = __drainWasmEnvCleanup()␊
|
|
4022
|
+
asyncWorkResult = __drainWasiAsyncWork()␊
|
|
2836
4023
|
} catch (cleanupError) {␊
|
|
2837
|
-
|
|
2838
|
-
settlementsUnreached = true␊
|
|
4024
|
+
return __retainAfterAsyncWorkDrainFailure(cleanupError)␊
|
|
2839
4025
|
}␊
|
|
2840
|
-
if (__isThenable(
|
|
2841
|
-
return Promise.resolve(
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
cleanupErrors.push(cleanupError)␊
|
|
2845
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2846
|
-
},␊
|
|
4026
|
+
if (__isThenable(asyncWorkResult)) {␊
|
|
4027
|
+
return Promise.resolve(asyncWorkResult).then(␊
|
|
4028
|
+
__rollbackWasmEnvForWasiInitialization,␊
|
|
4029
|
+
__retainAfterAsyncWorkDrainFailure,␊
|
|
2847
4030
|
)␊
|
|
2848
4031
|
}␊
|
|
2849
|
-
|
|
2850
|
-
return __retainFailedWasiRollback(cleanupErrors)␊
|
|
2851
|
-
}␊
|
|
2852
|
-
return __destroyContextForWasiRollback(cleanupErrors)␊
|
|
4032
|
+
return __rollbackWasmEnvForWasiInitialization()␊
|
|
2853
4033
|
}␊
|
|
2854
4034
|
␊
|
|
2855
4035
|
let __wasiModule␊
|
|
@@ -2870,7 +4050,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
2870
4050
|
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {␊
|
|
2871
4051
|
context: __emnapiContext,␊
|
|
2872
4052
|
asyncWorkPoolSize: 0,␊
|
|
2873
|
-
plugins: [
|
|
4053
|
+
plugins: [␊
|
|
4054
|
+
__captureWasiThreadManager,␊
|
|
4055
|
+
__emnapiAsyncWorkPlugin,␊
|
|
4056
|
+
__emnapiTSFNPlugin,␊
|
|
4057
|
+
],␊
|
|
2874
4058
|
wasi: __wasi,␊
|
|
2875
4059
|
overwriteImports(importObject) {␊
|
|
2876
4060
|
importObject.env = {␊
|
|
@@ -3383,6 +4567,101 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3383
4567
|
})()␊
|
|
3384
4568
|
}␊
|
|
3385
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
|
+
␊
|
|
3386
4665
|
function __createLifecycleReentryError(__operation) {␊
|
|
3387
4666
|
const __error = new Error(␊
|
|
3388
4667
|
__operation +␊
|
|
@@ -3877,7 +5156,15 @@ Generated by [AVA](https://avajs.dev).
|
|
|
3877
5156
|
if (__lifecycleState !== 'failed') {␊
|
|
3878
5157
|
__lifecycleState = 'disposal'␊
|
|
3879
5158
|
}␊
|
|
3880
|
-
//
|
|
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␊
|
|
3881
5168
|
// accepting JavaScript calls. Undefined unless something is queued, so␊
|
|
3882
5169
|
// an idle disposal is not delayed by a single turn.␊
|
|
3883
5170
|
const __drained = __prepareForDisposal()␊
|
|
@@ -4032,6 +5319,14 @@ Generated by [AVA](https://avajs.dev).
|
|
|
4032
5319
|
// so a failure before beforeInit costs no extra turn.␊
|
|
4033
5320
|
let __settlementsUnreached = false␊
|
|
4034
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
|
+
}␊
|
|
4035
5330
|
const __drained = __prepareForDisposal()␊
|
|
4036
5331
|
if (__drained) {␊
|
|
4037
5332
|
await __drained␊
|
|
@@ -4770,6 +6065,101 @@ Generated by [AVA](https://avajs.dev).
|
|
|
4770
6065
|
})()␊
|
|
4771
6066
|
}␊
|
|
4772
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
|
+
␊
|
|
4773
6163
|
function __createLifecycleReentryError(__operation) {␊
|
|
4774
6164
|
const __error = new Error(␊
|
|
4775
6165
|
__operation +␊
|
|
@@ -5285,7 +6675,15 @@ Generated by [AVA](https://avajs.dev).
|
|
|
5285
6675
|
if (__lifecycleState !== 'failed') {␊
|
|
5286
6676
|
__lifecycleState = 'disposal'␊
|
|
5287
6677
|
}␊
|
|
5288
|
-
//
|
|
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␊
|
|
5289
6687
|
// accepting JavaScript calls. Undefined unless something is queued, so␊
|
|
5290
6688
|
// an idle disposal is not delayed by a single turn.␊
|
|
5291
6689
|
const __drained = __prepareForDisposal()␊
|
|
@@ -5469,6 +6867,14 @@ Generated by [AVA](https://avajs.dev).
|
|
|
5469
6867
|
// so a failure before beforeInit costs no extra turn.␊
|
|
5470
6868
|
let __settlementsUnreached = false␊
|
|
5471
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
|
+
}␊
|
|
5472
6878
|
const __drained = __prepareForDisposal()␊
|
|
5473
6879
|
if (__drained) {␊
|
|
5474
6880
|
await __drained␊
|