-
Notifications
You must be signed in to change notification settings - Fork 152
Description
I'd just like to query a pattern I documented in https://electric-sql.com/blog/2025/07/29/local-first-sync-with-tanstack-db#transactions-and-optimistic-actions and https://electric-sql.com/blog/2025/07/29/local-first-sync-with-tanstack-db#write-path-sync
Specifically, is there a better way we can monitor the txid syncing back when writes are made to multiple collections?
The context being that transactions are explicitly designed to allow you to write atomically across collections. With the example as documented:
import { createOptimisticAction } from '@tanstack/react-db'
const createUserWithDefaultWorkspace = createOptimisticAction({
(loginName) => {
const userId = crypto.randomUUID()
const workspaceId = crypto.randomUUID()
// These inserts are applied atomically
userCollection.insert({
id: userId,
name: loginName
})
workspaceCollection.insert({
id: workspaceId,
name: 'Default'
})
membershipCollection.insert({
role: 'owner',
userId,
workspaceId
})
},
mutationFn: async (_loginName, { transaction }) => {
// In this case, the `transaction` contains all three mutations.
// ... handle sending to the server ...
}
})The local optimistic state is applied atomically (I believe?) and the mutationFn is invoked atomically (i.e.: once, with a single transaction). However, when then monitoring the replication stream for the write, the example awaits the transaction arriving in all three collections:
export const mutationFn = async (_variables, { transaction }) => {
const mutations = transaction.mutations
const collections = new Set(mutations.map(mutation => mutation.collection))
const payloadData = mutations.map(mutation => {
const { collection, ...result } = mutation
return result
})
// Post the mutations data to the ingest endpoint.
const txid = await api.post('/ingest', { mutations: payloadData })
// Monitor the collections for the transaction to sync back in.
const promises = [...collections].map(({ utils }) => utils.awaitTxId(txid))
await Promise.all(promises)
}Now, the happy path is collections all sync the write back roughly at the same time and there's no real gap between updating the synced state in each collection and dropping the optimistic state. However, obviously there can be race conditions and it's easy for one of the collections to hang, potentially for a long time.
I'm not sure whether this ever would result in flickering / temporary anomalies? But generally it feels like this weakens the transactional guarantees and leaves the door open to oddness. As well as not being very ergonomic DX.
We've obviously discussed various approaches to transactional streams across multiple shapes. I'm not sure what's possible or sane here. Just flagging it up because I was eyeballing it when writing the docs.