All files / src/utils idle.ts

100% Statements 19/19
100% Branches 8/8
100% Functions 4/4
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37    10x 10x                 284x 284x 521x 521x 521x   521x     499x 499x 5x         5x 5x   494x 494x 275x      
export class StreamIdleError extends Error {
    constructor(timeoutMs: number) {
        super(`stream idle for ${Math.round(timeoutMs / 1000)}s`);
        this.name = "StreamIdleError";
    }
}
 
export async function* withIdleTimeout<T>(
    gen: AsyncGenerator<T, void>,
    timeoutMs: number,
    abort: () => void,
): AsyncGenerator<T> {
    const iter = gen[Symbol.asyncIterator]();
    while (true) {
        let timer: number | null = null;
        const idle = new Promise<"idle">((resolve) => {
            timer = window.setTimeout(() => resolve("idle"), timeoutMs);
        });
        const race = await Promise.race([iter.next(), idle]);
        // Guard against vitest's fake-timer lifecycle leaving clearTimeout undefined
        // across test-file boundaries; in production both are always defined.
        if (timer !== null && typeof clearTimeout === "function") window.clearTimeout(timer);
        if (race === "idle") {
            abort();
            // Fire-and-forget: iter.return() lets the source generator exit its
            // try/finally blocks, but we don't await it — when the underlying
            // fetch is aborted mid-read, the current iter.next() may hang, and
            // awaiting return() would hang with it. abort() already propagates.
            void iter.return?.(undefined);
            throw new StreamIdleError(timeoutMs);
        }
        const { done, value } = race;
        if (done) return;
        yield value;
    }
}