🐛 Record user undo changes before starting derived component sync

Hand applied transitions to the component watcher only after the workspace
records their undo entry. Keep the handoff synchronous with the captured
prior file data. This preserves transaction order when nesting triggers
several component syncs. Fix F137.

The minimal replay failed both undo and redo before the fix; its 4 checks
now pass. Existing sync scenarios pass their behavior and undo checks;
60 golden digests need review for the accumulated Phase 4 fixes.
Common benchmark ratios against Phase 1e: main 10/100/1000
0.46/0.52/0.53; levels 0.47; chain 0.51; library 0.51; many 0.49;
switch 0.61. This handoff changes no common benchmark code.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-23 14:59:14 +02:00
parent 84affc15c0
commit 67550ebb5c
5 changed files with 36 additions and 15 deletions

View File

@ -19,11 +19,16 @@
- `commit-changes` refuses to create commits unless `:permissions :can-edit` is true. It captures file revn/vern, selected-before, features, tags, undo group, and translation flag into a `::commit` event. A refusal answers with an empty stream, never nil, so callers subscribe uniformly instead of branching on nil.
- Applying a remote commit first rolls back pending local commits, applies the remote changes, then replays pending local redo changes. Index updates are emitted for undo, remote redo, and replayed redo paths.
- Local commits are independently consumed by undo, persistence, WASM model updates, thumbnail/library watchers, and text position-data recalculation.
- Persistence, WASM model updates, thumbnails and text position-data watchers
consume local commits independently. Undo records applied transitions first.
- `dch/changes-applied?` follows local application and carries the commit
with its prior file data. The component watcher syncs each transition
before the next user commit, keeping undo groups adjacent. Its barrier
remains open through the event turn. Foreign file cascades stay in common.
with its prior file data. The workspace records the undo entry, then emits
`dch/changes-recorded?` synchronously with that same transition. Component
sync starts from this handoff, after history records the original edit.
Syncing from `changes-applied?` can record derived commits before the user
commit and reverse the undo/redo order inside a transaction (F137).
The watcher syncs before the next user commit; its barrier remains open
through the event turn. Foreign file cascades stay in common.
- Undo changes carry `:app.main.data.workspace.undo/file-id`, separate from
change payload fields named `:file-id` (e.g. `:set-tokens-source`). Undo
and redo replay consecutive runs per destination in order; old entries

View File

@ -38,6 +38,15 @@
(def changes-applied?
(ptk/type? ::changes-applied))
(def changes-recorded?
(ptk/type? ::changes-recorded))
(defn changes-recorded
"The commit has applied and its undo entry has been recorded. Derived
edits can now follow it without reversing transaction history."
[transition]
(ptk/data-event ::changes-recorded transition))
(defn- fix-page-id
"For events that modifies the page, page-id does not comes
as a property so we assign it from the `id` property."

View File

@ -494,18 +494,22 @@
(rx/take-until stopper-s)))
(->> stream
(rx/filter dch/commit?)
(rx/filter dch/changes-applied?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [file-id save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes (mapv #(assoc % ::dwu/file-id file-id) undo-changes)
:redo-changes (mapv #(assoc % ::dwu/file-id file-id) redo-changes)
:undo-group undo-group
:tags tags
:selected-before selected-before}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(fn [{:keys [commit] :as transition}]
(let [{:keys [file-id save-undo? undo-changes redo-changes undo-group
tags stack-undo? selected-before]} commit]
(rx/concat
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes (mapv #(assoc % ::dwu/file-id file-id) undo-changes)
:redo-changes (mapv #(assoc % ::dwu/file-id file-id) redo-changes)
:undo-group undo-group
:tags tags
:selected-before selected-before}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))
(rx/of (dch/changes-recorded transition))))))))
(rx/take-until stopper-s)))))))
(defn initialize-workspace

View File

@ -1420,7 +1420,7 @@
local-commits-s
(->> stream
(rx/filter dch/changes-applied?)
(rx/filter dch/changes-recorded?)
(rx/map deref)
(rx/filter #(= :local (get-in % [:commit :source])))
;; Keep waits pending while component changes are checked.

View File

@ -86,3 +86,6 @@
(t/deftest ^:async nested-text-rotation-shrunk-regression
(await (replay {:world [2 0 {:text true}]
:ops [[:nest 0] [:detach 0] [:rotate 0]]})))
(t/deftest ^:async nesting-restores-default-constraints-on-undo
(await (replay {:world [2 0] :ops [[:nest 0]]})))