From 98fec90c53f6edc9d214bca9fe1ac02a33e05ecd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 04:55:36 +0000 Subject: [PATCH 1/2] fix(shared): use Symbol.for('Symbol.asyncDispose') as the async-dispose fallback On runtimes without a native Symbol.asyncDispose (Hermes/React Native, older Safari or Node), AsyncIteratorClass registered its dispose method under Symbol.for('asyncDispose'). esbuild's `__using`, Babel's `usingCtx` and core-js all look it up under Symbol.for('Symbol.asyncDispose'), so `await using` on an event-stream body threw "Object not disposable" and its cleanup never ran. Switch the fallback to the conventional key (still preferring the native symbol), correct the JSDoc, and cover the fallback path with a test so the v8 ignore markers can go. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NRRM1bujEJiPEv7FbiLXDr --- packages/shared/src/iterator.test.ts | 31 ++++++++++++++++++++++++++++ packages/shared/src/iterator.ts | 7 +++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/iterator.test.ts b/packages/shared/src/iterator.test.ts index 33ad9b35..bedd93d9 100644 --- a/packages/shared/src/iterator.test.ts +++ b/packages/shared/src/iterator.test.ts @@ -21,6 +21,37 @@ it('isAsyncIteratorObject', () => { expect(isAsyncIteratorObject(gen2())).toBe(false) }) +it('uses Symbol.for("Symbol.asyncDispose") when Symbol.asyncDispose is not native', async () => { + onTestFinished(() => { + vi.unstubAllGlobals() + vi.resetModules() + }) + + // Symbol.asyncDispose is non-configurable, so a Proxy over Symbol itself would break the get invariant + const RealSymbol = Symbol + vi.stubGlobal( + 'Symbol', + new Proxy(function () {} as any, { + apply: (_target, _this, args) => RealSymbol(...args), + get: (_target, key) => (key === 'asyncDispose' ? undefined : Reflect.get(RealSymbol, key)), + }), + ) + vi.resetModules() + const { AsyncIteratorClass } = await import('./iterator') + vi.unstubAllGlobals() + + const cleanup = vi.fn() + const iterator = new AsyncIteratorClass(async () => ({ done: false, value: 1 }), cleanup) + + // the key esbuild's `__using`, Babel's `usingCtx` and core-js use without the native symbol + const dispose = (iterator as any)[RealSymbol.for('Symbol.asyncDispose')] + expect(typeof dispose).toBe('function') + + await dispose.call(iterator) + expect(cleanup).toHaveBeenCalledTimes(1) + expect(cleanup).toHaveBeenCalledWith({ kind: 'cancelled' }) +}) + describe('asyncIteratorClass', () => { const next = vi.fn() const cleanup = vi.fn() diff --git a/packages/shared/src/iterator.ts b/packages/shared/src/iterator.ts index b2385dc4..2d1a6215 100644 --- a/packages/shared/src/iterator.ts +++ b/packages/shared/src/iterator.ts @@ -18,12 +18,11 @@ export interface AsyncIteratorClassNextFn { (): Promise> } -const fallbackAsyncDisposeSymbol: unique symbol = Symbol.for('asyncDispose') -/* v8 ignore start */ +// same key as esbuild's `__using`, Babel's `usingCtx` and core-js use without a native symbol +const fallbackAsyncDisposeSymbol: unique symbol = Symbol.for('Symbol.asyncDispose') const asyncDisposeSymbol: typeof Symbol extends { asyncDispose: infer T } ? T : typeof fallbackAsyncDisposeSymbol = (Symbol as any).asyncDispose ?? fallbackAsyncDisposeSymbol -/* v8 ignore end */ export class AsyncIteratorClass implements AsyncIteratorObject, AsyncGenerator @@ -99,7 +98,7 @@ export class AsyncIteratorClass } /** - * asyncDispose symbol only available in esnext, we should fallback to Symbol.for('asyncDispose') + * Uses the native Symbol.asyncDispose, or Symbol.for('Symbol.asyncDispose') on runtimes without it */ async [asyncDisposeSymbol](): Promise { this.isDone = true From 8a8e59b2e1254b90f0f4e3031fd04e9b2d776e13 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 07:36:48 +0000 Subject: [PATCH 2/2] test(shared): simplify the asyncDispose fallback test Take onTestFinished from the test context like the rest of the repo, drop a comment that repeats the one in iterator.ts, and call the dispose method directly instead of checking its type and invoking it via .call. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NRRM1bujEJiPEv7FbiLXDr --- packages/shared/src/iterator.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/iterator.test.ts b/packages/shared/src/iterator.test.ts index bedd93d9..7a2ea80a 100644 --- a/packages/shared/src/iterator.test.ts +++ b/packages/shared/src/iterator.test.ts @@ -21,7 +21,9 @@ it('isAsyncIteratorObject', () => { expect(isAsyncIteratorObject(gen2())).toBe(false) }) -it('uses Symbol.for("Symbol.asyncDispose") when Symbol.asyncDispose is not native', async () => { +it('uses Symbol.for("Symbol.asyncDispose") when Symbol.asyncDispose is not native', async ({ + onTestFinished, +}) => { onTestFinished(() => { vi.unstubAllGlobals() vi.resetModules() @@ -43,11 +45,7 @@ it('uses Symbol.for("Symbol.asyncDispose") when Symbol.asyncDispose is not nativ const cleanup = vi.fn() const iterator = new AsyncIteratorClass(async () => ({ done: false, value: 1 }), cleanup) - // the key esbuild's `__using`, Babel's `usingCtx` and core-js use without the native symbol - const dispose = (iterator as any)[RealSymbol.for('Symbol.asyncDispose')] - expect(typeof dispose).toBe('function') - - await dispose.call(iterator) + await (iterator as any)[RealSymbol.for('Symbol.asyncDispose')]() expect(cleanup).toHaveBeenCalledTimes(1) expect(cleanup).toHaveBeenCalledWith({ kind: 'cancelled' }) })