refactor(bindx): rebuild undo/redo as a write-journal over the decomposed store - #50
Merged
Conversation
matej21
force-pushed
the
feat/undo-write-journal
branch
from
June 24, 2026 11:44
6cb7d77 to
52bcf43
Compare
matej21
force-pushed
the
feat/undo-write-journal
branch
from
July 1, 2026 14:30
52bcf43 to
3abef4b
Compare
matej21
force-pushed
the
refactor/relationstore-decomposition
branch
from
July 2, 2026 13:33
f019160 to
02d7a79
Compare
…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
A gesture that detaches a created (never-persisted) child writes only the parent's relation/has-many cell, so the journal entry carried just that cell. sweepUnreachableCreated() — run post-persist and via the unmount cleanup, outside any journal transaction — then reclaimed the child's snapshot and owned relation state, and a later undo restored membership pointing at an entity that no longer existed: dangling reference, lost unsaved data. Entry-closure invariant: on commit each entry is now folded over the created, currently-unreachable subgraph its relation/has-many pre-images reference (entity image + owned relation cells, transitively through nested creates), gated by the exact sweep predicate. Undo entries are self-contained no matter what reclaims memory in between; reachable created siblings stay out, keeping entries O(edit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsR5r5KJFrV63yFCZtSQN4
Nothing wired SnapshotStore.clear() to the undo system, so after a full store wipe (logout / teardown / schema switch) the stacks kept pre-images of the wiped world — undo would resurrect stale entities into an empty store and canUndo misreported. Latent today (only tests call clear()), but a real hole the moment a teardown path uses it. Mirrors the existing rekey forwarding: store → journal.clear() → onClear → UndoManager.clear(). A mid-gesture clear drops the open transaction's recorded cells while keeping begin/commit depth paired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsR5r5KJFrV63yFCZtSQN4
…ters The "record before writing" convention behind the write-journal had no enforcement: a future mutating SnapshotStore method that forgets its journal.record* call silently corrupts undo. Sub-stores now bump a cheap editable-write counter at their write funnels (a different layer than the record calls, so a forgotten record still trips it); at each outermost transaction close the journal compares per-kind deltas against the kinds of recorded cells and throws UnrecordedWriteError naming the missing kind. Server ingestion, baseline commits, undo restore imports, rekey, sweep and clear() are classified as legitimately unjournaled and do not count. Always-on: integer bumps plus an O(1) comparison per gesture, verified false-positive-free across the full suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsR5r5KJFrV63yFCZtSQN4
matej21
force-pushed
the
feat/undo-write-journal
branch
from
August 19, 2026 09:44
698ef62 to
8d26833
Compare
matej21
changed the base branch from
refactor/relationstore-decomposition
to
main
August 19, 2026 09:44
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