Skip to content

Web: option to run sync on the main isolate (disable the shared sync worker) for reload-based crash recovery #459

Description

@kenchennyc

Use case / problem

On Flutter web the sync engine runs inside a shared worker (WebPowerSyncDatabase.connectInternal → SyncWorkerHandle.start → new SharedWorker(workerUri) in lib/src/web/sync_controller.dart). That shared worker is deliberately kept alive across DB close (lib/src/web/worker.dart — "The worker is also used for sync … we shouldn't close it just because a database has been closed") and persists across a full page reload.

The downside: if the sync isolate inside that shared worker ever wedges or crashes, a plain page reload does not recover it — the reloaded page reconnects to the same (still-dead) shared worker, and any other open tab of the origin keeps it alive regardless. The only recoveries are to close every tab of the origin (so the browser GCs the now-zero-client worker) or to terminate the worker via chrome://inspect. For a single-user PWA this is a real papercut: sync silently stops and the obvious user action (reload) doesn't fix it.

Notably, connectInternal already contains a main-isolate fallback — it constructs a StreamingSyncImplementation (no shared worker) in the catch branch — but that branch is only reached if SyncWorkerHandle.start throws, which in practice only happens for a blob: workerUri behind an assert (i.e. debug/test only). There is no supported way to opt into it in a release build.

Proposed solution

Expose an option — e.g. a field on SyncOptions, or a parameter to connect(...) — to run sync on the main isolate instead of the shared sync worker, i.e. deterministically take the existing StreamingSyncImplementation path.

Benefits:

  • A plain page reload (or an app-coordinated all-tabs reload) reliably restarts sync, because nothing survives the reload.
  • Single-user PWAs that don't need cross-tab sync coordination can trade that for crash-recoverability.

The database can keep running in its own (dedicated or shared) worker independently — this request is specifically about the sync worker.

Alternatives considered

  • Overriding WebPowerSyncOpenFactory.connectToWorker — only controls the database worker's AccessMode (via sqlite3_web). The sync worker is spawned by a separate new SharedWorker(workerUri) call that never consults the factory, so moving the DB to a dedicated worker does not move sync off the shared worker (verified empirically — the DB-dedicated build still spawns a shared *_db.worker.js for sync).
  • Forking the SDK to skip SyncWorkerHandle.start — works, but is an ongoing maintenance burden on the most correctness-sensitive layer.

Environment

  • powersync 2.3.1 (also checked latest 2.3.3 — no such option); sqlite3_web 0.9.2; sqlite_async 0.14.3.
  • Flutter web (dart2js), Chrome.

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