Version
@electric-sql/pglite 0.5.8 (@electric-sql/pglite/worker, Multi-Tab Worker)
Summary
When the worker-side init() callback rejects, the failure is never propagated to the main-thread client. Three consequences, all observed with fault injection (a plain init: async () => { throw new DOMException(..., "InvalidStateError") } — no OPFS required):
PGliteWorkerClientApi.create() resolves anyway — it only awaits the ready ack, which worker() posts before running init (worker/index.ts: postMessage({ type: "ready", id }) precedes dbPromise = init(options)).
waitReady pends forever — it resolves only on the connected message, which connectTab posts only after await dbPromise succeeds. With a rejected init it neither resolves nor rejects: any application awaiting waitReady before its first query hangs silently, with zero errors and no teardown signal.
- The failure is amplified into an unhandled-rejection storm — the client retries
tab-here every 16 ms until connected; the leader-side handler (case "tab-here": connectTab(id, await dbPromise, …)) re-awaits the same rejected promise on every retry, so each retry is an unhandled rejection in the worker. Measured in a consumer application's fault-injection run: 4,500 uncaught InvalidStateError in 40 s (~110/s), with thousands more dropped by the console buffer.
Minimal repro
// worker
await worker({
init: async (options) => {
throw new DOMException("boom", "InvalidStateError");
},
});
// main thread
const db = await PGliteWorker.create(worker, { dataDir: "opfs-ahp://demo" }); // resolves!
await db.waitReady; // pends forever; meanwhile the worker console floods
Expected
A rejected init() should reject create() (and any pending or future waitReady) with the original error, stop the tab-here retry loop, and fail later tabs' handshakes with the stored init error instead of re-raising it per retry.
Also observed (same code path, adjacent)
The main-thread retry timer keeps firing after client close() (it doesn't observe closure), so closing the BroadcastChannels turns the silent loop into InvalidStateError: postMessage on closed Channel thrown inside a timer callback. Either the timer should observe closed, or close() docs should warn that channels must remain open until unload.
Related
Suggested fix shape
Attach a handler to dbPromise that records the terminal init failure (name/message/stack); reject waitReady and settled create() callers from that state; stop/catch the retry loop on failure and on closed; in connectTab, reply to tab-here with a failure message so connecting tabs reject immediately.
Happy to send a PR if maintainers agree on the shape.
Version
@electric-sql/pglite0.5.8 (@electric-sql/pglite/worker, Multi-Tab Worker)Summary
When the worker-side
init()callback rejects, the failure is never propagated to the main-thread client. Three consequences, all observed with fault injection (a plaininit: async () => { throw new DOMException(..., "InvalidStateError") }— no OPFS required):PGliteWorkerClientApi.create()resolves anyway — it only awaits thereadyack, whichworker()posts before running init (worker/index.ts:postMessage({ type: "ready", id })precedesdbPromise = init(options)).waitReadypends forever — it resolves only on theconnectedmessage, whichconnectTabposts only afterawait dbPromisesucceeds. With a rejected init it neither resolves nor rejects: any application awaitingwaitReadybefore its first query hangs silently, with zero errors and no teardown signal.tab-hereevery 16 ms untilconnected; the leader-side handler (case "tab-here": connectTab(id, await dbPromise, …)) re-awaits the same rejected promise on every retry, so each retry is an unhandled rejection in the worker. Measured in a consumer application's fault-injection run: 4,500 uncaughtInvalidStateErrorin 40 s (~110/s), with thousands more dropped by the console buffer.Minimal repro
Expected
A rejected
init()should rejectcreate()(and any pending or futurewaitReady) with the original error, stop thetab-hereretry loop, and fail later tabs' handshakes with the stored init error instead of re-raising it per retry.Also observed (same code path, adjacent)
The main-thread retry timer keeps firing after client
close()(it doesn't observe closure), so closing the BroadcastChannels turns the silent loop intoInvalidStateError: postMessage on closed Channelthrown inside a timer callback. Either the timer should observeclosed, orclose()docs should warn that channels must remain open until unload.Related
Suggested fix shape
Attach a handler to
dbPromisethat records the terminal init failure (name/message/stack); rejectwaitReadyand settledcreate()callers from that state; stop/catch the retry loop on failure and onclosed; inconnectTab, reply totab-herewith a failure message so connecting tabs reject immediately.Happy to send a PR if maintainers agree on the shape.