| 1 | import { browserFailure } from "./errors.js"; |
| 2 | |
| 3 | // One temporary native host at a time. Cancellation removes queued work without |
| 4 | // ever borrowing the surface owned by the preceding request. |
| 5 | export class CaptureQueue { |
| 6 | private tail: Promise<void> = Promise.resolve(); |
| 7 | |
| 8 | async run<T>(signal: AbortSignal, work: () => Promise<T>): Promise<T> { |
| 9 | const previous = this.tail; |
| 10 | let release!: () => void; |
| 11 | const slot = new Promise<void>((resolve) => { release = resolve; }); |
| 12 | this.tail = previous.then(() => slot); |
| 13 | try { |
| 14 | await abortable(previous, signal); |
| 15 | signal.throwIfAborted(); |
| 16 | return await work(); |
| 17 | } finally { release(); } |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | export function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> { |
| 22 | return new Promise<T>((resolve, reject) => { |
| 23 | const abort = () => reject(signal.reason?.code < 0 ? signal.reason : browserFailure("cancelled", "browser request cancelled or expired")); |
| 24 | // The operation may already be running, even when its waiter is cancelled. |
| 25 | // Always consume its eventual rejection before taking the early-abort path. |
| 26 | promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)); |
| 27 | if (signal.aborted) { abort(); return; } |
| 28 | signal.addEventListener("abort", abort, { once: true }); |
| 29 | }); |
| 30 | } |
| 31 |