fix: bound signal/imessage transport readiness waits

Co-authored-by: Szpadel <1857251+Szpadel@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-16 20:33:04 +00:00
parent 0cd24137e8
commit aaa310c047
9 changed files with 172 additions and 26 deletions

View File

@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { waitForTransportReady } from "./transport-ready.js";
describe("waitForTransportReady", () => {
it("returns when the check succeeds and logs after the delay", async () => {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
let attempts = 0;
await waitForTransportReady({
label: "test transport",
timeoutMs: 500,
logAfterMs: 100,
logIntervalMs: 100,
pollIntervalMs: 50,
runtime,
check: async () => {
attempts += 1;
if (attempts > 3) return { ok: true };
return { ok: false, error: "not ready" };
},
});
expect(runtime.error).toHaveBeenCalled();
});
it("throws after the timeout", async () => {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
await expect(
waitForTransportReady({
label: "test transport",
timeoutMs: 200,
logAfterMs: 0,
logIntervalMs: 100,
pollIntervalMs: 50,
runtime,
check: async () => ({ ok: false, error: "still down" }),
}),
).rejects.toThrow("test transport not ready");
expect(runtime.error).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,59 @@
import { danger } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import { sleepWithAbort } from "./backoff.js";
export type TransportReadyResult = {
ok: boolean;
error?: string | null;
};
export type WaitForTransportReadyParams = {
label: string;
timeoutMs: number;
logAfterMs?: number;
logIntervalMs?: number;
pollIntervalMs?: number;
abortSignal?: AbortSignal;
runtime: RuntimeEnv;
check: () => Promise<TransportReadyResult>;
};
export async function waitForTransportReady(params: WaitForTransportReadyParams): Promise<void> {
const started = Date.now();
const timeoutMs = Math.max(0, params.timeoutMs);
const deadline = started + timeoutMs;
const logAfterMs = Math.max(0, params.logAfterMs ?? timeoutMs);
const logIntervalMs = Math.max(1_000, params.logIntervalMs ?? 30_000);
const pollIntervalMs = Math.max(50, params.pollIntervalMs ?? 150);
let nextLogAt = started + logAfterMs;
let lastError: string | null = null;
while (true) {
if (params.abortSignal?.aborted) return;
const res = await params.check();
if (res.ok) return;
lastError = res.error ?? null;
const now = Date.now();
if (now >= deadline) break;
if (now >= nextLogAt) {
const elapsedMs = now - started;
params.runtime.error?.(
danger(`${params.label} not ready after ${elapsedMs}ms (${lastError ?? "unknown error"})`),
);
nextLogAt = now + logIntervalMs;
}
try {
await sleepWithAbort(pollIntervalMs, params.abortSignal);
} catch (err) {
if (params.abortSignal?.aborted) return;
throw err;
}
}
params.runtime.error?.(
danger(`${params.label} not ready after ${timeoutMs}ms (${lastError ?? "unknown error"})`),
);
throw new Error(`${params.label} not ready (${lastError ?? "unknown error"})`);
}