Summary
On Flutter Web, every time a PowerSyncDatabase is opened, sqlite3_web spawns a dedicated web worker named sqlite3_worker that is never terminate()d. close()/dispose() only sends a CloseDatabase message — it does not tear the worker down. Because a hot restart re-runs main() in the same JS VM (Dart state is wiped, but the page and its workers persist), each hot restart spawns a fresh sqlite3_worker and orphans the previous one. They accumulate until a full browser refresh recreates the JS VM.
Impact is development-only (duplicated logs, harder debugging after hot restart); production is unaffected since the VM isn't repeatedly re-initialised. Reported on powersync: 2.2.0.
Context: https://discord.com/channels/1138230179878154300/1511710494766141540
Environment
powersync 2.2.0 (pulls sqlite3_web 0.8.1, sqlite_async 0.14.2)
- Flutter Web, debug, hot restart (not hot reload — hot reload doesn't re-run
main())
- Reproduced on plain desktop Chromium (Chrome for Testing 127), no special headers
Steps to reproduce
- Minimal Flutter Web app on
powersync 2.2.0. In main(), open a PowerSyncDatabase and initialize() it. No backend, no connect(), no schema specifics required — the worker is spawned at open.
- Run on web (
flutter run -d chrome).
- Press
R (hot restart) a few times.
- Count dedicated workers named
sqlite3_worker (Chrome Task Manager, or programmatically).
Expected vs actual
- Expected: hot restart leaves a single live
sqlite3_worker; the previous VM run's worker is torn down.
- Actual: the count of
sqlite3_worker instances grows by one per hot restart and is only cleared by a full browser refresh.
Root cause
sqlite3_web 0.8.1's DatabaseClient.startWorkers() (lib/src/client.dart:429) runs both _startDedicated() and _startShared() during feature detection — neither is gated on which access mode ultimately hosts the DB. _startDedicated() spawns, via _DefaultWorkerConnector.spawnDedicatedWorker():
// sqlite3_web/lib/src/worker_connector.dart:183
Worker(_workerUrl.toJS, WorkerOptions(name: 'sqlite3_worker'))
and nothing in sqlite3_web ever calls .terminate() (zero terminate references in the package). PowerSync's web teardown (close() / sync controller close()) sends CloseDatabase over the channel but never retains or terminates the dedicated Worker handle. So:
- The dedicated
sqlite3_worker is spawned regardless of which access mode hosts the DB — even when a shared worker is selected for the actual database — and is never torn down.
- Within a single JS-VM / Dart program state the worker is created once and shared across opens:
sqlite_async caches the WebSqlite instance in a library-global map keyed by wasm+worker URL (sqlite_async/lib/src/web/web_sqlite_open_factory.dart:14-32). (This is why opening N databases in one VM does not spawn N workers.)
- A Flutter-web hot restart re-runs
main(), resetting that Dart-side cache, so the next open rebuilds WebSqlite and spawns a fresh sqlite3_worker while the prior one is orphaned in the surviving page — one extra worker per restart, cleared only by a full browser refresh (new JS VM).
This is the web analogue of the native fix in powersync 2.2.0 ("Fix disconnect() call leaving stale database isolates behind") — the same teardown gap, in the web worker execution context.
AI Disclosure
Used Claude Code with Opus 4.7 to research, debug the issue, and assist in writing a test for reproducing it. Verified with the team internally first.
Summary
On Flutter Web, every time a
PowerSyncDatabaseis opened,sqlite3_webspawns a dedicated web worker namedsqlite3_workerthat is neverterminate()d.close()/dispose()only sends aCloseDatabasemessage — it does not tear the worker down. Because a hot restart re-runsmain()in the same JS VM (Dart state is wiped, but the page and its workers persist), each hot restart spawns a freshsqlite3_workerand orphans the previous one. They accumulate until a full browser refresh recreates the JS VM.Impact is development-only (duplicated logs, harder debugging after hot restart); production is unaffected since the VM isn't repeatedly re-initialised. Reported on
powersync: 2.2.0.Context: https://discord.com/channels/1138230179878154300/1511710494766141540
Environment
powersync2.2.0 (pullssqlite3_web0.8.1,sqlite_async0.14.2)main())Steps to reproduce
powersync 2.2.0. Inmain(), open aPowerSyncDatabaseandinitialize()it. No backend, noconnect(), no schema specifics required — the worker is spawned at open.flutter run -d chrome).R(hot restart) a few times.sqlite3_worker(Chrome Task Manager, or programmatically).Expected vs actual
sqlite3_worker; the previous VM run's worker is torn down.sqlite3_workerinstances grows by one per hot restart and is only cleared by a full browser refresh.Root cause
sqlite3_web0.8.1'sDatabaseClient.startWorkers()(lib/src/client.dart:429) runs both_startDedicated()and_startShared()during feature detection — neither is gated on which access mode ultimately hosts the DB._startDedicated()spawns, via_DefaultWorkerConnector.spawnDedicatedWorker():and nothing in
sqlite3_webever calls.terminate()(zeroterminatereferences in the package). PowerSync's web teardown (close()/ sync controllerclose()) sendsCloseDatabaseover the channel but never retains or terminates the dedicatedWorkerhandle. So:sqlite3_workeris spawned regardless of which access mode hosts the DB — even when a shared worker is selected for the actual database — and is never torn down.sqlite_asynccaches theWebSqliteinstance in a library-global map keyed by wasm+worker URL (sqlite_async/lib/src/web/web_sqlite_open_factory.dart:14-32). (This is why opening N databases in one VM does not spawn N workers.)main(), resetting that Dart-side cache, so the next open rebuildsWebSqliteand spawns a freshsqlite3_workerwhile the prior one is orphaned in the surviving page — one extra worker per restart, cleared only by a full browser refresh (new JS VM).This is the web analogue of the native fix in
powersync2.2.0 ("Fixdisconnect()call leaving stale database isolates behind") — the same teardown gap, in the web worker execution context.AI Disclosure
Used Claude Code with Opus 4.7 to research, debug the issue, and assist in writing a test for reproducing it. Verified with the team internally first.