Skip to content

Commit 58f119a

Browse files
KyleAMathewsclaude
andauthored
Fix returning an array of txids from an Electric collection handler (#795)
* test: add failing test for array txid bug in electric collection This test demonstrates issue #793 where returning an array of txids from a collection handler (e.g., `return { txid: [txid1, txid2] }`) causes a TimeoutWaitingForTxIdError instead of properly waiting for all txids to be seen in the sync stream. The test shows that when a handler returns multiple txids, the processMatchingStrategy function doesn't correctly handle the array, even though it has code that appears to support arrays. * test: make array txid test more realistic with staggered arrivals Update the test to send txids on different ticks (50ms and 100ms apart) rather than in a single batch. This better simulates real-world conditions where multiple txids rarely arrive together. * fix: correct array txid handling in electric collection handlers The bug was in processMatchingStrategy when handling arrays of txids. Using `.map(awaitTxId)` directly caused Array.map() to pass three arguments (element, index, array), and the index was being interpreted as the timeout parameter of awaitTxId. For example: - awaitTxId(txid1, 0) → timeout 0ms (immediate timeout!) - awaitTxId(txid2, 1) → timeout 1ms (immediate timeout!) This caused TimeoutWaitingForTxIdError even though the txids were being sent correctly via the sync stream. The fix uses an arrow function to ensure only the txid is passed: await Promise.all(result.txid.map((txid) => awaitTxId(txid))) This allows the timeout to use its default value of 5000ms. Fixes #793 * chore: add changeset for array txid fix * test: fix TypeScript errors in array txid test Add non-null assertions for array element access to fix type errors where txids[0] and txids[1] could theoretically be undefined. * chore: remove obvious comment --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ebc5ed2 commit 58f119a

3 files changed

Lines changed: 84 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/electric-db-collection": patch
3+
---
4+
5+
Fix array txid handling in electric collection handlers. When returning `{ txid: [txid1, txid2] }` from an `onInsert`, `onUpdate`, or `onDelete` handler, the system would timeout with `TimeoutWaitingForTxIdError` instead of properly waiting for all txids. The bug was caused by passing array indices as timeout parameters when calling `awaitTxId` via `.map()`.

packages/electric-db-collection/src/electric.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ export function electricCollectionOptions(
533533
if (result && `txid` in result) {
534534
// Handle both single txid and array of txids
535535
if (Array.isArray(result.txid)) {
536-
await Promise.all(result.txid.map(awaitTxId))
536+
await Promise.all(result.txid.map((txid) => awaitTxId(txid)))
537537
} else {
538538
await awaitTxId(result.txid)
539539
}

packages/electric-db-collection/tests/electric.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,84 @@ describe(`Electric Integration`, () => {
672672
expect(onInsert).toHaveBeenCalled()
673673
})
674674

675+
it(`should handle array of txids returned from handler`, async () => {
676+
// Create a fake backend that returns multiple txids
677+
const fakeBackend = {
678+
persist: (
679+
mutations: Array<PendingMutation<Row>>
680+
): Promise<Array<number>> => {
681+
// Simulate multiple items being persisted and each getting a txid
682+
const txids = mutations.map(() => Math.floor(Math.random() * 10000))
683+
return Promise.resolve(txids)
684+
},
685+
}
686+
687+
// Create handler that returns array of txids
688+
const onInsert = vi.fn(async (params: MutationFnParams<Row>) => {
689+
const txids = await fakeBackend.persist(params.transaction.mutations)
690+
691+
// Simulate server sending sync messages on different ticks
692+
// In the real world, multiple txids rarely arrive together
693+
setTimeout(() => {
694+
subscriber([
695+
{
696+
key: `1`,
697+
value: { id: 1, name: `Item 1` },
698+
headers: {
699+
operation: `insert`,
700+
txids: [txids[0]!],
701+
},
702+
},
703+
{ headers: { control: `up-to-date` } },
704+
])
705+
}, 1)
706+
707+
setTimeout(() => {
708+
subscriber([
709+
{
710+
key: `2`,
711+
value: { id: 2, name: `Item 2` },
712+
headers: {
713+
operation: `insert`,
714+
txids: [txids[1]!],
715+
},
716+
},
717+
{ headers: { control: `up-to-date` } },
718+
])
719+
}, 2)
720+
721+
// Return array of txids - this is the pattern that's failing
722+
return { txid: txids }
723+
})
724+
725+
const config = {
726+
id: `test-array-txids`,
727+
shapeOptions: {
728+
url: `http://test-url`,
729+
params: { table: `test_table` },
730+
},
731+
startSync: true,
732+
getKey: (item: Row) => item.id as number,
733+
onInsert,
734+
}
735+
736+
const testCollection = createCollection(electricCollectionOptions(config))
737+
738+
// Insert multiple items
739+
const tx = testCollection.insert([
740+
{ id: 1, name: `Item 1` },
741+
{ id: 2, name: `Item 2` },
742+
])
743+
744+
// This should resolve when all txids are seen
745+
await expect(tx.isPersisted.promise).resolves.toBeDefined()
746+
expect(onInsert).toHaveBeenCalled()
747+
748+
// Verify both items were added
749+
expect(testCollection.has(1)).toBe(true)
750+
expect(testCollection.has(2)).toBe(true)
751+
})
752+
675753
it(`should support custom match function using awaitMatch utility`, async () => {
676754
let resolveCustomMatch: () => void
677755
const customMatchPromise = new Promise<void>((resolve) => {

0 commit comments

Comments
 (0)