Background
Polly #104 closed against the fragmentation + observability + yield-only scope. That fix removed the silent-drop wedge (werift's 64 KiB cap dropping every 65 630-byte fragment) and added the inFlightSync snapshot field, the sync-progress event, and the receiver-side macrotask yield. The contract the ticket asked for is partially held:
- Sentinel propagates end-to-end on a throttled in-process transport with a 5.5 MB seeded snapshot.
inFlightSync is observable mid-run.
- Most of the receive-side sync time is spent off the wire-callback stack frame.
What is NOT held: the strict <100ms event-loop tick-gap bar during the bulk-apply step. The single applyChanges call for a multi-megabyte initial sync is one synchronous Automerge call. The existing setTimeout(0) yield moves dispatch off the wire frame but cannot split that call. The harness in examples/mesh-large-initial-sync/ reports a residual ~300 ms tick spike for a 5.5 MB snapshot and the example's NOTE line documents this.
Burden of proof
Same shape as #104. This ticket closes only when polly demonstrates with a captured run that:
- The harness exercises the failing-shape diff (≥ 5 MB compacted snapshot, throttled transport, tick-gap probe running before sync starts).
- The probe records NO tick > 100 ms during the sync window.
- The sentinel still propagates end-to-end.
- A falsification path (pre-fix-emulated mode) reproduces the residual spike and exits non-zero with a named reason that points at the apply step, not at a generic timeout.
Two candidate approaches
A. Receive-side change-batching apply
Intercept reassembled sync messages, decode the embedded changes array, apply them in batches of N changes with setTimeout yields between batches. Re-emit the synchroniser's outbound sync message only after the full chain has applied so the sender doesn't see partial state.
Risk: Automerge's sync state machine isn't designed to be paused mid-apply. The decode path is implementation-private; relying on it pins polly to an Automerge version. The synchroniser's own state-tracking would need to match the slow apply to avoid sending stale have heads while we're still applying.
B. Web Worker apply
Run the Automerge Repo in a Worker. The receive-side mesh adapter forwards inbound sync messages to the worker via postMessage; the worker applies them and posts state deltas back. The main thread never sees the multi-megabyte apply call.
Risk / scope: storage adapter bridging (the worker needs to own its own storage subsystem, or proxy through postMessage), state subscription bridging ($meshState consumers expect synchronous reads of the latest doc state), and the document handle lifecycle (whenReady, change) all need worker-aware variants. Tests and examples need to be redone for the worker target. Multi-week scope.
Approach (B) is more invasive but more durable; (A) is less invasive but more fragile against Automerge upstream churn.
What I will not accept as proof
Same exclusions as #104. In particular: "all unit tests pass" does not imply the contract is held; the load-bearing signal is the tick-gap probe running against the failing-shape diff.
Out of scope
Tracking
Follow-up to polly#104. The harness examples/mesh-large-initial-sync/ is the proof surface; the residual tick-gap spike it reports under the post-fix mode is the failure this ticket closes.
Background
Polly #104 closed against the fragmentation + observability + yield-only scope. That fix removed the silent-drop wedge (werift's 64 KiB cap dropping every 65 630-byte fragment) and added the
inFlightSyncsnapshot field, thesync-progressevent, and the receiver-side macrotask yield. The contract the ticket asked for is partially held:inFlightSyncis observable mid-run.What is NOT held: the strict
<100msevent-loop tick-gap bar during the bulk-apply step. The singleapplyChangescall for a multi-megabyte initial sync is one synchronous Automerge call. The existingsetTimeout(0)yield moves dispatch off the wire frame but cannot split that call. The harness inexamples/mesh-large-initial-sync/reports a residual ~300 ms tick spike for a 5.5 MB snapshot and the example'sNOTEline documents this.Burden of proof
Same shape as #104. This ticket closes only when polly demonstrates with a captured run that:
Two candidate approaches
A. Receive-side change-batching apply
Intercept reassembled sync messages, decode the embedded
changesarray, apply them in batches of N changes withsetTimeoutyields between batches. Re-emit the synchroniser's outbound sync message only after the full chain has applied so the sender doesn't see partial state.Risk: Automerge's sync state machine isn't designed to be paused mid-apply. The decode path is implementation-private; relying on it pins polly to an Automerge version. The synchroniser's own state-tracking would need to match the slow apply to avoid sending stale
haveheads while we're still applying.B. Web Worker apply
Run the Automerge
Repoin aWorker. The receive-side mesh adapter forwards inbound sync messages to the worker viapostMessage; the worker applies them and posts state deltas back. The main thread never sees the multi-megabyte apply call.Risk / scope: storage adapter bridging (the worker needs to own its own storage subsystem, or proxy through
postMessage), state subscription bridging ($meshStateconsumers expect synchronous reads of the latest doc state), and the document handle lifecycle (whenReady,change) all need worker-aware variants. Tests and examples need to be redone for the worker target. Multi-week scope.Approach (B) is more invasive but more durable; (A) is less invasive but more fragile against Automerge upstream churn.
What I will not accept as proof
Same exclusions as #104. In particular: "all unit tests pass" does not imply the contract is held; the load-bearing signal is the tick-gap probe running against the failing-shape diff.
Out of scope
Tracking
Follow-up to polly#104. The harness
examples/mesh-large-initial-sync/is the proof surface; the residual tick-gap spike it reports under the post-fix mode is the failure this ticket closes.