diff --git a/.changeset/improve-txid-timeout-error.md b/.changeset/improve-txid-timeout-error.md new file mode 100644 index 000000000..99506bf1a --- /dev/null +++ b/.changeset/improve-txid-timeout-error.md @@ -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). diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index 994889ded..6d0f08561 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -637,13 +637,33 @@ export function electricCollectionOptions>( 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 = [] + 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`, diff --git a/packages/electric-db-collection/src/errors.ts b/packages/electric-db-collection/src/errors.ts index 14ee6ab50..c8491a624 100644 --- a/packages/electric-db-collection/src/errors.ts +++ b/packages/electric-db-collection/src/errors.ts @@ -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, + ) { + 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): 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)