Skip to content

fix(client): don't skip live changes after transaction ID wraparound - #4761

Merged
KyleAMathews merged 3 commits into
electric-sql:mainfrom
neoantox:fix/client-txid-wraparound
Aug 14, 2026
Merged

fix(client): don't skip live changes after transaction ID wraparound#4761
KyleAMathews merged 3 commits into
electric-sql:mainfrom
neoantox:fix/client-txid-wraparound

Conversation

@neoantox

@neoantox neoantox commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes subset snapshot filtering when PostgreSQL 32-bit transaction IDs wrap across an xid8 epoch. Clients now keep live collections current instead of discarding valid changes after wraparound, including on quiet SSE streams that have not received another up-to-date boundary.

Root Cause

Change messages carry 32-bit transaction IDs, while subset snapshot metadata uses 64-bit epoch-aware IDs. Comparing them directly loses the epoch and can classify a new wrapped transaction as visible in an old snapshot.

Reconstructing the epoch also has a half-range constraint: the 32-bit xid and its 64-bit reference must be within 2^31 transactions. A quiet stream could retain a snapshot past that bound because its next change may arrive before another up-to-date message.

Approach

  • Resolve every wire xid into the epoch nearest the snapshot xmax, then use the latest resolved xid for snapshot eviction and duplicate filtering.
  • Advance snapshot retirement in message order from both global_last_seen_lsn control messages and each change message lsn.
  • Expand the model-based tests across xid8 epochs, wrap boundaries, and multi-xid messages.
  • Add regressions for wrapped xid eviction and for a quiet stream whose first later change passes the snapshot WAL position.

This mirrors the xid reconstruction strategy already used by the sync service in #2320.

Key Invariants

  • An active snapshot never outlives the point where the stream WAL position passes its database_lsn.
  • Changes before that boundary still receive snapshot duplicate filtering; later changes do not.
  • Each wire xid is resolved before the latest contributing transaction is chosen.

Non-goals

  • No sync-service or wire-protocol changes.
  • No changes to subset snapshot semantics outside transaction ID resolution and retirement.

Trade-offs

Using xmax as the epoch reference keeps the fix local and matches PostgreSQL wraparound arithmetic. Its half-range requirement is safe because LSN-based retirement bounds each snapshot lifetime without adding protocol state.

Verification

pnpm --dir packages/typescript-client test --run
pnpm --dir packages/typescript-client typecheck
pnpm --dir packages/typescript-client stylecheck

The full TypeScript client unit suite passed (440 tests), along with focused regression tests, type checking, linting, and formatting checks.

Files changed

  • packages/typescript-client/src/client.ts — retires snapshots from control and change LSNs in stream order.
  • packages/typescript-client/src/snapshot-tracker.ts — reconstructs epoch-aware transaction IDs before filtering.
  • packages/typescript-client/test/pbt-micro.test.ts — adds cross-epoch property coverage and stream retirement regression.
  • packages/typescript-client/test/snapshot-tracker.test.ts — adds a wrapped xid retirement regression.
  • .changeset/tidy-xids-wrap.md — records the client patch.

Related

Related: #2320

AI disclosure: Codex helped investigate, test, and prepare this change.

neoantox and others added 2 commits August 13, 2026 19:30
Reconstruct epoch-aware transaction IDs from 32-bit replication headers before subset snapshot filtering and cleanup.

Add an epoch-two regression case and publish the correction as a client patch release.
@pkg-pr-new

pkg-pr-new Bot commented Aug 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@electric-sql/react@4761
npm i https://pkg.pr.new/@electric-sql/client@4761
npm i https://pkg.pr.new/@electric-sql/y-electric@4761

commit: 86696bf

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.48%. Comparing base (8cabe9b) to head (86696bf).

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4761       +/-   ##
===========================================
+ Coverage   60.07%   71.48%   +11.41%     
===========================================
  Files         409       70      -339     
  Lines       44235     8975    -35260     
  Branches    12585     2848     -9737     
===========================================
- Hits        26574     6416    -20158     
+ Misses      17580     2541    -15039     
+ Partials       81       18       -63     
Flag Coverage Δ
electric-telemetry ?
elixir ?
packages/agents ?
packages/agents-mcp ?
packages/agents-mobile ?
packages/agents-runtime ?
packages/agents-server 75.54% <ø> (ø)
packages/agents-server-ui ?
packages/electric-ax 51.06% <ø> (ø)
packages/experimental ?
packages/react-hooks ?
packages/start ?
packages/typescript-client ?
packages/y-electric 56.05% <ø> (ø)
typescript 71.48% <ø> (+11.45%) ⬆️
unit-tests 71.48% <ø> (+11.41%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@KyleAMathews
KyleAMathews merged commit 1983bf1 into electric-sql:main Aug 14, 2026
28 of 32 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

This PR has been released! 🚀

The following packages include changes from this PR:

  • @electric-sql/client@1.5.26

Thanks for contributing to Electric!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants