Repository navigation
Replay a sync delta on the local history before asking the server (#1905) - #1920
michielbdejong wants to merge 2 commits into
Conversation
) A local query returns JSON-AD without snapshots, so the resources it hydrates get a Loro doc rebuilt from values: new ops by a new peer, none of the history. Two things followed. - `addResource` wrote that doc back to the local database, replacing the history the query had just read its values from. - The drive sync after a reload sends deltas from the version vectors the local database reported. None of them can apply to a rebuilt doc, so each parked as an incomplete import and waited on a full snapshot from the server. In the atomic-plugins money e2e the app a table offers sat under that repair past the ten seconds the test waited for it in "+ Add view" (#1846). Now the store records which docs were rebuilt without history, does not write them back, and replays an unappliable delta on the local database's snapshot, which is by construction the base the server diffed against. The server is asked only when that fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 36636664834 on candidate17b failed the delete resource @smoke e2e with an unexpected browser warning: [Store] failed to recover missing state for <child>: Resource not found After the reload, a sync delta for the nested folder parks as incomplete and the store repairs it. With #1920 the repair first waits on the local database, then asks the server; by then the test has deleted the parent, the server cascaded to the child, and the server's not-found is correct. The store warned and called failResource, which puts an errored copy of a deleted resource back. Locally: 1 of 3 runs failed with #1920, 0 of 6 without it, 0 of 6 with this fix. The repair now checks whether the subject was destroyed (or has a destroy in flight): it skips the server fetch then, and a failure after the delete is logged as info, without failResource. Unit tests in sync-delta-local-base.test.ts cover both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 36636664834 on candidate17b failed the delete resource @smoke e2e with an unexpected browser warning: [Store] failed to recover missing state for <child>: Resource not found After the reload, a sync delta for the nested folder parks as incomplete and the store repairs it. With #1920 the repair first waits on the local database, then asks the server; by then the test has deleted the parent, the server cascaded to the child, and the server's not-found is correct. The store warned and called failResource, which puts an errored copy of a deleted resource back. Locally: 1 of 3 runs failed with #1920, 0 of 6 without it, 0 of 6 with this fix. The repair now checks whether the subject was destroyed (or has a destroy in flight): it skips the server fetch then, and a failure after the delete is logged as info, without failResource. Unit tests in sync-delta-local-base.test.ts cover both. (cherry picked from commit 06c6e1b) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed ef90a13 ("Store: a resource deleted during gap recovery is not a failure"). It's cherry-picked from candidate17b (06c6e1b) and applied cleanly on this branch. Why: with this PR on candidate17b, the The Playwright trace shows this sequence:
Before this PR, the repair went to the server straight away and won the race. Waiting on the local database first widened the window. Locally, the test failed 1 run in 3 with this PR, and passed 6 of 6 without it. Fix:
Two unit tests cover it in |
|
@michielbdejong CI run 4835 on ef90a13 (Mancave, full e2e) is red on two tests that develop (run 4829) passes: Generated by Claude Code |
One tested commit for the open PRs of 5 October: link device to account (#1948), one profile form (#1953), faster branch runs (#1956), end-to-end encrypted conversations (#1971), passkey ceremony header (#1972), Flutter account package (#2035), cheap canvas undo (#2053), atomic.place tab title (#2056), table resource cell (#2057), small table fixes (#2058), storage map (#2061), Sync page cloud ladder (#2072), several served-domain suffixes (#2073), one sign-in (#2074), smaller chat messages (#2077), bring-along e2e fix (#2078), inline row data loss (#2079), Retry-After (#2080), #2070, #2071, #2066, #2067, #2068, #1969, #1887, #1897 and #1920. Compressed stored values from #2077 stay readable, but a downgrade cannot read compressed rows. Co-authored-by: Joep Meindertsma <joep@ontola.io>
|
Merged into develop through #2085 (batch of 5 October), so this PR is closed. Your commits are in the squashed history. Generated by Claude Code |
Closes #1905. Follow-up to #1846 / #1904: this is why "+ Add view" still lacked the app in the atomic-plugins money e2e after #1904.
Root cause
A local query returns JSON-AD, not snapshots. So every resource it hydrates (the table page's app query, the sidebar, and so on) gets a Loro doc rebuilt from values. That doc has new ops, by a new peer, and none of the history. Two things went wrong from there:
addResourcewrote that rebuilt doc back to the local database, replacing the real history the query had just read the values from.putResourceWithSnapshotstores the snapshot as given. The new lib test shows the write: a history-less, single-peer snapshot.incomplete Loro import … missing base stateand waited on a full snapshot from the server, with the resource marked as recovering.Evidence from the traces (atomic-plugins run 36616868387, candidate16 3830675 with #1904)
ws-sync-pushimports come in incomplete. They are the app's entry point (HTGzQ…), its agent, its own row class (HVD4W…), its ontology (QQ-gR…), and one more (KIpnk…). These are exactly the resources the previous page created or edited.failed to recoverwarning appears, and "New app" never appears in the menu. This happened on the first try and again on the retry.Ruled out
ResourceUpdatedfires and the hook folds the app in.DropdownMenufilters itsitemsprop on every render.I could not see why the server repair was slow in CI, because Playwright traces don't record WebSocket frames. This PR takes that round trip off the path instead of tuning it.
Fix (
browser/lib/src/store.ts)_withoutLoroHistory). It sets this inhydrateOfflineReplaywhen there is no snapshot, and clears it when a full snapshot arrives from the server, when an HTTP fetch returns, or when the resource is evicted.writeToClientDbdoes not write such a doc back to the local database unless it carries an unsaved edit. The local history survives.recoverFromIncompleteImportfirst triesreplayOnLocalBase:Tests
browser/lib/src/sync-delta-local-base.test.ts, 5 cases:4 of the 5 fail on develop.
browser/data-browser/src/chunks/AppPage/useDriveApps.localBase.test.tsxcovers the exact order from the traces:It fails on develop's hook and on Ask the drive for its apps again when a view menu opens (#1846) #1904's hook without this fix, and passes with it on both.
Lib vitest: 1085 passed. data-browser vitest: 1450 passed. Lib typecheck and lint are clean. data-browser
tscreports one error inAICreditsService.tsx, which this PR doesn't touch.Cherry-pick onto candidate16 / candidate17
Three conflicts in
store.tsneed the same hand edits on both. The candidates predatewriteToClientDb,isPersistableState,lastPersistedandoutbox.mayHavePending:hydrateOfflineReplay, keep the candidate's): Resource {return type and addconst existing = this.getResolved(subject);.addResource's inline persist condition, append&& !(this._withoutLoroHistory.has(this.normalizeSubject(emitResource.subject)) && !emitResource.hasUnsavedChanges()).evictResource, keepthis.lastPersistedStamp.delete(resolved);and addthis._withoutLoroHistory.delete(resolved);.replayOnLocalBase,this.outbox.mayHavePendingbecomesthis.outbox.hasPending.With those edits, on both candidates the lib vitest passes (1156 on candidate16 and 1170 on candidate17), and so do AppPage and TablePage, including the new hook test.
TableViewTabs.tsxis not touched.Not covered
Editing a resource that is still without its history saves from the rebuilt doc, as before.
TESTING_COVERAGE.mdnotes this. Loading the local snapshot before the first edit would close that gap and belongs in its own change.🤖 Generated with Claude Code