Deletes frontend.worker.sync/* wholesale (apply_txs, asset_db_listener,
assets, auth, client_op, const, crypt, download, handle_message,
large_title, log_and_state, presence, temp_sqlite, transport, upload,
util), worker/sync.cljs, worker/handler/sync.cljs, and
worker/ui_request.cljs -- the remote CRDT sync protocol and its E2EE
password-request plumbing, both made obsolete by the server-client
daemon architecture built in Fases 1-7. Deviates from the plan's KEEP
list on sync/presence.cljs and sync/transport.cljs: neither has any
caller left once the rest of the cluster is gone (confirmed by grep),
because Fase 4's WS resume-by-revision work already built its own
self-contained module (db_worker_node_ws.cljs) instead of adopting
these as the plan assumed it would.
This touched far more than a mechanical deletion:
- frontend.worker.sync.client-op wasn't sync-only. Its client_ops
SQLite table is also the idempotency marker for db_core.cljs's
built-in-property repair migration and the forward/inverse-outliner-
ops relay undo/redo reads back through history-action-ops-by-tx-id.
Trimmed (not deleted) into frontend.worker.local-tx-log, keeping only
the tx-entry/local-tx functions those two callers need and dropping
the genuinely sync-only surface (pending-upload counts, checksums,
graph-uuid, sync-conflict tracking, asset-op queues).
- frontend.worker.sync.apply-txs's apply-history-action! looked
sync-specific (it lived next to the remote-tx-apply/rebase machinery)
but turned out to be the *only* implementation ever registered for
worker-undo-redo/*apply-history-action! -- undo_redo.cljs calls it on
every db-transact undo/redo, local edits included, not just
remote-origin ones. Deleting it outright would have silently broken
undo/redo app-wide the next time anyone hit Cmd-Z. Ported the
general-purpose slice (replaying a recorded entry's forward/inverse
outliner ops against the current db state, plus the raw-tx-data
fallback for edits that bypass the structured outliner-op layer) into
a new frontend.worker.undo-redo.replay, along with persist-local-tx!
(derive history ops on every local commit, stash them in
local-tx-log, hand off to gen-undo-ops!) -- dropping only the
remote-rebase-specific rest (CRDT conflict resolution, upload-queue
bookkeeping). Wired into db_listener.cljs as a renamed
:persist-local-tx handler (was :db-sync, a name that stopped meaning
anything once sync was gone).
Caught by running the full existing undo_redo_test.cljs suite (30
deftests written for the old mechanism) against the new module after
fixing its test harness to use the real db_listener.cljs dispatch
instead of a hand-rolled listener -- all 29 tests port over passing
with no behavior change, which is the actual evidence this preserves
undo/redo correctness rather than just compiling.
- db_core.cljs's own 8 :thread-api/db-sync-* handlers (remote-graph-
download import, close-db/invalidate-search-db/recreate-lock
sync-specific variants) are deleted along with their sync-crypt/
sync-download calls; :thread-api/unsafe-unlink-db, :close-db, and
:release-access-handles keep their real (non-sync) bodies.
db_worker_node.cljs's non-repo-methods set drops from 22 to 7
entries (the 15 removed were all e2ee/db-sync/ui-request keywords
backed by deleted files).
- worker/handler/export.cljs's :thread-api/recompute-checksum-diagnostics
(and worker/db/validate.cljs's backing recompute-checksum-diagnostics,
which pulled in logseq-db-sync.checksum) compared a local checksum
against a remote one that no longer exists -- deleted outright, not
ported, since there's nothing left to diff against.
- worker/handler/block.cljs's :block.temp/sync-conflicts metadata
(backed by the deleted client-op/get-all-sync-conflicts) is gone;
its client-side consumer (a retry-upload/clear-conflicts UI in
components/block.cljs) is dead code now and still needs a follow-up
pass -- flagging here since worker-side cleanup alone can't remove
it without breaking that UI's thread-api calls at runtime.
Verified: app/electron/db-worker-node/mobile all compile clean. Full
worker test suite green (db_core_test 131, db_listener_test 11,
db_worker_test 8, db_worker_node_test 59, db_worker_node_lock_test 10,
local_tx_log_test 4, undo_redo_test 29, subs_test 50, subs_loader_test
2, config_test 6 -- 0 failures across all of them). The entire
src/test/frontend/worker/sync/ test directory (7 files) and
db_sync_test.cljs/db_sync_sim_test.cljs/node_sync_test.cljs (all
testing only-deleted namespaces) are deleted wholesale rather than
ported.
Known follow-up, not yet done: the client-side (frontend.handler.*,
frontend.components.*) RTC/E2EE/db-sync UI and its dead thread-api call
sites (components/block.cljs, components/repo.cljs,
components/settings.cljs, handler/user.cljs, handler/assets.cljs,
db/restore.cljs, handler/common/developer.cljs, and the
handler/db-based/{sync,rtc-flows,rtc-background-tasks}.cljs +
handler/events/rtc.cljs + components/rtc/* + db/rtc/* files themselves)
-- a separate commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
369 lines
12 KiB
Clojure
369 lines
12 KiB
Clojure
(ns frontend.worker.undo-redo
|
|
"Undo redo new implementation"
|
|
(:require [datascript.core :as d]
|
|
[frontend.worker.state :as worker-state]
|
|
[frontend.worker.local-tx-log :as local-tx-log]
|
|
[lambdaisland.glogi :as log]
|
|
[logseq.common.defkeywords :refer [defkeywords]]
|
|
[malli.core :as m]
|
|
[malli.util :as mu]))
|
|
|
|
(defkeywords
|
|
::record-editor-info {:doc "record current editor and cursor"}
|
|
::db-transact {:doc "db tx"}
|
|
::ui-state {:doc "ui state such as route && sidebar blocks"})
|
|
|
|
(defonce *apply-history-action! (atom nil))
|
|
|
|
;; TODO: add other UI states such as `::ui-updates`.
|
|
(comment
|
|
;; TODO: convert it to a qualified-keyword
|
|
(sr/defkeyword :gen-undo-ops?
|
|
"tx-meta option, generate undo ops from tx-data when true (default true)"))
|
|
|
|
(def ^:private selection-editor-info-schema
|
|
[:map
|
|
[:selected-block-uuids [:sequential :uuid]]
|
|
[:selection-direction {:optional true} [:maybe [:enum :up :down]]]])
|
|
|
|
(def ^:private editor-cursor-info-schema
|
|
[:map
|
|
[:block-uuid :uuid]
|
|
[:container-id [:or :int [:enum :unknown-container]]]
|
|
[:start-pos [:maybe :int]]
|
|
[:end-pos [:maybe :int]]
|
|
[:selected-block-uuids {:optional true} [:sequential :uuid]]
|
|
[:selection-direction {:optional true} [:maybe [:enum :up :down]]]])
|
|
|
|
(def ^:private undo-op-item-schema
|
|
(mu/closed-schema
|
|
[:multi {:dispatch first}
|
|
[::db-transact
|
|
[:cat :keyword
|
|
[:map
|
|
[:tx-meta [:map {:closed false}
|
|
[:outliner-op :keyword]]]
|
|
[:added-ids [:set :int]]
|
|
[:retracted-ids [:set :int]]
|
|
[:db-sync/tx-id {:optional true} :uuid]
|
|
[:db-sync/forward-outliner-ops {:optional true}
|
|
[:maybe [:sequential :any]]]
|
|
[:db-sync/inverse-outliner-ops {:optional true}
|
|
[:maybe [:sequential :any]]]]]]
|
|
|
|
[::record-editor-info
|
|
[:cat :keyword
|
|
[:or
|
|
editor-cursor-info-schema
|
|
selection-editor-info-schema]]]
|
|
|
|
[::ui-state
|
|
[:cat :keyword :string]]]))
|
|
|
|
(def ^:private undo-op-validator (m/validator [:sequential undo-op-item-schema]))
|
|
|
|
(defonce max-stack-length 250)
|
|
(defonce *undo-ops (atom {}))
|
|
(defonce *redo-ops (atom {}))
|
|
(defonce *pending-editor-info (atom {}))
|
|
|
|
(defn clear-history!
|
|
[repo]
|
|
(swap! *undo-ops assoc repo [])
|
|
(swap! *redo-ops assoc repo [])
|
|
(swap! *pending-editor-info dissoc repo))
|
|
|
|
(defn set-pending-editor-info!
|
|
[repo editor-info]
|
|
(if editor-info
|
|
(swap! *pending-editor-info assoc repo editor-info)
|
|
(swap! *pending-editor-info dissoc repo)))
|
|
|
|
(defn- take-pending-editor-info!
|
|
[repo]
|
|
(let [editor-info (get @*pending-editor-info repo)]
|
|
(swap! *pending-editor-info dissoc repo)
|
|
editor-info))
|
|
|
|
(defn- conj-op
|
|
[col op]
|
|
(let [result (conj (if (empty? col) [] col) op)]
|
|
(if (>= (count result) max-stack-length)
|
|
(subvec result 0 (/ max-stack-length 2))
|
|
result)))
|
|
|
|
(defn- pop-stack
|
|
[stack]
|
|
(when (seq stack)
|
|
[(last stack) (pop stack)]))
|
|
|
|
(defn- push-undo-op
|
|
[repo op]
|
|
(assert (undo-op-validator op) {:op op})
|
|
(swap! *undo-ops update repo conj-op op))
|
|
|
|
(defn- push-redo-op
|
|
[repo op]
|
|
(assert (undo-op-validator op) {:op op})
|
|
(swap! *redo-ops update repo conj-op op))
|
|
|
|
(defn- pop-undo-op
|
|
[repo]
|
|
(let [undo-stack (get @*undo-ops repo)
|
|
[op undo-stack*] (pop-stack undo-stack)]
|
|
(swap! *undo-ops assoc repo undo-stack*)
|
|
op))
|
|
|
|
(defn- pop-redo-op
|
|
[repo]
|
|
(let [redo-stack (get @*redo-ops repo)
|
|
[op redo-stack*] (pop-stack redo-stack)]
|
|
(swap! *redo-ops assoc repo redo-stack*)
|
|
op))
|
|
|
|
(defn- empty-undo-stack?
|
|
[repo]
|
|
(empty? (get @*undo-ops repo)))
|
|
|
|
(defn- empty-redo-stack?
|
|
[repo]
|
|
(empty? (get @*redo-ops repo)))
|
|
|
|
(defn- undo-redo-action-meta
|
|
[{:keys [tx-meta]
|
|
source-tx-id :db-sync/tx-id}
|
|
undo?]
|
|
(-> tx-meta
|
|
(dissoc :db-sync/tx-id)
|
|
(assoc
|
|
:gen-undo-ops? false
|
|
:persist-op? true
|
|
:undo? undo?
|
|
:redo? (not undo?)
|
|
:db-sync/source-tx-id source-tx-id)))
|
|
|
|
(defn- rebind-op-db-sync-tx-id
|
|
[op history-tx-id]
|
|
(if (uuid? history-tx-id)
|
|
(mapv (fn [item]
|
|
(if (= ::db-transact (first item))
|
|
[::db-transact (assoc (second item) :db-sync/tx-id history-tx-id)]
|
|
item))
|
|
op)
|
|
op))
|
|
|
|
(defn- skippable-worker-error?
|
|
[error]
|
|
(= :invalid-history-action-ops (:reason (ex-data error))))
|
|
|
|
(defn- skippable-worker-result?
|
|
[undo? {:keys [reason]}]
|
|
(if undo?
|
|
(contains? #{:invalid-history-action-ops
|
|
:invalid-history-action-tx
|
|
:unsupported-history-action}
|
|
reason)
|
|
(contains? #{:invalid-history-action-ops}
|
|
reason)))
|
|
|
|
(defn- expected-invalid-history-action-reason?
|
|
[reason]
|
|
(contains? #{:invalid-history-action-ops
|
|
:invalid-history-action-tx}
|
|
reason))
|
|
|
|
(declare undo-redo-aux)
|
|
|
|
(defn- empty-stack-result
|
|
[undo?]
|
|
(if undo? ::empty-undo-stack ::empty-redo-stack))
|
|
|
|
(defn- push-opposite-op!
|
|
[repo undo? op]
|
|
(let [sanitize-db-transact
|
|
(fn [data]
|
|
;; Keep undo/redo history op-only. Drop any legacy/raw tx payloads.
|
|
(dissoc data
|
|
:tx
|
|
:tx-data
|
|
:reversed-tx
|
|
:reversed-tx-data
|
|
:db-sync/normalized-tx-data
|
|
:db-sync/reversed-tx-data))
|
|
op' (mapv (fn [item]
|
|
(if (= ::db-transact (first item))
|
|
[::db-transact (sanitize-db-transact (second item))]
|
|
item))
|
|
op)]
|
|
((if undo? push-redo-op push-undo-op) repo op')))
|
|
|
|
(defn- undo-redo-result
|
|
[repo conn undo? op op']
|
|
(push-opposite-op! repo undo? op')
|
|
(let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) op)
|
|
(map second))
|
|
cursor (if undo?
|
|
(first editor-cursors)
|
|
(or (last editor-cursors) (first editor-cursors)))
|
|
block-content (when-let [block-uuid (:block-uuid cursor)]
|
|
(:block/title (d/entity @conn [:block/uuid block-uuid])))]
|
|
{:undo? undo?
|
|
:editor-cursors editor-cursors
|
|
:block-content block-content}))
|
|
|
|
(defn- skip-op-and-recur
|
|
[repo undo?]
|
|
(undo-redo-aux repo undo?))
|
|
|
|
(defn- apply-history-action
|
|
[repo conn undo? op tx-meta' tx-id]
|
|
(if-let [apply-action @*apply-history-action!]
|
|
(try
|
|
(let [worker-result (apply-action repo tx-id undo? tx-meta')]
|
|
(cond
|
|
(:applied? worker-result)
|
|
(undo-redo-result repo conn undo? op
|
|
(if undo?
|
|
op
|
|
(rebind-op-db-sync-tx-id op (:history-tx-id worker-result))))
|
|
|
|
(skippable-worker-result? undo? worker-result)
|
|
(skip-op-and-recur repo undo?)
|
|
|
|
:else
|
|
(do
|
|
(when-not (expected-invalid-history-action-reason? (:reason worker-result))
|
|
(log/error ::undo-redo-worker-action-unavailable
|
|
{:undo? undo?
|
|
:repo repo
|
|
:tx-id tx-id
|
|
:result worker-result}))
|
|
(clear-history! repo)
|
|
(empty-stack-result undo?))))
|
|
(catch :default e
|
|
(if (skippable-worker-error? e)
|
|
(skip-op-and-recur repo undo?)
|
|
(do
|
|
(log/error ::undo-redo-worker-failed e)
|
|
(clear-history! repo)
|
|
(throw e)))))
|
|
(do
|
|
(log/error ::undo-redo-worker-action-unavailable
|
|
{:undo? undo?
|
|
:repo repo
|
|
:tx-id tx-id
|
|
:tx-meta tx-meta'
|
|
:reason :missing-apply-history-action})
|
|
(clear-history! repo)
|
|
(empty-stack-result undo?))))
|
|
|
|
(defn- process-db-op
|
|
[repo conn undo? op]
|
|
(when-let [data (some #(when (= ::db-transact (first %))
|
|
(second %))
|
|
op)]
|
|
(let [tx-id (:db-sync/tx-id data)
|
|
tx-meta' (merge (undo-redo-action-meta data undo?)
|
|
(select-keys data [:db-sync/forward-outliner-ops
|
|
:db-sync/inverse-outliner-ops]))]
|
|
(apply-history-action repo conn undo? op tx-meta' tx-id))))
|
|
|
|
(defn- undo-redo-aux
|
|
[repo undo?]
|
|
(if-let [op (not-empty ((if undo? pop-undo-op pop-redo-op) repo))]
|
|
(if (= ::ui-state (ffirst op))
|
|
(do
|
|
(push-opposite-op! repo undo? op)
|
|
{:undo? undo?
|
|
:ui-state-str (second (first op))})
|
|
(process-db-op repo (worker-state/get-datascript-conn repo) undo? op))
|
|
(when ((if undo? empty-undo-stack? empty-redo-stack?) repo)
|
|
(empty-stack-result undo?))))
|
|
|
|
(defn undo
|
|
[repo]
|
|
(undo-redo-aux repo true))
|
|
|
|
(defn redo
|
|
[repo]
|
|
(undo-redo-aux repo false))
|
|
|
|
(defn record-editor-info!
|
|
[repo editor-info]
|
|
(when editor-info
|
|
(swap! *undo-ops
|
|
update repo
|
|
(fn [stack]
|
|
(if (seq stack)
|
|
(update stack (dec (count stack))
|
|
(fn [op]
|
|
(conj (vec op) [::record-editor-info editor-info])))
|
|
stack)))))
|
|
|
|
(defn record-ui-state!
|
|
[repo ui-state-str]
|
|
(when ui-state-str
|
|
(push-undo-op repo [[::ui-state ui-state-str]])))
|
|
|
|
(defn- pending-history-action-ops
|
|
[repo tx-id]
|
|
(when (uuid? tx-id)
|
|
(local-tx-log/history-action-ops-by-tx-id repo tx-id)))
|
|
|
|
(defn gen-undo-ops!
|
|
[repo {:keys [tx-data tx-meta db-after db-before]} tx-id
|
|
{:keys [apply-history-action!]}]
|
|
(when (nil? @*apply-history-action!)
|
|
(reset! *apply-history-action! apply-history-action!))
|
|
(let [{:keys [outliner-op local-tx?]} tx-meta
|
|
{:db-sync/keys [forward-outliner-ops inverse-outliner-ops]} (pending-history-action-ops repo tx-id)]
|
|
(when (and
|
|
(true? local-tx?)
|
|
outliner-op
|
|
(not (false? (:gen-undo-ops? tx-meta)))
|
|
(not (:create-today-journal? tx-meta))
|
|
(not (contains? #{:create-view} (:source-outliner-op tx-meta))))
|
|
(let [all-ids (distinct (map :e tx-data))
|
|
retracted-ids (set
|
|
(filter
|
|
(fn [id] (and (nil? (d/entity db-after id)) (d/entity db-before id)))
|
|
all-ids))
|
|
added-ids (set
|
|
(filter
|
|
(fn [id] (and (nil? (d/entity db-before id)) (d/entity db-after id)))
|
|
all-ids))
|
|
editor-info (or (:undo-redo/editor-info tx-meta)
|
|
(take-pending-editor-info! repo))
|
|
|
|
data (cond-> {:db-sync/tx-id tx-id
|
|
:tx-meta (dissoc tx-meta :outliner-ops)
|
|
:added-ids added-ids
|
|
:retracted-ids retracted-ids
|
|
:db-sync/forward-outliner-ops forward-outliner-ops
|
|
:db-sync/inverse-outliner-ops inverse-outliner-ops})
|
|
op (->> [(when editor-info [::record-editor-info editor-info])
|
|
[::db-transact data]]
|
|
(remove nil?)
|
|
vec)]
|
|
;; A new local action invalidates redo history.
|
|
(swap! *redo-ops assoc repo [])
|
|
(push-undo-op repo op)))))
|
|
|
|
(defn get-debug-state
|
|
[repo]
|
|
{:undo-ops (get @*undo-ops repo [])
|
|
:redo-ops (get @*redo-ops repo [])
|
|
:pending-editor-info (get @*pending-editor-info repo)})
|
|
|
|
(defn referenced-history-tx-ids
|
|
[repo]
|
|
(->> (concat (get @*undo-ops repo [])
|
|
(get @*redo-ops repo []))
|
|
(mapcat identity)
|
|
(keep (fn [item]
|
|
(when (= ::db-transact (first item))
|
|
(let [tx-id (:db-sync/tx-id (second item))]
|
|
(when (uuid? tx-id)
|
|
tx-id)))))
|
|
set))
|