refactor(bindx): rebuild undo/redo as a write-journal over the decomposed store#50
Open
matej21 wants to merge 1 commit into
Open
refactor(bindx): rebuild undo/redo as a write-journal over the decomposed store#50matej21 wants to merge 1 commit into
matej21 wants to merge 1 commit into
Conversation
…osed store Replaces the static-projection snapshot-restore undo (getAffectedKeys computed before execution) with a write-journal keyed by what each gesture actually writes. Fixes three root-cause defects: created entities in a list weren't captured (lost on undo->sweep->redo), root registration wasn't captured (phantom/lost creates), and undo didn't survive a temp->persisted rekey (stale-key corruption). Core: - UndoJournal records each gesture (one dispatch / one handle transaction) as a JournalEntry of editable-layer pre-images, first-writer-wins per cell. - SnapshotStore implements JournalTarget (exportEntityCell/exportRelationCell/ exportHasManyCell + applyJournalImages) and gains beginTransaction/ commitTransaction/transaction(); mutating methods record before writing. - ActionDispatcher.dispatch and the pre-create handle gestures (HasManyListHandle.add, HasOneHandle.create) open transactions so one gesture = one undo unit. - Restore splices only the editable layer onto the LIVE server baseline, so undoing a persisted edit re-dirties against the current baseline. - Persist survival: the journal rekeys stacked entries (keys + embedded ids), seals now-persisted creates, and rebases has-many membership so a persisted child stays in the list under both default and explicit ordering. - UndoManager becomes a thin policy layer (debounce/manual grouping, block during persist, rekey-of-stacks); the createMiddleware() API is preserved. - Removes the dead actionClassification static projection. Tests: 36 green. Original undo.test.ts unchanged, plus undo-stabilization.test.ts (3 characterization bugs + scale + handle gesture) and undo-journal.test.ts (seal incl. the create-across-save falsification, rekey embedded-id, has-many move/disconnect/delete, multi-cell atomic, redo-after-persist, absent-relation restore, edge cases). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbKRVbhqE2N3uTi9mbaWb6
6cb7d77 to
52bcf43
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The existing undo/redo worked but stood on a fragile mechanism that diverged from the post-decomposition store. Capture was driven by a static projection of the action computed before execution (
getAffectedKeys), with three confirmed root-cause defects:RootRegistrywas never captured. A top-level created entity's "pending create" status is anchored solely by its root membership → phantom/lost creates after undo.What
Rebuilds undo as a write-journal keyed by what each gesture actually writes. The key invariant exploited: derived state (edge index, reachability cache, idIndex) is a pure function of primary state, rebuilt through the write chokepoints — so undo only restores primary state and the rest follows. No event/interceptor replay.
UndoJournalrecords each gesture (one dispatch / one handle transaction) as aJournalEntryof editable-layer pre-images, first-writer-wins per cell.SnapshotStoreimplementsJournalTarget(exportEntityCell/exportRelationCell/exportHasManyCell+applyJournalImages) and gainsbeginTransaction/commitTransaction/transaction(); mutating methods record before writing.ActionDispatcher.dispatchand the pre-create handle gestures (HasManyListHandle.add,HasOneHandle.create) open transactions → one gesture = one undo unit.UndoManagerbecomes a thin policy layer (debounce/manual grouping, block during persist, rekey-of-stacks); thecreateMiddleware()API is preserved.actionClassificationstatic projection.Tests — 36 green
tests/undo.test.ts— original suite, unchanged assertions.tests/undo-stabilization.test.ts— the 3 characterization bugs (written failing-first), plus a scale guard (entry is O(edit), not O(store)) and the handle-gesture round-trip.tests/undo-journal.test.ts— deep coverage: seal across persist incl. the create-across-save falsification (create C under saved P + edit sibling S in one group → undo reverts S, keeps C) and the explicit-ordering edge, rekey embedded-id remap (has-many + has-one), has-many move/disconnect/delete undo, multi-cell atomic gesture, redo-after-persist, absent-relation restore, nested field, schedule-delete, group first-writer-wins.Writing the deep tests revealed and fixed a real bug: explicit-ordering create-across-save dropped the persisted child on undo → fixed with the membership rebase (
SnapshotStore.getLiveHasManyServerIds).Validation
@contember/bindxcore + all consumer packages (react/form/dataview/ui) typecheck clean.Targets
refactor/relationstore-decompositionsince it builds on that decomposition.🤖 Generated with Claude Code