Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/improve-txid-timeout-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/electric-db-collection': patch
---

Improve awaitTxId timeout error with debugging info showing which txids were received during the timeout period, plus a hint about the common cause (calling pg_current_xact_id outside the mutation transaction).
22 changes: 21 additions & 1 deletion packages/electric-db-collection/src/electric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,13 +637,33 @@ export function electricCollectionOptions<T extends Row<unknown>>(
if (hasSnapshot) return true

return new Promise((resolve, reject) => {
// Track txids at start to know which ones arrived during timeout
const txidsAtStart = new Set(seenTxids.state)
const txidsReceivedDuringTimeout: Array<Txid> = []

const timeoutId = setTimeout(() => {
unsubscribeSeenTxids()
unsubscribeSeenSnapshots()
reject(new TimeoutWaitingForTxIdError(txId, config.id))
reject(
new TimeoutWaitingForTxIdError(
txId,
config.id,
txidsReceivedDuringTimeout,
),
)
}, timeout)

const unsubscribeSeenTxids = seenTxids.subscribe(() => {
// Track new txids that arrived during the timeout period
for (const seenTxid of seenTxids.state) {
if (
!txidsAtStart.has(seenTxid) &&
!txidsReceivedDuringTimeout.includes(seenTxid)
) {
txidsReceivedDuringTimeout.push(seenTxid)
}
}

if (seenTxids.state.has(txId)) {
debug(
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
Expand Down
24 changes: 22 additions & 2 deletions packages/electric-db-collection/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,32 @@ export class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
}

export class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
constructor(txId: number, collectionId?: string) {
super(`Timeout waiting for txId: ${txId}`, collectionId)
constructor(
txId: number,
collectionId?: string,
receivedTxids?: Array<number>,
) {
const receivedInfo = formatReceivedTxidsInfo(receivedTxids)
const hint = `\n\nThis often happens when pg_current_xact_id() is called outside the transaction that performs the mutation. Make sure to call it INSIDE the same transaction. See: https://tanstack.com/db/latest/docs/collections/electric-collection#common-issue-awaittxid-stalls-or-times-out`

super(
`Timeout waiting for txId: ${txId}${receivedInfo}${hint}`,
collectionId,
)
this.name = `TimeoutWaitingForTxIdError`
}
}

function formatReceivedTxidsInfo(receivedTxids?: Array<number>): string {
if (receivedTxids === undefined) {
return ``
}
if (receivedTxids.length === 0) {
return `\nNo txids were received during the timeout period.`
}
return `\nTxids received during timeout: [${receivedTxids.join(`, `)}]`
}

export class TimeoutWaitingForMatchError extends ElectricDBCollectionError {
constructor(collectionId?: string) {
super(`Timeout waiting for custom match function`, collectionId)
Expand Down
Loading