diff --git a/gs/builtin/defer-browser.test.ts b/gs/builtin/defer-browser.test.ts new file mode 100644 index 00000000..873fba29 --- /dev/null +++ b/gs/builtin/defer-browser.test.ts @@ -0,0 +1,56 @@ +import { execFileSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { chromium, webkit } from 'playwright' +import { afterAll, describe, expect, it } from 'vitest' + +const outputDir = mkdtempSync(join(tmpdir(), 'goscript-disposable-stack-')) +const outputPath = join(outputDir, 'bundle.js') +const entryPath = fileURLToPath( + new URL('./testdata/disposable-stack-browser.ts', import.meta.url), +) + +execFileSync('bun', [ + 'build', + entryPath, + '--target=browser', + '--format=iife', + `--outfile=${outputPath}`, +]) +const bundle = readFileSync(outputPath, 'utf8') + +afterAll(() => rmSync(outputDir, { recursive: true, force: true })) + +describe.each([ + ['Chromium', chromium], + ['WebKit', webkit], +])('DisposableStack in %s', (_name, browserType) => { + it('supports generated using declarations', async () => { + const browser = await browserType.launch({ headless: true }) + const page = await browser.newPage() + const errors: string[] = [] + page.on('pageerror', (error) => errors.push(error.message)) + + try { + await page.addScriptTag({ content: bundle }) + expect(errors).toEqual([]) + expect( + await page.evaluate( + async () => await globalThis.__goscriptDisposableStackResult, + ), + ).toEqual({ + disposeSymbolPresent: _name === 'Chromium', + asyncDisposeSymbolPresent: _name === 'Chromium', + usesDisposeSymbol: true, + usesAsyncDisposeSymbol: true, + order: ['second', 'first', 'async'], + disposeError: 'deferred failure', + }) + } finally { + await browser.close() + } + }) +}) diff --git a/gs/builtin/defer.ts b/gs/builtin/defer.ts index 6abd94f2..12b00390 100644 --- a/gs/builtin/defer.ts +++ b/gs/builtin/defer.ts @@ -1,5 +1,11 @@ import { withRecoveringPanic } from './panic.js' +const disposeSymbol: typeof Symbol.dispose = + Symbol.dispose ?? (Symbol.for('Symbol.dispose') as typeof Symbol.dispose) +const asyncDisposeSymbol: typeof Symbol.asyncDispose = + Symbol.asyncDispose ?? + (Symbol.for('Symbol.asyncDispose') as typeof Symbol.asyncDispose) + /** * DisposableStack manages synchronous disposable resources, mimicking Go's defer behavior. * Functions added via `defer` are executed in LIFO order when the stack is disposed. @@ -30,7 +36,7 @@ export class DisposableStack implements Disposable { /** * Disposes during ordinary scope exit. */ - [Symbol.dispose](): void { + [disposeSymbol](): void { this.dispose() } } @@ -68,11 +74,11 @@ export class AsyncDisposableStack implements AsyncDisposable { } } - async [Symbol.asyncDispose](): Promise { + async [asyncDisposeSymbol](): Promise { await this.dispose() } - [Symbol.dispose](): void { + [disposeSymbol](): void { while (this.stack.length) { const fn = this.stack.pop()! const result = fn() diff --git a/gs/builtin/testdata/disposable-stack-browser.ts b/gs/builtin/testdata/disposable-stack-browser.ts new file mode 100644 index 00000000..bb8ea003 --- /dev/null +++ b/gs/builtin/testdata/disposable-stack-browser.ts @@ -0,0 +1,64 @@ +import { AsyncDisposableStack, DisposableStack } from '../defer.js' + +type DisposableStackResult = { + disposeSymbolPresent: boolean + asyncDisposeSymbolPresent: boolean + usesDisposeSymbol: boolean + usesAsyncDisposeSymbol: boolean + order: string[] + disposeError: string | undefined +} + +declare global { + var __goscriptDisposableStackResult: + | Promise + | undefined +} + +globalThis.__goscriptDisposableStackResult = (async () => { + const disposeSymbol = Symbol.dispose ?? Symbol.for('Symbol.dispose') + const asyncDisposeSymbol = + Symbol.asyncDispose ?? Symbol.for('Symbol.asyncDispose') + const result: DisposableStackResult = { + disposeSymbolPresent: typeof Symbol.dispose === 'symbol', + asyncDisposeSymbolPresent: typeof Symbol.asyncDispose === 'symbol', + usesDisposeSymbol: + typeof Object.getOwnPropertyDescriptor( + DisposableStack.prototype, + disposeSymbol, + )?.value === 'function', + usesAsyncDisposeSymbol: + typeof Object.getOwnPropertyDescriptor( + AsyncDisposableStack.prototype, + asyncDisposeSymbol, + )?.value === 'function', + order: [], + disposeError: undefined, + } + + { + using stack = new DisposableStack() + stack.defer(() => result.order.push('first')) + stack.defer(() => result.order.push('second')) + } + + await (async () => { + await using stack = new AsyncDisposableStack() + stack.defer(async () => { + result.order.push('async') + }) + })() + + try { + { + using stack = new DisposableStack() + stack.defer(() => { + throw new Error('deferred failure') + }) + } + } catch (error) { + result.disposeError = error instanceof Error ? error.message : String(error) + } + + return result +})()