fix: stabilize undo/redo raw tx replay order

This commit is contained in:
Tienson Qin
2026-04-12 15:55:52 +08:00
parent 1736743824
commit c9288f2077
3 changed files with 84 additions and 7 deletions

View File

@@ -113,7 +113,7 @@
#{e block-uuid (str block-uuid)})
#{e}))
(defn- reorder-retract-entity
(defn reorder-retract-entity
[tx-data]
(let [retract-ops (filter retract-entity-op? tx-data)
{recreated-block-retract-ops true
@@ -122,7 +122,8 @@
(boolean
(some (fn [x]
(and
(= 5 (count x))
(vector? x)
(>= (count x) 4)
(= (first x) :db/add)
(= (nth x 2) :block/uuid)
(= (nth x 3) id))) tx-data)))))
@@ -132,7 +133,8 @@
set)
datom-for-retracted-eid?
(fn [item]
(and (= 5 (count item))
(and (vector? item)
(>= (count item) 4)
(contains? retract-keys (second item))))
datoms-for-retracted-eids (filter datom-for-retracted-eid? tx-data)
others (remove (fn [item]

View File

@@ -104,7 +104,8 @@
(let [reversed-datom (d/datom e a v t (not added))]
;; trick: reverse the order of `db-before` and `db-after`
(db-normalize/normalize-datom db-before db-after reversed-datom))))
(db-normalize/replace-attr-retract-with-retract-entity-v2 db-after)))
(db-normalize/replace-attr-retract-with-retract-entity-v2 db-after)
db-normalize/reorder-retract-entity))
(defn normalize-rebased-pending-tx
[{:keys [db-before db-after tx-data]}]
@@ -190,7 +191,8 @@
(if (and (vector? item) (= 5 (count item)))
(let [[op e a v _t] item]
[op e a v])
item)))))
item)))
db-normalize/reorder-retract-entity))
(defn- inferred-outliner-ops?
[tx-meta]
@@ -307,7 +309,8 @@
(contains? op-construct/semantic-outliner-ops
(first op-entry)))))
seq)
tx-data (if undo? reversed-tx tx)
tx-data (-> (if undo? reversed-tx tx)
normalize-tx-data-for-rebase)
ops' (if (seq ops)
ops
[[:transact [tx-data nil]]])
@@ -425,7 +428,8 @@
(defn- reverse-history-action!
[conn local-tx]
(if-let [tx-data (seq (:reversed-tx local-tx))]
(ldb/transact! conn tx-data
(ldb/transact! conn
(normalize-tx-data-for-rebase tx-data)
{:outliner-op (:outliner-op local-tx)
:reverse? true})
(invalid-rebase-op! :reverse-history-action

View File

@@ -159,6 +159,43 @@
(second %))
redo-op)))
(defn- move-retract-entity-ops-to-front
[tx-data]
(let [retract-entity-op? (fn [item]
(and (vector? item)
(= 2 (count item))
(= :db/retractEntity (first item))))
retract-ops (filter retract-entity-op? tx-data)
others (remove retract-entity-op? tx-data)]
(vec (concat retract-ops others))))
(defn- poison-history-tx-order!
[tx-id]
(when-let [entry (client-op/get-local-tx-entry test-repo tx-id)]
(client-op/upsert-local-tx-entry!
test-repo
{:tx-id tx-id
:pending? true
:failed? false
:outliner-op (:outliner-op entry)
:undo-redo (:db-sync/undo-redo entry)
:forward-outliner-ops (:forward-outliner-ops entry)
:inverse-outliner-ops (:inverse-outliner-ops entry)
:inferred-outliner-ops? (:inferred-outliner-ops? entry)
:normalized-tx-data (move-retract-entity-ops-to-front (:tx entry))
:reversed-tx-data (move-retract-entity-ops-to-front (:reversed-tx entry))})))
(defn- property-value-titles
[value]
(cond
(nil? value) []
(string? value) [value]
(map? value) [(:block/title value)]
(coll? value) (->> value
(mapcat property-value-titles)
vec)
:else [value]))
(deftest undo-missing-history-action-row-replays-from-inline-ops-test
(testing "undo/redo should replay from inline history ops when pending row is missing"
(worker-undo-redo/clear-history! test-repo)
@@ -953,6 +990,40 @@
(worker-undo-redo/redo test-repo)
(is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
(deftest repeated-set-block-property-text-value-undo-redo-test
(testing "set-block-property text value survives repeated undo/redo for one and many cardinalities"
(worker-undo-redo/clear-history! test-repo)
(let [conn (worker-state/get-datascript-conn test-repo)
{:keys [child-uuid]} (seed-page-parent-child!)]
(doseq [[suffix cardinality] [[:one :one] [:many :many]]]
(let [property-id (keyword (str "user.property/p1-undo-redo-" (name suffix)))]
(outliner-op/apply-ops! conn
[[:upsert-property [property-id
{:logseq.property/type :default
:db/cardinality cardinality}
{}]]]
(local-tx-meta {:client-id "test-client"}))
(worker-undo-redo/clear-history! test-repo)
(outliner-op/apply-ops! conn
[[:set-block-property [[:block/uuid child-uuid]
property-id
"value-1"]]]
(local-tx-meta {:client-id "test-client"}))
(let [history (latest-undo-history-data)]
(is (empty? (:db-sync/forward-outliner-ops history))))
(dotimes [_ 3]
(when-let [undo-tx-id (:db-sync/tx-id (latest-undo-history-data))]
(poison-history-tx-order! undo-tx-id))
(is (map? (worker-undo-redo/undo test-repo)))
(is (empty? (property-value-titles
(get (d/entity @conn [:block/uuid child-uuid]) property-id))))
(when-let [redo-tx-id (:db-sync/tx-id (latest-redo-history-data))]
(poison-history-tx-order! redo-tx-id))
(is (map? (worker-undo-redo/redo test-repo)))
(let [titles (property-value-titles
(get (d/entity @conn [:block/uuid child-uuid]) property-id))]
(is (contains? (set titles) "value-1")))))))))
(deftest save-two-blocks-undo-targets-latest-block-test
(testing "undo after saving two blocks reverts the latest saved block first"
(worker-undo-redo/clear-history! test-repo)