TanStack / TanStack/db

BrowserCollectionCoordinator + Electric adapter: collections never reach ready state

Open
#1,443 1 comment 2 reactions 0 assignees View on GitHub

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

  1. OPFS database opens successfully
  2. BrowserCollectionCoordinator instantiates without error
  3. Electric shape HTTP requests fire and return data (confirmed in Network tab)
  4. useLiveQuery returns empty arrays — collections appear stuck in non-ready state
  5. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.