UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

94 lines (93 loc) • 3.91 kB
/** * One implementation of the "race a promise against a timer" pattern that had * drifted into three near-identical copies (#366): `withTimeout` (clients/lsp), * `withBudget` (read-expansion), and `withinRemaining` (module-report-lsp). * * The differences between them were real — timeout can *reject* or *resolve * undefined*, and the raced promise's own rejection can *propagate* or be * *swallowed* — so they are kept as named adapters over one core. Consolidating * fixes two latent bugs the copies had: `withBudget` did not suppress the loser * promise's late rejection (an unhandled rejection if the timer won first), and * `withinRemaining` never cleared its timer. */ /** * Combine multiple abort signals into one that aborts when ANY of them does. * Returns the single signal unchanged when only one is live, and `undefined` * when none are — so callers can pass it straight through. Used so a tool honors * both its tool-call `signal` positional and the turn-wired `ctx.signal` (Escape), * and to fold in a wall-clock ceiling via `AbortSignal.timeout`. */ export function combineAbortSignals(...signals) { const live = signals.filter((s) => s !== undefined); if (live.length <= 1) return live[0]; if (typeof AbortSignal.any === "function") return AbortSignal.any(live); const controller = new AbortController(); for (const s of live) { if (s.aborted) { controller.abort(s.reason); break; } s.addEventListener("abort", () => controller.abort(s.reason), { once: true }); } return controller.signal; } export function withDeadline(promise, options) { const onTimeout = options.onTimeout ?? "reject"; const onReject = options.onReject ?? "propagate"; const ms = options.ms ?? (options.deadlineAt !== undefined ? options.deadlineAt - Date.now() : 0); // Past deadline / non-positive budget: settle immediately, no timer. if (ms <= 0) { return onTimeout === "undefined" ? Promise.resolve(undefined) : Promise.reject(new Error(`Timeout after ${Math.max(0, ms)}ms`)); } // Base promise with rejection handled per `onReject`. In propagate mode we // still attach a no-op catch so that if the timer wins the race, the loser // promise's later rejection does not surface as an unhandled rejection. const base = onReject === "undefined" ? promise.catch(() => undefined) : promise; if (onReject === "propagate") promise.catch(() => { }); let timer; const timeoutPromise = new Promise((resolve, reject) => { timer = setTimeout(() => { if (onTimeout === "undefined") resolve(undefined); else reject(new Error(`Timeout after ${ms}ms`)); }, ms); }); return Promise.race([base, timeoutPromise]).finally(() => { if (timer) clearTimeout(timer); }); } /** * Resolve `promise`, or reject with `Error("Timeout after <ms>ms")` once * `timeoutMs` elapses. The raced promise's own rejection propagates. */ export function withTimeout(promise, timeoutMs) { return withDeadline(promise, { ms: timeoutMs }); } /** * Resolve `promise`, or `undefined` once `budgetMs` elapses (a non-positive * budget resolves `undefined` immediately). The raced promise's own rejection * propagates. */ export function withBudget(promise, budgetMs) { return withDeadline(promise, { ms: budgetMs, onTimeout: "undefined" }); } /** * Resolve `promise`, or `undefined` once the shared `deadlineAt` passes (a past * deadline resolves `undefined` immediately). The raced promise's own rejection * is swallowed to `undefined`. */ export function withinRemaining(promise, deadlineAt) { return withDeadline(promise, { deadlineAt, onTimeout: "undefined", onReject: "undefined", }); }