@napi-rs/cli 3.10.1 → 3.10.3

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