BrowserCollectionCoordinator + Electric adapter: collections never reach ready state
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.9k
- Forks
- 266
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 55
Description
BrowserCollectionCoordinator + Electric adapter: collections never reach "ready" state
Package & Version
@tanstack/browser-db-sqlite-persistence@0.1.5@tanstack/db@0.6.1@tanstack/electric-db-collection@0.2.43@electric-sql/client@1.5.14
Environment
- Chrome 136 (latest stable)
- Vite / TanStack Start
- Single browser tab (no multi-tab involved)
Description
When BrowserCollectionCoordinator is passed to createBrowserWASQLitePersistence and the persistence wraps Electric collections (via electricCollectionOptions), collections never reach the "ready" state. Electric shape HTTP requests go out and return 200 with data (confirmed via Network tab), but useLiveQuery subscribers return empty results.
Removing the coordinator (falling back to the default SingleProcessCoordinator) immediately fixes the issue — Electric data flows through persistence and renders correctly.
Isolation Testing
To isolate whether the issue is the coordinator generally or specific to Electric, I tested with queryCollectionOptions (TanStack Query adapter) + BrowserCollectionCoordinator:
| Config | Collections | Result |
|---|---|---|
queryCollectionOptions + no coordinator |
1 | ✅ Works |
queryCollectionOptions + BrowserCollectionCoordinator |
1 | ✅ Works |
queryCollectionOptions + BrowserCollectionCoordinator |
21 | ✅ Works |
electricCollectionOptions + no coordinator |
21 | ✅ Works |
electricCollectionOptions + BrowserCollectionCoordinator |
21 | ❌ No data |
The coordinator works correctly with the query adapter at any scale. The issue is specific to the Electric adapter + coordinator combination.
Test Coverage Note
The official example (examples/react/offline-transactions/src/db/persisted-todos.ts) uses BrowserCollectionCoordinator only with local-only persistence — no sync adapter. The e2e test (browser-single-tab-persisted-collection.e2e.test.ts) also uses no coordinator. The Electric + coordinator combination may be an untested code path.
Reproduction (Real App)
const database = await openBrowserWASQLiteOPFSDatabase({
databaseName: "myapp.sqlite",
});
const coordinator = new BrowserCollectionCoordinator({ dbName: "myapp" });
const persistence = createBrowserWASQLitePersistence({ database, coordinator });
// ❌ BROKEN — Electric data arrives but collection never becomes "ready"
const myCollection = createCollection(
persistedCollectionOptions({
...electricCollectionOptions({
id: "my-collection",
shapeOptions: { url: "/api/my-shape", columnMapper: snakeCamelMapper() },
syncMode: "eager",
getKey: (item) => item.id,
schema: mySchema,
}),
persistence,
schemaVersion: 1,
}),
);
// Electric requests return 200 with data in Network tab
// useLiveQuery returns { data: [] }
Works when coordinator is removed:
const persistence = createBrowserWASQLitePersistence({ database }); // no coordinator
Observed Behavior
- OPFS database opens successfully
BrowserCollectionCoordinatorinstantiates without error- Electric shape HTTP requests fire and return data (confirmed in Network tab)
useLiveQueryreturns empty arrays — collections appear stuck in non-ready state- No errors in the console after clearing stale OPFS data
Earlier Error (Before Clearing OPFS)
On the first attempt, before clearing stale OPFS data, we saw:
Failed to acquire leadership for auth-state: OPFSWorkerRequestError: UNIQUE constraint failed: collection_registry.tombstone_table_name
After clearing OPFS, this error stopped, but data still didn't flow.
Suspected Root Cause
Based on source analysis: markReady() is wrapped in persisted.ts:2250-2264 to defer until runtime.ensureStarted() resolves. The sync function runs unconditionally (Electric data arrives regardless of leadership), but useLiveQuery waits for "ready" before returning results.
If runtime.ensureStarted() hangs when the coordinator is involved — possibly due to how Electric's SSE-based sync interacts with the coordinator's OPFS write path or startup sequence — markReady() is never called.
Supporting evidence: console.warn("Failed persisted sync startup before markReady:") was not observed, suggesting the startup promise hangs rather than rejects.
Since the query adapter (which uses a simple fetch-and-complete pattern) works correctly with the coordinator, the issue may be related to Electric's long-lived SSE connection interacting with the coordinator's transaction serialization or leader election timing.
Workaround
Omit the coordinator to use SingleProcessCoordinator (default). This disables multi-tab coordination but allows single-tab persistence with Electric to work correctly:
const persistence = createBrowserWASQLitePersistence({ database });
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with persisted.ts:2250-2264 and trace how runtime.ensureStarted() and markReady() interact with BrowserCollectionCoordinator and the Electric sync path. Compare the existing browser-single-tab-persisted-collection.e2e.test.ts and persisted-todos.ts example, then reproduce the Electric-plus-coordinator configuration. Done means the collection reaches ready and useLiveQuery returns the synced data while the coordinator remains enabled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sqlite, typescript
- Domain
- database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100