Skip to content

fix(sync): hold replies to requesting peers until storage has been checked - #763

Open
Tanyayayya wants to merge 2 commits into
automerge:mainfrom
Tanyayayya:fix/defer-requesting-peers-until-storage
Open

Tanyayayya wants to merge 2 commits into
automerge:mainfrom
Tanyayayya:fix/defer-requesting-peers-until-storage

Conversation

@Tanyayayya

@Tanyayayya Tanyayayya commented Sep 14, 2026

Copy link
Copy Markdown

When a peer requests a document the repo does not have in memory yet, #evaluate answers it straight away from the empty doc while the storage source is still loading. The storage-deferral guard only covers peers in the unknown state; a requesting peer is already wants, so it is skipped.

Some clients treat that first empty-heads reply as "unavailable". We hit this with a sync server that creates documents through a separate REST service and loads them from S3 on first request: the Python automerge client got an empty reply, marked the document unavailable, and stalled until its 120 s timeout.

This is the same failure class as #343, reported in 2024 from the Swift client: a premature "unavailable" for a document that then syncs fine. The envelope has changed since (empty-heads sync now, explicit doc-unavailable then) but the cause is the same, answering a requester before storage has been checked. The test here is the one asked for in that thread. Fixes #343.

This applies the existing guard to wants peers too, so the reply waits until storage has settled. Once storage resolves, the deferred peer is still dirty and gets its message on the next evaluate pass.

The deferral is bounded: shouldDeferAvailability only waits on a strictly higher-priority source, so a requester waits for the storage lookup and nothing else. Other peers that are connected but silent do not hold it up. I tried keying on the query being in loading instead; it passes the suite, but a single silent peer would then stall a requester with no bound.

When storage settles empty, the wire order in one evaluate pass is an empty-heads sync followed by doc-unavailable. So a requester can still see an empty sync for a document that does not exist, just not before storage has been checked, and never without the doc-unavailable behind it.

Note on blast radius: a JS peer is unaffected by the empty reply either way. It only promotes a sender to has on non-empty heads, so the empty sync leaves the sender unknown, the sync source stays pending, and the query stays in loading until real data arrives. The change matters for clients that read an initial empty-heads sync as "unavailable", but the guard's own comment already states the principle: don't publish something other peers may read as evidence we don't have the document. An empty-heads sync to a requesting peer is that same evidence in a different envelope.

Full test suite passes (891 tests). Includes a regression test in DocSynchronizer.test.ts: a requesting peer gets no reply while the storage source is pending, and does get one once storage settles. It fails on main and passes with this change.

@Tanyayayya
Tanyayayya marked this pull request as ready for review September 14, 2026 17:43
Tanyayayya and others added 2 commits September 14, 2026 16:05
…ecked

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Tanyayayya
Tanyayayya force-pushed the fix/defer-requesting-peers-until-storage branch from e34f11a to 0718e66 Compare September 14, 2026 20:05
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.

Repo and WebSocket provider send two responses for a "request" - the first unavailable, the second a sync

1 participant