-
Notifications
You must be signed in to change notification settings - Fork 201
BrowserCollectionCoordinator + Electric adapter: collections never reach ready state #1443
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 coordinatorObserved 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 });