fix: flush mutations one acknowledged batch at a time for hosts that echo patches - #3277
christianhg wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: 8fa70fe The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bundle Stats✅ No significant changes. All scenario measurements (7)🗺️
Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 869e188. Configure here.
…wledged by host echoes The mutation pipeline was fire-and-forget: the batcher emitted `mutation` events and nothing recorded whether the host ever applied them. The evidence that it did was already arriving and being thrown away: hosts wired to a document stream echo every applied patch back into the `patches` event, tagging this editor's own as `origin: 'local'`, and `setupRemotePatches` filtered those out unread. `createMutationLedger` records each flushed bulk's patches at the moment the batcher emits the `mutation`, and the remote-patches subscriber now routes `origin: 'local'` echoes into `ledger.acknowledge` before filtering them out of the apply path as before. No transaction identity crosses the `mutation` boundary, so an echo acknowledges the earliest recorded patch it structurally equals, ignoring `origin`, and drops every record older than the match: hosts echo in application order, so older echoes either already arrived or are never coming (keeping them would otherwise stall the first gated flush of a host that starts echoing mid-session). Unmatched echoes (emitted before this session, or reshaped by the host in transit) are reported unmatched and change nothing. The backlog is capped at 500 records, oldest dropped first: a snapshot-only host never drains it, and only recent emissions can prove an echo channel exists. `unacknowledged()` exposes the backlog, oldest first, via `editorEngine.mutationLedger`. Net behavior unchanged: the ledger only observes. Gating pending flushes and superseded-repair decisions on the unacknowledged backlog is layered on separately.
…echo patches The batcher flushed every pending bulk on each cadence tick regardless of whether the host had applied the previous flush, so consecutive mutations could interleave with inbound state computed before their predecessors landed. The `mutationLedger` now gates the flush: once a host has proven it echoes the editor's own patches back (the first successful ledger acknowledgment, and never before, so snapshot-only hosts keep the fire-and-forget cadence untouched), the next batch holds until every patch of the in-flight one is echoed back. The drain flushes eagerly via the ledger's `onAcknowledge` hook; a backlog older than `ACK_TIMEOUT` (5s, 3s in test mode) is cleared and the gate degrades to send-anyway, so a patch dropped or rewritten in transit slows saving down rather than stopping it. The unsubscribe flush ignores the gate: there is no later tick to deliver on, so a gated batch is sent rather than lost. Activation is deliberately evidence-based: a structural match of an emitted patch is proof the echo loop works end-to-end, so a host that rewrites patch shapes in transit never activates the gate and loses nothing. Deferred-while-read-only patch events keep flushing ahead of the gate check; only `mutation` delivery is paced.
fbb7480 to
8fa70fe
Compare

When a host feeds the editor's own patches back through the
patchesevent (taggedorigin: 'local', the way a document patch stream does), the editor now treats those echoes as delivery confirmations: the nextmutationevent waits until every patch of the previous one has come back. A batch whose echoes never arrive is sent anyway after 5 seconds, with a console warning naming the unacknowledged patches, so a broken echo loop slows saving down and shows up in the console rather than stopping saves silently. Unmounting delivers a waiting batch immediately. Hosts that never feed patches back are unaffected and keep the existing cadence.The pipeline was fire-and-forget: the batcher flushed on a cadence and nothing ever learned whether the host applied a flush, so every downstream decision about pending work was a guess. The evidence was already arriving and being discarded, since the remote-patches subscriber filtered
origin: 'local'echoes out unread. The first commit records every flushed batch's patches in an in-flight ledger and routes those echoes into it as acknowledgments. Matching is structural because no transaction identity crosses themutationboundary, and it assumes hosts echo in application order: an acknowledgment also drops every older record, so a host that starts echoing mid-session activates cleanly instead of stalling its first gated flush on pre-echo backlog, and the backlog is capped at 500 records so a never-echoing host cannot grow it for the life of the session. The second commit gates the flush on the ledger: one batch in flight at a time, released eagerly the moment the backlog drains.Activation is evidence-based and one-way: the gate turns on at the first successful acknowledgment, which proves the echo loop works end-to-end, so a snapshot-only host, or one that rewrites patch shapes in transit, never activates it and keeps today's behavior (pinned by the echo-less cadence test). Only
mutationdelivery is paced: deferred patch events relay ahead of the gate, pinned by a read-only-then-gated scenario. The other gated states each carry their own pin: held while unacknowledged with eager release on the drain, send-anyway after the timeout with the warning and the stale ledger records dropped, mid-session activation without a stall, and delivery on unsubscribe since no later tick exists. An end-to-end test drives an echoing host through the public surface: type, echo, type, withhold the echo, observe the hold, echo, observe the release.This is the first slice of moving mutation delivery from heuristics to acknowledgments; gating decisions about superseded repairs onto the same ledger is the follow-up, and reconciling with #3246's read-only hold happens when that branch rebases.