Skip to content

Worker: init() rejection is never propagated to clients #1122

Description

@esafak

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):

  1. 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)).
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions