-
Notifications
You must be signed in to change notification settings - Fork 152
Description
- I've validated the bug against the latest version of DB packages
Describe the bug
When using electricCollectionOptions with syncMode: 'progressive', the application throws SyncTransactionAlreadyCommittedError or SyncTransactionAlreadyCommittedWriteError after a browser visibility change (tab switch, window minimize/restore, etc.).
The error occurs because the visibilityHandler triggers resume_fn, which attempts to write to or commit a sync transaction that has already been committed. The transaction state (transactionStarted) is not properly reset when resuming from a visibility change.
Package Versions
@tanstack/db: ^0.5.18@tanstack/electric-db-collection: ^0.2.22@tanstack/react-db: ^0.1.62@electric-sql/client: (latest)
To Reproduce
-
Create an Electric collection with
syncMode: 'progressive':const eventsCollection = createCollection( electricCollectionOptions({ id: `events-${chatId}`, schema: mySchema, shapeOptions: { ... }, getKey: item => item.id, syncMode: 'progressive', }), );
-
Perform a mutation that triggers data sync (e.g., insert new records)
-
While the sync is in progress, switch to another browser tab or minimize the window
-
Return to the tab/window
-
Observe the error in the console
Error Stack Trace
Uncaught TransactionError: The pending sync transaction is already committed, you can't commit it again.
TanStackDBError errors.ts:4
TransactionError errors.ts:272
SyncTransactionAlreadyCommittedError errors.ts:338
commit sync.ts:177
unsubscribeStream electric.ts:1504
publish_fn client.ts:1398
publish_fn client.ts:1396
promise callback*publish_fn client.ts:1394
onMessages_fn client.ts:1088
requestShapeLongPoll_fn client.ts:1155
fetchShape_fn client.ts:1133
requestShape_fn client.ts:749
start_fn client.ts:664
resume_fn client.ts:1285
visibilityHandler client.ts:1427
Also occurs as a write error:
Uncaught TransactionError: The pending sync transaction is already committed, you can't still write to it.
TanStackDBError errors.ts:4
TransactionError errors.ts:272
SyncTransactionAlreadyCommittedWriteError errors.ts:324
write sync.ts:112
processChangeMessage electric.ts:1278
unsubscribeStream electric.ts:1377
publish_fn client.ts:1398
...
visibilityHandler client.ts:1427
Expected behavior
The sync transaction state should be properly reset when visibilityHandler triggers resume_fn. The application should handle visibility changes gracefully without throwing transaction errors.
Root Cause Analysis
Looking at the Electric collection source code, in progressive mode:
const isBufferingInitialSync = () =>
syncMode === `progressive` && !hasReceivedUpToDate
// ...
if (isBufferingInitialSync() && !transactionStarted) {
bufferedMessages.push(message)
} else {
if (!transactionStarted) {
begin()
transactionStarted = true
}
processChangeMessage(message)
}The issue appears to be:
- User performs a mutation, sync begins
- Messages are being processed, transaction is started and committed
- User switches tabs (visibility hidden)
- User returns to tab,
visibilityHandlertriggersresume_fn resume_fncallsstart_fnwhich re-subscribes to the shape- New messages arrive and attempt to write/commit to the already-committed transaction
transactionStartedflag is out of sync with actual transaction state
Suggested Fix: Reset transactionStarted to false when the visibility handler resumes, or ensure the transaction state is properly synchronized before processing new messages after a resume.
Workaround
Removing syncMode: 'progressive' resolves the issue:
const eventsCollection = createCollection(
electricCollectionOptions({
id: `events-${chatId}`,
schema: mySchema,
shapeOptions: { ... },
getKey: item => item.id,
// syncMode: 'progressive', // Disabled due to visibility resume bug
}),
);Desktop
- OS: Linux (Arch Linux, kernel 6.17.9)
- Browser: Firefox (latest)
- Also reproducible on Chrome
Additional context
This bug is particularly problematic for applications where users frequently switch between tabs while real-time sync operations are in progress. The error is not recoverable without a page refresh, which disrupts the user experience significantly.