Merge pull request #1055 from jiwangyihao/fix/browser-worker-startup-errors
fix(browser): surface tab worker startup errors
This commit is contained in:
@@ -30,6 +30,7 @@ import type {
|
||||
interface WorkerHandle {
|
||||
send(msg: WorkerInbound, transferList?: Transferable[]): void;
|
||||
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
|
||||
onError(handler: (error: Error) => void): () => void;
|
||||
terminate(): Promise<void>;
|
||||
readonly mode: "worker" | "inline";
|
||||
}
|
||||
@@ -120,23 +121,14 @@ export async function acquireTab(
|
||||
|
||||
const initPayload = await buildInitPayload(browser, opts);
|
||||
const worker = await spawnTabWorker();
|
||||
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
|
||||
const unlisten = worker.onMessage(msg => {
|
||||
if (msg.type === "ready") resolve(msg.info);
|
||||
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
|
||||
else if (msg.type === "log") logWorkerMessage(msg);
|
||||
});
|
||||
let info: ReadyInfo;
|
||||
try {
|
||||
worker.send({ type: "init", payload: initPayload });
|
||||
info = await raceWithTimeout(promise, opts.timeoutMs + GRACE_MS, "Timed out initializing browser tab worker");
|
||||
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
||||
} catch (error) {
|
||||
unlisten();
|
||||
await worker.terminate().catch(() => undefined);
|
||||
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
||||
throw error;
|
||||
}
|
||||
unlisten();
|
||||
|
||||
holdBrowser(browser);
|
||||
const tab: TabSession = {
|
||||
@@ -473,6 +465,17 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
|
||||
worker.addEventListener("message", wrap);
|
||||
return () => worker.removeEventListener("message", wrap);
|
||||
},
|
||||
onError(handler) {
|
||||
const onError = (event: ErrorEvent): void => handler(errorFromWorkerEvent(event));
|
||||
const onMessageError = (event: MessageEvent): void =>
|
||||
handler(new ToolError(`Tab worker message error: ${String(event.data)}`));
|
||||
worker.addEventListener("error", onError);
|
||||
worker.addEventListener("messageerror", onMessageError);
|
||||
return () => {
|
||||
worker.removeEventListener("error", onError);
|
||||
worker.removeEventListener("messageerror", onMessageError);
|
||||
};
|
||||
},
|
||||
async terminate() {
|
||||
worker.terminate();
|
||||
},
|
||||
@@ -511,6 +514,44 @@ async function spawnInlineWorker(): Promise<WorkerHandle> {
|
||||
hostListeners.add(handler);
|
||||
return () => hostListeners.delete(handler);
|
||||
},
|
||||
onError: () => () => {},
|
||||
async terminate() {},
|
||||
};
|
||||
}
|
||||
|
||||
async function initializeTabWorker(
|
||||
worker: WorkerHandle,
|
||||
payload: WorkerInitPayload,
|
||||
timeoutMs: number,
|
||||
): Promise<ReadyInfo> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
|
||||
const unlisten = worker.onMessage(msg => {
|
||||
if (msg.type === "ready") resolve(msg.info);
|
||||
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
|
||||
else if (msg.type === "log") logWorkerMessage(msg);
|
||||
});
|
||||
const unlistenError = worker.onError(error => {
|
||||
reject(new ToolError(`Tab worker failed during startup: ${error.message}`));
|
||||
});
|
||||
try {
|
||||
worker.send({ type: "init", payload });
|
||||
return await raceWithTimeout(promise, timeoutMs, "Timed out initializing browser tab worker");
|
||||
} finally {
|
||||
unlisten();
|
||||
unlistenError();
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeTabWorkerForTest(
|
||||
worker: WorkerHandle,
|
||||
payload: WorkerInitPayload,
|
||||
timeoutMs: number,
|
||||
): Promise<ReadyInfo> {
|
||||
return initializeTabWorker(worker, payload, timeoutMs);
|
||||
}
|
||||
|
||||
function errorFromWorkerEvent(event: ErrorEvent): Error {
|
||||
if (event.error instanceof Error) return event.error;
|
||||
if (event.message) return new Error(event.message);
|
||||
return new Error("Unknown tab worker error");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import type { ReadyInfo, WorkerInbound, WorkerOutbound } from "../../src/tools/browser/tab-protocol";
|
||||
import { initializeTabWorkerForTest } from "../../src/tools/browser/tab-supervisor";
|
||||
|
||||
class FakeStartupWorker {
|
||||
#errorHandlers = new Set<(error: Error) => void>();
|
||||
#messageHandlers = new Set<(msg: WorkerOutbound) => void>();
|
||||
readonly sent: WorkerInbound[] = [];
|
||||
readonly mode = "worker" as const;
|
||||
|
||||
send(msg: WorkerInbound): void {
|
||||
this.sent.push(msg);
|
||||
}
|
||||
|
||||
onMessage(handler: (msg: WorkerOutbound) => void): () => void {
|
||||
this.#messageHandlers.add(handler);
|
||||
return () => this.#messageHandlers.delete(handler);
|
||||
}
|
||||
|
||||
onError(handler: (error: Error) => void): () => void {
|
||||
this.#errorHandlers.add(handler);
|
||||
return () => this.#errorHandlers.delete(handler);
|
||||
}
|
||||
|
||||
async terminate(): Promise<void> {}
|
||||
|
||||
emitReady(info: ReadyInfo): void {
|
||||
for (const handler of this.#messageHandlers) handler({ type: "ready", info });
|
||||
}
|
||||
|
||||
emitError(error: Error): void {
|
||||
for (const handler of this.#errorHandlers) handler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const initPayload = {
|
||||
mode: "headless" as const,
|
||||
browserWSEndpoint: "ws://127.0.0.1/devtools/browser/test",
|
||||
safeDir: "/tmp/omp-puppeteer",
|
||||
timeoutMs: 1_000,
|
||||
};
|
||||
|
||||
describe("browser tab worker startup", () => {
|
||||
it("surfaces worker startup errors instead of waiting for the generic init timeout", async () => {
|
||||
const worker = new FakeStartupWorker();
|
||||
const pending = initializeTabWorkerForTest(worker, initPayload, 1_000);
|
||||
|
||||
worker.emitError(new Error("Cannot find tab-worker-entry.ts"));
|
||||
|
||||
await expect(pending).rejects.toThrow("Tab worker failed during startup: Cannot find tab-worker-entry.ts");
|
||||
expect(worker.sent).toEqual([{ type: "init", payload: initPayload }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user