From c0871b3e57baeaec2d36c5bdf71976d6ef2483fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Tue, 21 Apr 2026 14:49:21 +0900 Subject: [PATCH 1/2] [Fix] AddDisposableResource: use hyphen in `SYNC-DISPOSE` hint check Step 1.a of the spec short-circuits null/undefined `V` for sync-dispose, returning `UNUSED` without pushing a resource. The hint constant elsewhere is `SYNC-DISPOSE` (hyphen), but the guard here read `SYNC_DISPOSE` (underscore), so the branch was never taken and `use(null)` / `use()` on a sync `DisposableStack` pushed a method-less sync resource onto the stack. Co-Authored-By: Claude Opus 4.7 (1M context) --- aos/AddDisposableResource.js | 2 +- test/tests.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/aos/AddDisposableResource.js b/aos/AddDisposableResource.js index 33919f8..541f326 100644 --- a/aos/AddDisposableResource.js +++ b/aos/AddDisposableResource.js @@ -29,7 +29,7 @@ module.exports = function AddDisposableResource(disposeCapability, V, hint) { var resource; if (arguments.length < 4) { // step 1 - if (V == null && hint === 'SYNC_DISPOSE') { + if (V == null && hint === 'SYNC-DISPOSE') { return 'UNUSED'; // step 1.a } resource = CreateDisposableResource(V, hint); // step 1.c diff --git a/test/tests.js b/test/tests.js index a4dce47..6c9dc79 100644 --- a/test/tests.js +++ b/test/tests.js @@ -13,6 +13,7 @@ var semver = require('semver'); var gOPD = require('gopd'); var defineAccessorProperty = require('define-accessor-property'); var SuppressedError = require('suppressed-error/polyfill')(); +var SLOT = require('internal-slot'); var brokenNodePolyfill = semver.satisfies(process.version, '^18.18 || >= 20.4'); @@ -99,6 +100,12 @@ module.exports = { instance.use(); instance.use(disposable); + // AddDisposableResource step 1.a: `use(null)` and `use(undefined)` on a sync stack must return without pushing a resource. + if (SLOT.has(instance, '[[DisposeCapability]]')) { + var cap = SLOT.get(instance, '[[DisposeCapability]]'); + st.equal(cap['[[DisposableResourceStack]]'].length, 2, '`use(null)` and `use(undefined)` do not add a resource'); + } + forEach(v.nonNullPrimitives, function (nonNullishObject) { st['throws']( function () { instance.use(nonNullishObject); }, From 6982b470d7e338f61338bb5e4e3a103c2896fa26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Mon, 20 Apr 2026 09:57:02 +0900 Subject: [PATCH 2/2] [Fix] AsyncDisposableStack: continue disposal chain after a throwing defer Fixes https://github.com/es-shims/DisposableStack/issues/9 Co-Authored-By: Claude Opus 4.7 (1M context) --- aos/DisposeResources.js | 29 +++++++++++++++++++++-------- test/tests.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/aos/DisposeResources.js b/aos/DisposeResources.js index 1a1cbf8..9a342cb 100644 --- a/aos/DisposeResources.js +++ b/aos/DisposeResources.js @@ -11,7 +11,6 @@ var $then = callBound('Promise.prototype.then', true); var CompletionRecord = require('es-abstract/2025/CompletionRecord'); var Dispose = require('./Dispose'); -var NormalCompletion = require('es-abstract/2025/NormalCompletion'); var PromiseResolve = require('es-abstract/2025/PromiseResolve'); var ThrowCompletion = require('es-abstract/2025/ThrowCompletion'); @@ -56,9 +55,8 @@ module.exports = function DisposeResources(disposeCapability, completion) { }; var getPromise = actualHint === 'ASYNC-DISPOSE' && function getPromise(resource) { - return $then( - promise, - function () { + var runDispose = function () { + try { var result = Dispose( // step 2.a resource['[[ResourceValue]]'], resource['[[Hint]]'], @@ -67,9 +65,19 @@ module.exports = function DisposeResources(disposeCapability, completion) { if (!result) { throw new $SyntaxError('Assertion failed: non-`~ASYNC-DISPOSE~` resource returned a promise from Dispose'); } - return $then(result, NormalCompletion); - }, - rejecter + return $then(result, void undefined, rejecter); + } catch (e) { + rejecter(e); + } + return void undefined; + }; + return $then( + promise, + runDispose, + function (e) { + rejecter(e); + return runDispose(); + } ); }; @@ -96,5 +104,10 @@ module.exports = function DisposeResources(disposeCapability, completion) { // eslint-disable-next-line no-param-reassign disposeCapability['[[DisposableResourceStack]]'] = null; // step 3 - return actualHint === 'ASYNC-DISPOSE' ? promise : completion; // step 4 + if (actualHint === 'ASYNC-DISPOSE') { // step 4 + return $then(promise, function () { + return completion; + }); + } + return completion; }; diff --git a/test/tests.js b/test/tests.js index 6c9dc79..e823268 100644 --- a/test/tests.js +++ b/test/tests.js @@ -621,7 +621,35 @@ module.exports = { function (e) { st.equal(e, throwSentinel, 'throws `throwSentinel`'); } - ); + ).then(function () { + // https://github.com/es-shims/DisposableStack/issues/9 + var instance3 = new AsyncDisposableStack(); + var ran = 0; + instance3.defer(function () { ran += 1; }); + instance3.defer(throwsSentinel); + + return instance3.disposeAsync().then( + function () { + st.fail('dispose with a throwing later disposable failed to throw'); + }, + function (e) { + st.equal(e, throwSentinel, 'throws `throwSentinel` when an earlier-pushed defer throws'); + st.equal(ran, 1, 'later-pushed non-throwing defer still ran'); + } + ); + }).then(function () { + var instance4 = new AsyncDisposableStack(); + instance4.defer(function () { return Promise.reject(throwSentinel); }); + + return instance4.disposeAsync().then( + function () { + st.fail('dispose with a rejecting disposable failed to throw'); + }, + function (e) { + st.equal(e, throwSentinel, 'rejecting defer surfaces `throwSentinel`'); + } + ); + }); }); });