fix(web): quiesce websocket teardown

This commit is contained in:
imccyu
2026-08-04 16:42:50 +08:00
parent c6d0cbd8de
commit 7f3a2dae91
17 changed files with 124 additions and 43 deletions

View File

@@ -21,7 +21,9 @@ afterEach(async () => {
function untilAbort(signal: AbortSignal): Promise<void> {
if (signal.aborted) return Promise.resolve()
return new Promise(resolve => signal.addEventListener('abort', () => { resolve() }, { once: true }))
return new Promise((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
}
async function * idle<F>(signal: AbortSignal): AsyncGenerator<RpcRequest<F>> {
@@ -147,7 +149,7 @@ describe('WebSocket downlinks', () => {
await once(socket, 'open')
const closed = once(socket, 'close')
socket.send('upstream payload')
const [code, reason] = await closed
const [code, reason] = await closed as [number, Buffer]
expect(code).toBe(1008)
expect(String(reason)).toBe('downlink only')
await vi.waitFor(() => { expect(aborted).toBe(true) })
@@ -197,9 +199,9 @@ describe('WebSocket downlinks', () => {
it('drops a source frame that races after the client has closed', async () => {
let release!: () => void
const gate = new Promise<void>(resolve => { release = resolve })
const gate = new Promise<void>((resolve) => { release = resolve })
let finish!: () => void
const finished = new Promise<void>(resolve => { finish = resolve })
const finished = new Promise<void>((resolve) => { finish = resolve })
let sourceSignal: AbortSignal | undefined
const downlinks = new WebSocketDownlinks(api(
async function * (signal) {
@@ -227,7 +229,7 @@ describe('WebSocket downlinks', () => {
it('contains socket send callback failures and closes the downlink', async () => {
let release!: () => void
const gate = new Promise<void>(resolve => { release = resolve })
const gate = new Promise<void>((resolve) => { release = resolve })
const downlinks = new WebSocketDownlinks(api(
async function * () {
await gate
@@ -262,4 +264,39 @@ describe('WebSocket downlinks', () => {
await downlinks.close()
await expect(downlinks.close()).rejects.toThrow('The server is not running')
})
it('waits for source cleanup before teardown resolves', async () => {
let cleanupStarted!: () => void
const started = new Promise<void>((resolve) => { cleanupStarted = resolve })
let releaseCleanup!: () => void
const cleanupGate = new Promise<void>((resolve) => { releaseCleanup = resolve })
let cleaned = false
const downlinks = new WebSocketDownlinks(api(
async function * (signal) {
try {
await untilAbort(signal)
} finally {
cleanupStarted()
await cleanupGate
cleaned = true
}
},
idle,
))
const host = await serve(downlinks)
const socket = new WebSocket(`${host.origin}${MUX_EVENTS_PATH}`)
await once(socket, 'open')
let closed = false
const closing = host.close().then(() => { closed = true })
try {
await started
expect(closed).toBe(false)
releaseCleanup()
await closing
expect(cleaned).toBe(true)
} finally {
releaseCleanup()
await closing
}
})
})