Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions gs/builtin/defer-browser.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Comment on lines +7 to +8

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 })

Check failure on line 32 in gs/builtin/defer-browser.test.ts

View workflow job for this annotation

GitHub Actions / tests (1.26.5, latest)

gs/builtin/defer-browser.test.ts > DisposableStack in WebKit > supports generated using declarations

Error: browserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/webkit-2227/pw_run.sh ╔═════════════════════════════════════════════════════════════════════════╗ ║ Looks like Playwright Test or Playwright was just installed or updated. ║ ║ Please run the following command to download new browsers: ║ ║ ║ ║ npx playwright install ║ ║ ║ ║ <3 Playwright Team ║ ╚═════════════════════════════════════════════════════════════════════════╝ ❯ gs/builtin/defer-browser.test.ts:32:39

Check failure on line 32 in gs/builtin/defer-browser.test.ts

View workflow job for this annotation

GitHub Actions / tests (1.26.5, latest)

gs/builtin/defer-browser.test.ts > DisposableStack in Chromium > supports generated using declarations

Error: browserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium_headless_shell-1200/chrome-headless-shell-linux64/chrome-headless-shell ╔═════════════════════════════════════════════════════════════════════════╗ ║ Looks like Playwright Test or Playwright was just installed or updated. ║ ║ Please run the following command to download new browsers: ║ ║ ║ ║ npx playwright install ║ ║ ║ ║ <3 Playwright Team ║ ╚═════════════════════════════════════════════════════════════════════════╝ ❯ gs/builtin/defer-browser.test.ts:32:39
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()
}
})
})
12 changes: 9 additions & 3 deletions gs/builtin/defer.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -30,7 +36,7 @@ export class DisposableStack implements Disposable {
/**
* Disposes during ordinary scope exit.
*/
[Symbol.dispose](): void {
[disposeSymbol](): void {
this.dispose()
}
}
Expand Down Expand Up @@ -68,11 +74,11 @@ export class AsyncDisposableStack implements AsyncDisposable {
}
}

async [Symbol.asyncDispose](): Promise<void> {
async [asyncDisposeSymbol](): Promise<void> {
await this.dispose()
}

[Symbol.dispose](): void {
[disposeSymbol](): void {
while (this.stack.length) {
const fn = this.stack.pop()!
const result = fn()
Expand Down
64 changes: 64 additions & 0 deletions gs/builtin/testdata/disposable-stack-browser.ts
Original file line number Diff line number Diff line change
@@ -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<DisposableStackResult>
| 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
})()
Loading