Skip to content

Preserve data messages during replay suppression in the TypeScript client - #4782

Merged
alco merged 5 commits into
mainfrom
alco/preserve-replayed-shape-data
Sep 1, 2026
Merged

alco merged 5 commits into
mainfrom
alco/preserve-replayed-shape-data

Conversation

@alco

@alco alco commented Aug 27, 2026

Copy link
Copy Markdown
Member

Supersedes #4723 by @joshdchang, rebased on main with follow-up commits. Josh's original commit is preserved as-is.

Problem

When a fresh ShapeStream enters replay mode (the up-to-date tracker holds a recent cursor for the shape) and the first up-to-date it sees carries that same cursor, ReplayingState requested suppression of the whole batch: #onMessages returned before publishing anything. A batch of [insert, …, up-to-date] therefore lost its rows while the stream's offset still advanced past them.

Fix

  • Represent the transition as suppressUpToDate instead of suppressBatch. The snapshot-tracker filtering runs as usual and only the duplicate up-to-date control message is removed before notifying subscribers.
  • Apply the global_last_seen_lsn bookkeeping for a suppressed up-to-date before dropping it, so suppression affects delivery to subscribers only, not snapshot retirement.
  • Document the invariant as C9 in the client SPEC.md.

The original PR also skipped the subscriber callback when the filtered batch was empty. That applied outside replay mode as well (a batch whose changes were all rejected by the snapshot tracker previously published an empty array), so it is not included here; the pre-existing behaviour is kept.

Reachability

The suppression branch cannot fire against a conforming server: electric-cursor is only set on live=true responses, the client only sends live=true from LiveState, and ReplayingState is only entered around a non-live request. The first up-to-date a replaying stream sees therefore never carries a cursor. The state-machine semantics were still wrong and are fixed here; the C9 test and spec entry are annotated as pinning the invariant with a synthetic cursor header rather than describing a server scenario. Whether the replay mechanism should exist at all is left for a follow-up.

Fixes #4722.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PCuaAMyknYeVkfWMkyGuuS

joshdchang and others added 4 commits August 27, 2026 12:26
…-date

A replayed up-to-date that is suppressed during replay mode still carries
a real global_last_seen_lsn. Run the snapshot tracker's lastSeenUpdate for
it before filtering it out, so suppression only affects delivery to
subscribers and not snapshot retirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCuaAMyknYeVkfWMkyGuuS
The server only sets electric-cursor on live responses and the client is
never in ReplayingState for a live request, so the cursor match that
triggers suppression cannot occur against a conforming server. Mark the
C9 test and spec entry as pinning the state-machine invariant rather than
describing a server scenario.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCuaAMyknYeVkfWMkyGuuS
Drop the early return on an empty filtered batch. It skipped the
subscriber callback not only for a suppressed replay marker but also for
any batch whose changes were all rejected by the snapshot tracker, which
previously published an empty array. Restore the pre-existing behaviour
so this change is limited to the replay path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCuaAMyknYeVkfWMkyGuuS
@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

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

commit: f3ace85

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.07%. Comparing base (dc07a1e) to head (f3ace85).
⚠️ Report is 5 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4782      +/-   ##
==========================================
+ Coverage   60.02%   60.07%   +0.05%     
==========================================
  Files         397      397              
  Lines       43772    43772              
  Branches    12588    12591       +3     
==========================================
+ Hits        26272    26294      +22     
+ Misses      17418    17397      -21     
+ Partials       82       81       -1     
Flag Coverage Δ
packages/agents 72.77% <ø> (+0.12%) ⬆️
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.74% <ø> (+0.02%) ⬆️
packages/agents-server 75.67% <ø> (+0.20%) ⬆️
packages/agents-server-ui 8.32% <ø> (ø)
packages/electric-ax 51.06% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.95% <100.00%> (+0.11%) ⬆️
packages/y-electric 56.05% <ø> (ø)
typescript 60.07% <100.00%> (+0.05%) ⬆️
unit-tests 60.07% <100.00%> (+0.05%) ⬆️

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a2ab2f380

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/typescript-client/src/client.ts
When a replayed batch carries nothing but the duplicate up-to-date, the
suppression filter leaves it empty and #publish still invoked every
subscriber with []. Subscribers are promised one or more messages per
notification, and the pre-suppression code never notified for a
suppressed replay, so return early in that case.

The early return is gated on transition.suppressUpToDate so that batches
emptied by the snapshot tracker alone keep their pre-existing behaviour
of publishing an empty array.

Addresses the Codex review on PR #4782.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qn6oVFdmDPXtTfioKPjHua
@alco
alco merged commit c4c48c0 into main Sep 1, 2026
63 of 66 checks passed
@alco
alco deleted the alco/preserve-replayed-shape-data branch September 1, 2026 09:33
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This PR has been released! 🚀

The following packages include changes from this PR:

  • @electric-sql/client@1.5.27

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.

ShapeStream replay suppression can discard data messages

2 participants