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.
Use case / problem
On Flutter web the sync engine runs inside a shared worker (
WebPowerSyncDatabase.connectInternal→SyncWorkerHandle.start→new SharedWorker(workerUri)inlib/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,
connectInternalalready contains a main-isolate fallback — it constructs aStreamingSyncImplementation(no shared worker) in thecatchbranch — but that branch is only reached ifSyncWorkerHandle.startthrows, which in practice only happens for ablob:workerUri behind anassert(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 toconnect(...)— to run sync on the main isolate instead of the shared sync worker, i.e. deterministically take the existingStreamingSyncImplementationpath.Benefits:
The database can keep running in its own (dedicated or shared) worker independently — this request is specifically about the sync worker.
Alternatives considered
WebPowerSyncOpenFactory.connectToWorker— only controls the database worker'sAccessMode(viasqlite3_web). The sync worker is spawned by a separatenew 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.jsfor sync).SyncWorkerHandle.start— works, but is an ongoing maintenance burden on the most correctness-sensitive layer.Environment
powersync2.3.1 (also checked latest 2.3.3 — no such option);sqlite3_web0.9.2;sqlite_async0.14.3.