Skip to content

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

Open
alco wants to merge 4 commits into
mainfrom
alco/preserve-replayed-shape-data
Open

Preserve data messages during replay suppression in the TypeScript client#4782
alco wants to merge 4 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: 4a2ab2f

@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 73.71%. Comparing base (dc07a1e) to head (4a2ab2f).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4782       +/-   ##
===========================================
+ Coverage   60.02%   73.71%   +13.69%     
===========================================
  Files         397       88      -309     
  Lines       43772    10094    -33678     
  Branches    12588     3105     -9483     
===========================================
- Hits        26272     7441    -18831     
+ Misses      17418     2595    -14823     
+ Partials       82       58       -24     
Flag Coverage Δ
packages/agents ?
packages/agents-mcp ?
packages/agents-mobile ?
packages/agents-runtime ?
packages/agents-server 75.65% <ø> (+0.17%) ⬆️
packages/agents-server-ui ?
packages/electric-ax 51.06% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start ?
packages/typescript-client 91.83% <100.00%> (ø)
packages/y-electric 56.05% <ø> (ø)
typescript 73.71% <100.00%> (+13.69%) ⬆️
unit-tests 73.71% <100.00%> (+13.69%) ⬆️

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".

// A replayed up-to-date for the previous session's cursor still
// carries real WAL progress, which is applied above; only its
// delivery to subscribers is skipped.
if (transition.suppressUpToDate) return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid publishing an empty suppressed replay batch

When a proxy or custom endpoint triggers replay suppression with a batch containing only the duplicate up-to-date message, this filter leaves messagesToProcess empty but #publish still invokes every subscriber with []. Previously suppressBatch returned without notifying, and the public subscription documentation promises batches containing one or more messages; consumers that inspect the first or last message can therefore fail. Return without publishing when replay suppression specifically leaves no messages, while retaining the existing empty-batch behavior for snapshot-tracker filtering.

Useful? React with 👍 / 👎.

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