Browse Source

Merge pull request #11819 from logseq/fix/multiple-tabs

feat: multiple tabs/windows support
Tienson Qin 6 months ago
parent
commit
d42621864a
34 changed files with 801 additions and 390 deletions
  1. 1 0
      .clj-kondo/config.edn
  2. 8 0
      deps/common/src/logseq/common/util.cljs
  3. 1 6
      src/electron/electron/core.cljs
  4. 1 1
      src/main/electron/listener.cljs
  5. 1 1
      src/main/frontend/db/conn.cljs
  6. 2 0
      src/main/frontend/db/restore.cljs
  7. 1 3
      src/main/frontend/db/rtc/debug_ui.cljs
  8. 3 3
      src/main/frontend/db/transact.cljs
  9. 26 28
      src/main/frontend/handler/db_based/rtc.cljs
  10. 2 2
      src/main/frontend/handler/editor/lifecycle.cljs
  11. 1 2
      src/main/frontend/handler/events.cljs
  12. 1 21
      src/main/frontend/handler/events/ui.cljs
  13. 16 24
      src/main/frontend/handler/history.cljs
  14. 5 3
      src/main/frontend/handler/repo.cljs
  15. 5 6
      src/main/frontend/handler/ui.cljs
  16. 4 8
      src/main/frontend/handler/user.cljs
  17. 20 6
      src/main/frontend/modules/outliner/pipeline.cljs
  18. 27 26
      src/main/frontend/modules/outliner/ui.cljc
  19. 24 27
      src/main/frontend/persist_db/browser.cljs
  20. 14 11
      src/main/frontend/state.cljs
  21. 68 48
      src/main/frontend/undo_redo.cljs
  22. 3 12
      src/main/frontend/util/page.cljs
  23. 10 10
      src/main/frontend/worker/db/validate.cljs
  24. 2 2
      src/main/frontend/worker/db_listener.cljs
  25. 106 65
      src/main/frontend/worker/db_worker.cljs
  26. 3 2
      src/main/frontend/worker/pipeline.cljs
  27. 40 14
      src/main/frontend/worker/rtc/core.cljs
  28. 9 7
      src/main/frontend/worker/rtc/full_upload_download_graph.cljs
  29. 3 2
      src/main/frontend/worker/rtc/log_and_state.cljs
  30. 13 13
      src/main/frontend/worker/rtc/skeleton.cljs
  31. 360 0
      src/main/frontend/worker/shared_service.cljs
  32. 0 20
      src/main/frontend/worker/state.cljs
  33. 5 5
      src/rtc_e2e_test/client_steps.cljs
  34. 16 12
      src/test/frontend/undo_redo_test.cljs

+ 1 - 0
.clj-kondo/config.edn

@@ -157,6 +157,7 @@
              frontend.util.text text-util
              frontend.util.text text-util
              frontend.util.thingatpt thingatpt
              frontend.util.thingatpt thingatpt
              frontend.util.url url-util
              frontend.util.url url-util
+             frontend.worker.shared-service shared-service
              frontend.worker.handler.page worker-page
              frontend.worker.handler.page worker-page
              frontend.worker.pipeline worker-pipeline
              frontend.worker.pipeline worker-pipeline
              frontend.worker.state worker-state
              frontend.worker.state worker-state

+ 8 - 0
deps/common/src/logseq/common/util.cljs

@@ -385,3 +385,11 @@ return: [{:id 3} {:id 2 :depend-on 3} {:id 1 :depend-on 2}]"
         (tc/to-long (f now (t/years 1)))
         (tc/to-long (f now (t/years 1)))
         nil)
         nil)
       (tc/to-long (tc/to-date value)))))
       (tc/to-long (tc/to-date value)))))
+
+(defn keyword->string
+  [x]
+  (if (keyword? x)
+    (if-let [nn (namespace x)]
+      (str nn "/" (name x))
+      (name x))
+    x))

+ 1 - 6
src/electron/electron/core.cljs

@@ -184,12 +184,7 @@
         template (conj template
         template (conj template
                        {:role "fileMenu"
                        {:role "fileMenu"
                         :submenu [{:label "New Window"
                         :submenu [{:label "New Window"
-                                   :click (fn []
-                                            ;; FIXME: Open a different graph for now
-                                            ;; (p/let [graph-name (get-graph-name (state/get-graph-path))
-                                            ;;         _ (handler/broadcast-persist-graph! graph-name)]
-                                            ;;   (handler/open-new-window!))
-                                            )
+                                   :click (fn [] (handler/open-new-window! nil))
                                    :accelerator (if mac?
                                    :accelerator (if mac?
                                                   "CommandOrControl+N"
                                                   "CommandOrControl+N"
                                                   ;; Avoid conflict with `Control+N` shortcut to move down in the text editor on Windows/Linux
                                                   ;; Avoid conflict with `Control+N` shortcut to move down in the text editor on Windows/Linux

+ 1 - 1
src/main/electron/listener.cljs

@@ -119,7 +119,7 @@
                  ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                  ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                  ;; No db cache persisting ensured. Should be handled by the caller
                  ;; No db cache persisting ensured. Should be handled by the caller
                  (fn [repo]
                  (fn [repo]
-                   (ui-handler/open-new-window-or-tab! nil repo)))
+                   (ui-handler/open-new-window-or-tab! repo)))
 
 
   (safe-api-call "invokeLogseqAPI"
   (safe-api-call "invokeLogseqAPI"
                  (fn [^js data]
                  (fn [^js data]

+ 1 - 1
src/main/frontend/db/conn.cljs

@@ -85,7 +85,7 @@
                    (gp-db/start-conn))]
                    (gp-db/start-conn))]
      (swap! conns assoc db-name db-conn)
      (swap! conns assoc db-name db-conn)
      (when listen-handler
      (when listen-handler
-       (listen-handler repo)))))
+       (listen-handler db-conn)))))
 
 
 (defn destroy-all!
 (defn destroy-all!
   []
   []

+ 2 - 0
src/main/frontend/db/restore.cljs

@@ -4,6 +4,7 @@
             [frontend.db.conn :as db-conn]
             [frontend.db.conn :as db-conn]
             [frontend.persist-db :as persist-db]
             [frontend.persist-db :as persist-db]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [logseq.db.common.sqlite :as sqlite-common-db]
             [logseq.db.common.sqlite :as sqlite-common-db]
             [promesa.core :as p]))
             [promesa.core :as p]))
@@ -25,6 +26,7 @@
                                                 :initial-data initial-data}))
                                                 :initial-data initial-data}))
                    (js/console.error e)
                    (js/console.error e)
                    (throw e)))
                    (throw e)))
+          _ (undo-redo/listen-db-changes! repo conn)
           db-name (db-conn/get-repo-path repo)
           db-name (db-conn/get-repo-path repo)
           _ (swap! db-conn/conns assoc db-name conn)
           _ (swap! db-conn/conns assoc db-name conn)
           end-time (t/now)]
           end-time (t/now)]

+ 1 - 3
src/main/frontend/db/rtc/debug_ui.cljs

@@ -111,9 +111,7 @@
        (shui/button
        (shui/button
         {:variant :outline
         {:variant :outline
          :class "text-green-rx-09 border-green-rx-10 hover:text-green-rx-10"
          :class "text-green-rx-09 border-green-rx-10 hover:text-green-rx-10"
-         :on-click (fn []
-                     (let [token (state/get-auth-id-token)]
-                       (state/<invoke-db-worker :thread-api/rtc-start (state/get-current-repo) token)))}
+         :on-click (fn [] (state/<invoke-db-worker :thread-api/rtc-start false))}
         (shui/tabler-icon "player-play") "start")
         (shui/tabler-icon "player-play") "start")
 
 
        [:div.my-2.flex
        [:div.my-2.flex

+ 3 - 3
src/main/frontend/db/transact.cljs

@@ -2,8 +2,8 @@
   "Provides async transact for use with ldb/transact!"
   "Provides async transact for use with ldb/transact!"
   (:require [clojure.core.async :as async]
   (:require [clojure.core.async :as async]
             [clojure.core.async.interop :refer [p->c]]
             [clojure.core.async.interop :refer [p->c]]
-            [promesa.core :as p]
-            [frontend.common.async-util :include-macros true :refer [<?]]))
+            [frontend.common.async-util :include-macros true :refer [<?]]
+            [promesa.core :as p]))
 
 
 (defonce *request-id (atom 0))
 (defonce *request-id (atom 0))
 (defonce requests (async/chan 1000))
 (defonce requests (async/chan 1000))
@@ -55,4 +55,4 @@
                         ;; not from remote(rtc)
                         ;; not from remote(rtc)
                         :local-tx? true)]
                         :local-tx? true)]
     (add-request! request-id (fn async-request []
     (add-request! request-id (fn async-request []
-                                   (worker-transact repo tx-data tx-meta')))))
+                               (worker-transact repo tx-data tx-meta')))))

+ 26 - 28
src/main/frontend/handler/db_based/rtc.cljs

@@ -96,37 +96,35 @@
   (when-let [graph-uuid (ldb/get-graph-rtc-uuid (db/get-db repo))]
   (when-let [graph-uuid (ldb/get-graph-rtc-uuid (db/get-db repo))]
     (p/do!
     (p/do!
      (js/Promise. user-handler/task--ensure-id&access-token)
      (js/Promise. user-handler/task--ensure-id&access-token)
-     (when stop-before-start? (<rtc-stop!))
-     (let [token (state/get-auth-id-token)]
-       (p/let [start-ex (state/<invoke-db-worker :thread-api/rtc-start repo token)
-               ex-data* (:ex-data start-ex)
-               _ (case (:type ex-data*)
-                   (:rtc.exception/not-rtc-graph
-                    :rtc.exception/not-found-db-conn)
-                   (notification/show! (:ex-message start-ex) :error)
+     (p/let [start-ex (state/<invoke-db-worker :thread-api/rtc-start stop-before-start?)
+             ex-data* (:ex-data start-ex)
+             _ (case (:type ex-data*)
+                 (:rtc.exception/not-rtc-graph
+                  :rtc.exception/not-found-db-conn)
+                 (notification/show! (:ex-message start-ex) :error)
 
 
-                   :rtc.exception/major-schema-version-mismatched
-                   (case (:sub-type ex-data*)
-                     :download
-                     (notification-download-higher-schema-graph! repo graph-uuid (:remote ex-data*))
-                     :create-branch
-                     (notification-upload-higher-schema-graph! repo)
-                        ;; else
-                     (do (log/info :start-ex start-ex)
-                         (notification/show! [:div
-                                              [:div (:ex-message start-ex)]
-                                              [:div (-> ex-data*
-                                                        (select-keys [:app :local :remote])
-                                                        pp/pprint
-                                                        with-out-str)]]
-                                             :error)))
+                 :rtc.exception/major-schema-version-mismatched
+                 (case (:sub-type ex-data*)
+                   :download
+                   (notification-download-higher-schema-graph! repo graph-uuid (:remote ex-data*))
+                   :create-branch
+                   (notification-upload-higher-schema-graph! repo)
+                   ;; else
+                   (do (log/info :start-ex start-ex)
+                       (notification/show! [:div
+                                            [:div (:ex-message start-ex)]
+                                            [:div (-> ex-data*
+                                                      (select-keys [:app :local :remote])
+                                                      pp/pprint
+                                                      with-out-str)]]
+                                           :error)))
 
 
-                   :rtc.exception/lock-failed
-                   (js/setTimeout #(<rtc-start! repo) 1000)
+                 :rtc.exception/lock-failed
+                 (js/setTimeout #(<rtc-start! repo) 1000)
 
 
-                      ;; else
-                   nil)]
-         nil)))))
+                 ;; else
+                 nil)]
+       nil))))
 
 
 (defn <get-remote-graphs
 (defn <get-remote-graphs
   []
   []

+ 2 - 2
src/main/frontend/handler/editor/lifecycle.cljs

@@ -3,6 +3,7 @@
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [frontend.util :as util]
             [goog.dom :as gdom]))
             [goog.dom :as gdom]))
 
 
@@ -33,8 +34,7 @@
       (let [page-id (:block/uuid (:block/page (db/entity (:db/id (state/get-edit-block)))))
       (let [page-id (:block/uuid (:block/page (db/entity (:db/id (state/get-edit-block)))))
             repo (state/get-current-repo)]
             repo (state/get-current-repo)]
         (when page-id
         (when page-id
-          (state/<invoke-db-worker :thread-api/record-editor-info repo (str page-id) (state/get-editor-info)))))
-
+          (undo-redo/record-editor-info! repo (state/get-editor-info)))))
     (state/set-state! :editor/op nil))
     (state/set-state! :editor/op nil))
   state)
   state)
 
 

+ 1 - 2
src/main/frontend/handler/events.cljs

@@ -142,8 +142,7 @@
       (graph-switch-on-persisted graph opts))))
       (graph-switch-on-persisted graph opts))))
 
 
 (defmethod handle :graph/open-new-window [[_ev target-repo]]
 (defmethod handle :graph/open-new-window [[_ev target-repo]]
-  (p/let [current-repo (state/get-current-repo)]
-    (ui-handler/open-new-window-or-tab! current-repo target-repo)))
+  (ui-handler/open-new-window-or-tab! target-repo))
 
 
 (defmethod handle :graph/migrated [[_ _repo]]
 (defmethod handle :graph/migrated [[_ _repo]]
   (js/alert "Graph migrated."))
   (js/alert "Graph migrated."))

+ 1 - 21
src/main/frontend/handler/events/ui.cljs

@@ -42,8 +42,7 @@
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [logseq.common.util :as common-util]
             [logseq.common.util :as common-util]
             [logseq.shui.ui :as shui]
             [logseq.shui.ui :as shui]
-            [promesa.core :as p]
-            [rum.core :as rum]))
+            [promesa.core :as p]))
 
 
 (defmethod events/handle :class/configure [[_ page]]
 (defmethod events/handle :class/configure [[_ page]]
   (shui/dialog-open!
   (shui/dialog-open!
@@ -303,25 +302,6 @@
 (defmethod events/handle :dialog-select/db-graph-replace []
 (defmethod events/handle :dialog-select/db-graph-replace []
   (select/dialog-select! :db-graph-replace))
   (select/dialog-select! :db-graph-replace))
 
 
-(rum/defc multi-tabs-dialog
-  []
-  (let [word (if (util/electron?) "window" "tab")]
-    [:div.flex.p-4.flex-col.gap-4.h-64
-     [:span.warning.text-lg
-      (util/format "Logseq doesn't support multiple %ss access to the same graph yet, please close this %s or switch to another graph."
-                   word word)]
-     [:div.text-lg
-      [:p "Switch to another repo: "]
-      [:div.border.rounded.bg-gray-01.overflow-hidden.w-60
-       (repo/repos-dropdown {:on-click (fn [e]
-                                         (util/stop e)
-                                         (state/set-state! :error/multiple-tabs-access-opfs? false)
-                                         (shui/dialog-close!))})]]]))
-
-(defmethod events/handle :show/multiple-tabs-error-dialog [_]
-  (state/set-state! :error/multiple-tabs-access-opfs? true)
-  (shui/dialog-open! multi-tabs-dialog))
-
 (defmethod events/handle :editor/show-action-bar []
 (defmethod events/handle :editor/show-action-bar []
   (let [selection (state/get-selection-blocks)
   (let [selection (state/get-selection-blocks)
         first-visible-block (some #(when (util/el-visible-in-viewport? % true) %) selection)]
         first-visible-block (some #(when (util/el-visible-in-viewport? % true) %) selection)]

+ 16 - 24
src/main/frontend/handler/history.cljs

@@ -5,8 +5,8 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.persist-db.browser :as db-browser]
             [frontend.persist-db.browser :as db-browser]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [frontend.util :as util]
-            [frontend.util.page :as page-util]
             [goog.functions :refer [debounce]]
             [goog.functions :refer [debounce]]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [promesa.core :as p]))
             [promesa.core :as p]))
@@ -48,19 +48,15 @@
       (p/do!
       (p/do!
        @*last-request
        @*last-request
        (when-let [repo (state/get-current-repo)]
        (when-let [repo (state/get-current-repo)]
-         (let [current-page-uuid-str (some->> (page-util/get-latest-edit-page-id)
-                                              db/entity
-                                              :block/uuid
-                                              str)]
-           (when (db-transact/request-finished?)
-             (util/stop e)
-             (p/do!
-              (state/set-state! [:editor/last-replace-ref-content-tx repo] nil)
-              (editor/save-current-block!)
-              (state/clear-editor-action!)
-              (reset! *last-request (state/<invoke-db-worker :thread-api/undo repo current-page-uuid-str))
-              (p/let [result @*last-request]
-                (restore-cursor-and-state! result))))))))))
+         (when (db-transact/request-finished?)
+           (util/stop e)
+           (p/do!
+            (state/set-state! [:editor/last-replace-ref-content-tx repo] nil)
+            (editor/save-current-block!)
+            (state/clear-editor-action!)
+            (reset! *last-request (undo-redo/undo repo))
+            (p/let [result @*last-request]
+              (restore-cursor-and-state! result)))))))))
 (defonce undo! (debounce undo-aux! 20))
 (defonce undo! (debounce undo-aux! 20))
 
 
 (let [*last-request (atom nil)]
 (let [*last-request (atom nil)]
@@ -71,14 +67,10 @@
       (p/do!
       (p/do!
        @*last-request
        @*last-request
        (when-let [repo (state/get-current-repo)]
        (when-let [repo (state/get-current-repo)]
-         (let [current-page-uuid-str (some->> (page-util/get-latest-edit-page-id)
-                                              db/entity
-                                              :block/uuid
-                                              str)]
-           (when (db-transact/request-finished?)
-             (util/stop e)
-             (state/clear-editor-action!)
-             (reset! *last-request (state/<invoke-db-worker :thread-api/redo repo current-page-uuid-str))
-             (p/let [result @*last-request]
-               (restore-cursor-and-state! result)))))))))
+         (when (db-transact/request-finished?)
+           (util/stop e)
+           (state/clear-editor-action!)
+           (reset! *last-request (undo-redo/redo repo))
+           (p/let [result @*last-request]
+             (restore-cursor-and-state! result))))))))
 (defonce redo! (debounce redo-aux! 20))
 (defonce redo! (debounce redo-aux! 20))

+ 5 - 3
src/main/frontend/handler/repo.cljs

@@ -22,6 +22,7 @@
             [frontend.persist-db :as persist-db]
             [frontend.persist-db :as persist-db]
             [frontend.search :as search]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.fs :as util-fs]
             [frontend.util.fs :as util-fs]
             [frontend.util.text :as text-util]
             [frontend.util.text :as text-util]
@@ -59,9 +60,10 @@
 (defn start-repo-db-if-not-exists!
 (defn start-repo-db-if-not-exists!
   [repo & {:as opts}]
   [repo & {:as opts}]
   (state/set-current-repo! repo)
   (state/set-current-repo! repo)
-  (db/start-db-conn! repo (merge
-                           opts
-                           {:db-graph? (config/db-based-graph? repo)})))
+  (db/start-db-conn! repo (assoc opts
+                                 :db-graph? (config/db-based-graph? repo)
+                                 :listen-handler (fn [conn]
+                                                   (undo-redo/listen-db-changes! repo conn)))))
 
 
 (defn restore-and-setup-repo!
 (defn restore-and-setup-repo!
   "Restore the db of a graph from the persisted data, and setup. Create a new
   "Restore the db of a graph from the persisted data, and setup. Create a new

+ 5 - 6
src/main/frontend/handler/ui.cljs

@@ -254,12 +254,11 @@
 
 
 (defn open-new-window-or-tab!
 (defn open-new-window-or-tab!
   "Open a new Electron window."
   "Open a new Electron window."
-  [repo target-repo]
-  (when-not (= repo target-repo)        ; TODO: remove this once we support multi-tabs OPFS access
-    (when target-repo
-      (if (util/electron?)
-        (ipc/ipc "openNewWindow" target-repo)
-        (js/window.open (str config/app-website "#/?graph=" target-repo) "_blank")))))
+  [target-repo]
+  (when target-repo
+    (if (util/electron?)
+      (ipc/ipc "openNewWindow" target-repo)
+      (js/window.open (str config/app-website "#/?graph=" target-repo) "_blank"))))
 
 
 (defn toggle-show-empty-hidden-properties!
 (defn toggle-show-empty-hidden-properties!
   []
   []

+ 4 - 8
src/main/frontend/handler/user.cljs

@@ -113,8 +113,7 @@
    (state/set-auth-access-token nil)
    (state/set-auth-access-token nil)
    (state/set-auth-refresh-token nil)
    (state/set-auth-refresh-token nil)
    (set-token-to-localstorage! "" "" "")
    (set-token-to-localstorage! "" "" "")
-   (clear-cognito-tokens!)
-   (state/<invoke-db-worker :thread-api/update-auth-tokens nil nil nil))
+   (clear-cognito-tokens!))
   ([except-refresh-token?]
   ([except-refresh-token?]
    (state/set-auth-id-token nil)
    (state/set-auth-id-token nil)
    (state/set-auth-access-token nil)
    (state/set-auth-access-token nil)
@@ -122,21 +121,18 @@
      (state/set-auth-refresh-token nil))
      (state/set-auth-refresh-token nil))
    (if except-refresh-token?
    (if except-refresh-token?
      (set-token-to-localstorage! "" "")
      (set-token-to-localstorage! "" "")
-     (set-token-to-localstorage! "" "" ""))
-   (state/<invoke-db-worker :thread-api/update-auth-tokens nil nil (state/get-auth-refresh-token))))
+     (set-token-to-localstorage! "" "" ""))))
 
 
 (defn- set-tokens!
 (defn- set-tokens!
   ([id-token access-token]
   ([id-token access-token]
    (state/set-auth-id-token id-token)
    (state/set-auth-id-token id-token)
    (state/set-auth-access-token access-token)
    (state/set-auth-access-token access-token)
-   (set-token-to-localstorage! id-token access-token)
-   (state/<invoke-db-worker :thread-api/update-auth-tokens id-token access-token (state/get-auth-refresh-token)))
+   (set-token-to-localstorage! id-token access-token))
   ([id-token access-token refresh-token]
   ([id-token access-token refresh-token]
    (state/set-auth-id-token id-token)
    (state/set-auth-id-token id-token)
    (state/set-auth-access-token access-token)
    (state/set-auth-access-token access-token)
    (state/set-auth-refresh-token refresh-token)
    (state/set-auth-refresh-token refresh-token)
-   (set-token-to-localstorage! id-token access-token refresh-token)
-   (state/<invoke-db-worker :thread-api/update-auth-tokens id-token access-token refresh-token)))
+   (set-token-to-localstorage! id-token access-token refresh-token)))
 
 
 (defn- <refresh-tokens
 (defn- <refresh-tokens
   "return refreshed id-token, access-token"
   "return refreshed id-token, access-token"

+ 20 - 6
src/main/frontend/modules/outliner/pipeline.cljs

@@ -1,13 +1,25 @@
 (ns frontend.modules.outliner.pipeline
 (ns frontend.modules.outliner.pipeline
-  (:require [frontend.db :as db]
-            [frontend.db.react :as react]
-            [frontend.state :as state]
+  (:require [clojure.string :as string]
             [datascript.core :as d]
             [datascript.core :as d]
+            [frontend.config :as config]
+            [frontend.db :as db]
+            [frontend.db.react :as react]
+            [frontend.fs :as fs]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
-            [frontend.fs :as fs]
-            [logseq.common.path :as path]
-            [frontend.config :as config]))
+            [logseq.common.path :as path]))
+
+(defn- update-editing-block-title-if-changed!
+  [tx-data]
+  (when-let [editing-block (state/get-edit-block)]
+    (let [editing-title (state/get-edit-content)]
+      (when-let [new-title (some (fn [d] (when (and (= (:e d) (:db/id editing-block))
+                                                    (= (:a d) :block/title)
+                                                    (not= (string/trim editing-title) (string/trim (:v d)))
+                                                    (:added d))
+                                           (:v d))) tx-data)]
+        (state/set-edit-content! new-title)))))
 
 
 (defn invoke-hooks
 (defn invoke-hooks
   [{:keys [_request-id repo tx-meta tx-data deleted-block-uuids deleted-assets affected-keys blocks]}]
   [{:keys [_request-id repo tx-meta tx-data deleted-block-uuids deleted-assets affected-keys blocks]}]
@@ -55,6 +67,8 @@
                               tx-data))]
                               tx-data))]
               (d/transact! conn tx-data' tx-meta))
               (d/transact! conn tx-data' tx-meta))
 
 
+            (update-editing-block-title-if-changed! tx-data)
+
             (when (seq deleted-assets)
             (when (seq deleted-assets)
               (doseq [asset deleted-assets]
               (doseq [asset deleted-assets]
                 (fs/unlink! repo (path/path-join (config/get-current-repo-assets-root) (str (:block/uuid asset) "." (:ext asset))) {})))
                 (fs/unlink! repo (path/path-join (config/get-current-repo-assets-root) (str (:block/uuid asset) "." (:ext asset))) {})))

+ 27 - 26
src/main/frontend/modules/outliner/ui.cljc

@@ -9,33 +9,34 @@
 
 
 (defmacro transact!
 (defmacro transact!
   [opts & body]
   [opts & body]
-  `(let [test?# frontend.util/node-test?]
-     (let [ops# frontend.modules.outliner.op/*outliner-ops*
-           editor-info# (frontend.state/get-editor-info)]
-       (if ops#
-         (do ~@body)                    ; nested transact!
-         (binding [frontend.modules.outliner.op/*outliner-ops* (transient [])]
-           ~@body
-           (let [r# (persistent! frontend.modules.outliner.op/*outliner-ops*)]
+  `(let [test?# frontend.util/node-test?
+         ops# frontend.modules.outliner.op/*outliner-ops*
+         editor-info# (frontend.state/get-editor-info)]
+     (reset! frontend.state/*editor-info editor-info#)
+     (if ops#
+       (do ~@body)                    ; nested transact!
+       (binding [frontend.modules.outliner.op/*outliner-ops* (transient [])]
+         ~@body
+         (let [r# (persistent! frontend.modules.outliner.op/*outliner-ops*)]
             ;;  (js/console.groupCollapsed "ui/transact!")
             ;;  (js/console.groupCollapsed "ui/transact!")
             ;;  (prn :ops r#)
             ;;  (prn :ops r#)
             ;;  (js/console.trace)
             ;;  (js/console.trace)
             ;;  (js/console.groupEnd)
             ;;  (js/console.groupEnd)
-             (if test?#
-               (when (seq r#)
-                 (logseq.outliner.op/apply-ops! (frontend.state/get-current-repo)
-                                                (frontend.db.conn/get-db false)
-                                                r#
-                                                (frontend.state/get-date-formatter)
-                                                ~opts))
-               (when (seq r#)
-                 (let [request-id# (frontend.state/get-worker-next-request-id)
-                       request# #(frontend.state/<invoke-db-worker
-                                  :thread-api/apply-outliner-ops
-                                  (frontend.state/get-current-repo)
-                                  r#
-                                  (assoc ~opts
-                                         :request-id request-id#
-                                         :editor-info editor-info#))
-                       response# (frontend.state/add-worker-request! request-id# request#)]
-                   response#)))))))))
+           (if test?#
+             (when (seq r#)
+               (logseq.outliner.op/apply-ops! (frontend.state/get-current-repo)
+                                              (frontend.db.conn/get-db false)
+                                              r#
+                                              (frontend.state/get-date-formatter)
+                                              ~opts))
+             (when (seq r#)
+               (let [request-id# (frontend.state/get-worker-next-request-id)
+                     request# #(frontend.state/<invoke-db-worker
+                                :thread-api/apply-outliner-ops
+                                (frontend.state/get-current-repo)
+                                r#
+                                (assoc ~opts
+                                       :request-id request-id#
+                                       :client-id (:client-id @frontend.state/state)))
+                     response# (frontend.state/add-worker-request! request-id# request#)]
+                 response#))))))))

+ 24 - 27
src/main/frontend/persist_db/browser.cljs

@@ -4,6 +4,7 @@
    This interface uses clj data format as input."
    This interface uses clj data format as input."
   (:require ["comlink" :as Comlink]
   (:require ["comlink" :as Comlink]
             [electron.ipc :as ipc]
             [electron.ipc :as ipc]
+            [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :as thread-api]
             [frontend.common.thread-api :as thread-api]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
@@ -12,8 +13,10 @@
             [frontend.handler.worker :as worker-handler]
             [frontend.handler.worker :as worker-handler]
             [frontend.persist-db.protocol :as protocol]
             [frontend.persist-db.protocol :as protocol]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [frontend.util :as util]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
+            [missionary.core :as m]
             [promesa.core :as p]))
             [promesa.core :as p]))
 
 
 (defn- ask-persist-permission!
 (defn- ask-persist-permission!
@@ -25,17 +28,21 @@
 
 
 (defn- sync-app-state!
 (defn- sync-app-state!
   []
   []
-  (add-watch state/state
-             :sync-worker-state
-             (fn [_ _ prev current]
-               (let [new-state (cond-> {}
-                                 (not= (:git/current-repo prev)
-                                       (:git/current-repo current))
-                                 (assoc :git/current-repo (:git/current-repo current))
-                                 (not= (:config prev) (:config current))
-                                 (assoc :config (:config current)))]
-                 (when (seq new-state)
-                   (state/<invoke-db-worker :thread-api/sync-app-state new-state))))))
+  (let [state-flow
+        (->> (m/watch state/state)
+             (m/eduction
+              (map #(select-keys % [:git/current-repo :config
+                                    :auth/id-token :auth/access-token :auth/refresh-token]))
+              (dedupe)))
+        <init-sync-done? (p/deferred)
+        task (m/reduce
+              (constantly nil)
+              (m/ap
+                (let [m (m/?> (m/relieve state-flow))]
+                  (c.m/<? (state/<invoke-db-worker :thread-api/sync-app-state m))
+                  (p/resolve! <init-sync-done?))))]
+    (c.m/run-task* task)
+    <init-sync-done?))
 
 
 (defn get-route-data
 (defn get-route-data
   [route-match]
   [route-match]
@@ -56,9 +63,7 @@
                        old-state (f prev)
                        old-state (f prev)
                        new-state (f current)]
                        new-state (f current)]
                    (when (not= new-state old-state)
                    (when (not= new-state old-state)
-                     (state/<invoke-db-worker :thread-api/sync-ui-state
-                                              (state/get-current-repo)
-                                              {:old-state old-state :new-state new-state})))))))
+                     (undo-redo/record-ui-state! (state/get-current-repo) (ldb/write-transit-str {:old-state old-state :new-state new-state}))))))))
 
 
 (defn transact!
 (defn transact!
   [repo tx-data tx-meta]
   [repo tx-data tx-meta]
@@ -97,12 +102,9 @@
       (Comlink/expose #js{"remoteInvoke" thread-api/remote-function} worker)
       (Comlink/expose #js{"remoteInvoke" thread-api/remote-function} worker)
       (worker-handler/handle-message! worker wrapped-worker)
       (worker-handler/handle-message! worker wrapped-worker)
       (reset! state/*db-worker wrapped-worker)
       (reset! state/*db-worker wrapped-worker)
-      (-> (p/let [_ (state/<invoke-db-worker :thread-api/init config/RTC-WS-URL)
+      (-> (p/let [_ (sync-app-state!)
+                  _ (state/<invoke-db-worker :thread-api/init config/RTC-WS-URL)
                   _ (js/console.debug (str "debug: init worker spent: " (- (util/time-ms) t1) "ms"))
                   _ (js/console.debug (str "debug: init worker spent: " (- (util/time-ms) t1) "ms"))
-                  _ (state/<invoke-db-worker :thread-api/sync-app-state
-                                             {:git/current-repo (state/get-current-repo)
-                                              :config (:config @state/state)})
-                  _ (sync-app-state!)
                   _ (sync-ui-state!)
                   _ (sync-ui-state!)
                   _ (ask-persist-permission!)
                   _ (ask-persist-permission!)
                   _ (state/pub-event! [:graph/sync-context])]
                   _ (state/pub-event! [:graph/sync-context])]
@@ -112,12 +114,11 @@
                (db-transact/transact transact!
                (db-transact/transact transact!
                                      (if (string? repo) repo (state/get-current-repo))
                                      (if (string? repo) repo (state/get-current-repo))
                                      tx-data
                                      tx-data
-                                     tx-meta)))
+                                     (assoc tx-meta :client-id (:client-id @state/state)))))
             (db-transact/listen-for-requests))
             (db-transact/listen-for-requests))
           (p/catch (fn [error]
           (p/catch (fn [error]
                      (prn :debug "Can't init SQLite wasm")
                      (prn :debug "Can't init SQLite wasm")
-                     (js/console.error error)
-                     (notification/show! "It seems that OPFS is not supported on this browser, please upgrade this browser to the latest version or use another browser." :error)))))))
+                     (js/console.error error)))))))
 
 
 (defn <export-db!
 (defn <export-db!
   [repo data]
   [repo data]
@@ -133,11 +134,7 @@
 
 
 (defn- sqlite-error-handler
 (defn- sqlite-error-handler
   [error]
   [error]
-  (if (= "NoModificationAllowedError"  (.-name error))
-    (do
-      (js/console.error error)
-      (state/pub-event! [:show/multiple-tabs-error-dialog]))
-    (notification/show! [:div (str "SQLiteDB error: " error)] :error)))
+  (notification/show! [:div (str "SQLiteDB error: " error)] :error))
 
 
 (defrecord InBrowser []
 (defrecord InBrowser []
   protocol/PersistentDB
   protocol/PersistentDB

+ 14 - 11
src/main/frontend/state.cljs

@@ -33,6 +33,7 @@
 (defonce *profile-state (volatile! {}))
 (defonce *profile-state (volatile! {}))
 
 
 (defonce *db-worker (atom nil))
 (defonce *db-worker (atom nil))
+(defonce *editor-info (atom nil))
 
 
 (defn- <invoke-db-worker*
 (defn- <invoke-db-worker*
   [qkw direct-pass-args? args-list]
   [qkw direct-pass-args? args-list]
@@ -61,7 +62,8 @@
                          (when graph (ipc/ipc "setCurrentGraph" graph))
                          (when graph (ipc/ipc "setCurrentGraph" graph))
                          graph)]
                          graph)]
     (atom
     (atom
-     {:route-match                           nil
+     {:client-id                             (str (random-uuid))
+      :route-match                           nil
       :today                                 nil
       :today                                 nil
       :system/events                         (async/chan 1000)
       :system/events                         (async/chan 1000)
       :file/unlinked-dirs                    #{}
       :file/unlinked-dirs                    #{}
@@ -1037,16 +1039,6 @@ Similar to re-frame subscriptions"
   []
   []
   @(get @state :editor/block))
   @(get @state :editor/block))
 
 
-(defn set-edit-content!
-  ([input-id value] (set-edit-content! input-id value true))
-  ([input-id value set-input-value?]
-   (when input-id
-     (when set-input-value?
-       (when-let [input (gdom/getElement input-id)]
-         (util/set-change-value input value)))
-     (set-state! :editor/content value :path-in-sub-atom
-                 (or (:block/uuid (get-edit-block)) input-id)))))
-
 (defn editing?
 (defn editing?
   []
   []
   (seq @(:editor/editing? @state)))
   (seq @(:editor/editing? @state)))
@@ -1065,6 +1057,17 @@ Similar to re-frame subscriptions"
                 id))))
                 id))))
         (catch :default _e)))))
         (catch :default _e)))))
 
 
+(defn set-edit-content!
+  ([value] (set-edit-content! (get-edit-input-id) value))
+  ([input-id value] (set-edit-content! input-id value true))
+  ([input-id value set-input-value?]
+   (when input-id
+     (when set-input-value?
+       (when-let [input (gdom/getElement input-id)]
+         (util/set-change-value input value)))
+     (set-state! :editor/content value :path-in-sub-atom
+                 (or (:block/uuid (get-edit-block)) input-id)))))
+
 (defn get-input
 (defn get-input
   []
   []
   (when-let [id (get-edit-input-id)]
   (when-let [id (get-edit-input-id)]

+ 68 - 48
src/main/frontend/worker/undo_redo.cljs → src/main/frontend/undo_redo.cljs

@@ -1,13 +1,15 @@
-(ns frontend.worker.undo-redo
+(ns frontend.undo-redo
   "Undo redo new implementation"
   "Undo redo new implementation"
   (:require [clojure.set :as set]
   (:require [clojure.set :as set]
             [datascript.core :as d]
             [datascript.core :as d]
-            [frontend.worker.db-listener :as db-listener]
-            [frontend.worker.state :as worker-state]
+            [frontend.db :as db]
+            [frontend.state :as state]
+            [frontend.util :as util]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [malli.core :as m]
             [malli.core :as m]
-            [malli.util :as mu]))
+            [malli.util :as mu]
+            [promesa.core :as p]))
 
 
 (defkeywords
 (defkeywords
   ::record-editor-info {:doc "record current editor and cursor"}
   ::record-editor-info {:doc "record current editor and cursor"}
@@ -48,8 +50,8 @@
 (def ^:private undo-op-validator (m/validator [:sequential undo-op-item-schema]))
 (def ^:private undo-op-validator (m/validator [:sequential undo-op-item-schema]))
 
 
 (defonce max-stack-length 100)
 (defonce max-stack-length 100)
-(defonce *undo-ops (:undo/repo->ops @worker-state/*state))
-(defonce *redo-ops (:redo/repo->ops @worker-state/*state))
+(defonce *undo-ops (atom {}))
+(defonce *redo-ops (atom {}))
 
 
 (defn- conj-op
 (defn- conj-op
   [col op]
   [col op]
@@ -250,51 +252,60 @@
         (throw e)))))
         (throw e)))))
 
 
 (defn- undo-redo-aux
 (defn- undo-redo-aux
-  [repo conn undo?]
+  [repo undo?]
   (if-let [op (not-empty ((if undo? pop-undo-op pop-redo-op) repo))]
   (if-let [op (not-empty ((if undo? pop-undo-op pop-redo-op) repo))]
-    (cond
-      (= ::ui-state (ffirst op))
-      (do
-        ((if undo? push-redo-op push-undo-op) repo op)
-        (let [ui-state-str (second (first op))]
-          {:undo? undo?
-           :ui-state-str ui-state-str}))
-
-      :else
-      (let [{:keys [tx-data tx-meta] :as data} (some #(when (= ::db-transact (first %))
-                                                        (second %)) op)]
-        (when (seq tx-data)
-          (let [reversed-tx-data (get-reversed-datoms conn undo? data tx-meta)
-                tx-meta' (-> tx-meta
-                             (dissoc :pipeline-replace?
-                                     :batch-tx/batch-tx-mode?)
-                             (assoc
-                              :gen-undo-ops? false
-                              :undo? undo?))]
-            (when (seq reversed-tx-data)
-              (ldb/transact! conn reversed-tx-data tx-meta')
-              ((if undo? push-redo-op push-undo-op) repo op)
-              (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) op)
-                                        (map second))
-                    block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid
-                                                                              (if undo?
-                                                                                (first editor-cursors)
-                                                                                (last editor-cursors)))]))]
-                {:undo? undo?
-                 :editor-cursors editor-cursors
-                 :block-content block-content}))))))
+    (let [conn (db/get-db repo false)]
+      (cond
+        (= ::ui-state (ffirst op))
+        (do
+          ((if undo? push-redo-op push-undo-op) repo op)
+          (let [ui-state-str (second (first op))]
+            {:undo? undo?
+             :ui-state-str ui-state-str}))
+
+        :else
+        (let [{:keys [tx-data tx-meta] :as data} (some #(when (= ::db-transact (first %))
+                                                          (second %)) op)]
+          (when (seq tx-data)
+            (let [reversed-tx-data (get-reversed-datoms conn undo? data tx-meta)
+                  tx-meta' (-> tx-meta
+                               (dissoc :pipeline-replace?
+                                       :batch-tx/batch-tx-mode?)
+                               (assoc
+                                :gen-undo-ops? false
+                                :undo? undo?))
+                  handler (fn handler []
+                            ((if undo? push-redo-op push-undo-op) repo op)
+                            (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) op)
+                                                      (map second))
+                                  block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid
+                                                                                            (if undo?
+                                                                                              (first editor-cursors)
+                                                                                              (last editor-cursors)))]))]
+                              {:undo? undo?
+                               :editor-cursors editor-cursors
+                               :block-content block-content}))]
+              (when (seq reversed-tx-data)
+                (if util/node-test?
+                  (do
+                    (ldb/transact! conn reversed-tx-data tx-meta')
+                    (handler))
+                  (p/do!
+                   ;; async write to the master worker
+                   (ldb/transact! repo reversed-tx-data tx-meta')
+                   (handler)))))))))
 
 
     (when ((if undo? empty-undo-stack? empty-redo-stack?) repo)
     (when ((if undo? empty-undo-stack? empty-redo-stack?) repo)
       (prn (str "No further " (if undo? "undo" "redo") " information"))
       (prn (str "No further " (if undo? "undo" "redo") " information"))
       (if undo? ::empty-undo-stack ::empty-redo-stack))))
       (if undo? ::empty-undo-stack ::empty-redo-stack))))
 
 
 (defn undo
 (defn undo
-  [repo conn]
-  (undo-redo-aux repo conn true))
+  [repo]
+  (undo-redo-aux repo true))
 
 
 (defn redo
 (defn redo
-  [repo conn]
-  (undo-redo-aux repo conn false))
+  [repo]
+  (undo-redo-aux repo false))
 
 
 (defn record-editor-info!
 (defn record-editor-info!
   [repo editor-info]
   [repo editor-info]
@@ -312,13 +323,15 @@
   (when ui-state-str
   (when ui-state-str
     (push-undo-op repo [[::ui-state ui-state-str]])))
     (push-undo-op repo [[::ui-state ui-state-str]])))
 
 
-(defmethod db-listener/listen-db-changes :gen-undo-ops
-  [_ {:keys [repo]} {:keys [tx-data tx-meta db-after db-before]}]
+(defn gen-undo-ops!
+  [repo {:keys [tx-data tx-meta db-after db-before]}]
   (let [{:keys [outliner-op]} tx-meta]
   (let [{:keys [outliner-op]} tx-meta]
-    (when (and outliner-op (not (false? (:gen-undo-ops? tx-meta)))
-               (not (:create-today-journal? tx-meta)))
-      (let [editor-info (:editor-info tx-meta)
-            all-ids (distinct (map :e tx-data))
+    (when (and
+           (= (:client-id tx-meta) (:client-id @state/state))
+           outliner-op
+           (not (false? (:gen-undo-ops? tx-meta)))
+           (not (:create-today-journal? tx-meta)))
+      (let [all-ids (distinct (map :e tx-data))
             retracted-ids (set
             retracted-ids (set
                            (filter
                            (filter
                             (fn [id] (and (nil? (d/entity db-after id)) (d/entity db-before id)))
                             (fn [id] (and (nil? (d/entity db-after id)) (d/entity db-before id)))
@@ -329,6 +342,8 @@
                         all-ids))
                         all-ids))
             tx-data' (->> (remove (fn [d] (contains? #{:block/path-refs} (:a d))) tx-data)
             tx-data' (->> (remove (fn [d] (contains? #{:block/path-refs} (:a d))) tx-data)
                           vec)
                           vec)
+            editor-info @state/*editor-info
+            _ (reset! state/*editor-info nil)
             op (->> [(when editor-info [::record-editor-info editor-info])
             op (->> [(when editor-info [::record-editor-info editor-info])
                      [::db-transact
                      [::db-transact
                       {:tx-data tx-data'
                       {:tx-data tx-data'
@@ -338,3 +353,8 @@
                     (remove nil?)
                     (remove nil?)
                     vec)]
                     vec)]
         (push-undo-op repo op)))))
         (push-undo-op repo op)))))
+
+(defn listen-db-changes!
+  [repo conn]
+  (d/listen! conn ::gen-undo-ops
+             (fn [tx-report] (gen-undo-ops! repo tx-report))))

+ 3 - 12
src/main/frontend/util/page.cljs

@@ -1,8 +1,8 @@
 (ns frontend.util.page
 (ns frontend.util.page
   "Provides util fns for page blocks"
   "Provides util fns for page blocks"
-  (:require [frontend.state :as state]
-            [frontend.util :as util]
-            [frontend.db :as db]))
+  (:require [frontend.db :as db]
+            [frontend.state :as state]
+            [frontend.util :as util]))
 
 
 (defn get-current-page-name
 (defn get-current-page-name
   "Fetch the current page's original name with same approach as get-current-page-id"
   "Fetch the current page's original name with same approach as get-current-page-id"
@@ -23,15 +23,6 @@
   (let [page-name (state/get-current-page)]
   (let [page-name (state/get-current-page)]
     (:db/id (db/get-page page-name))))
     (:db/id (db/get-page page-name))))
 
 
-(defn get-latest-edit-page-id
-  "Fetch the editing page id. If there is an edit-input-id set, we are probably still
-   on editing mode"
-  []
-  (or
-    (get-in (first (state/get-editor-args)) [:block :block/page :db/id])
-    ;; not found
-    (get-current-page-id)))
-
 (defn get-page-file-rpath
 (defn get-page-file-rpath
   "Gets the file path of a page. If no page is given, detects the current page.
   "Gets the file path of a page. If no page is given, detects the current page.
 Returns nil if no file path is found or no page is detected or given"
 Returns nil if no file path is found or no page is detected or given"

+ 10 - 10
src/main/frontend/worker/db/validate.cljs

@@ -1,6 +1,6 @@
 (ns frontend.worker.db.validate
 (ns frontend.worker.db.validate
   "Validate db"
   "Validate db"
-  (:require [frontend.worker.util :as worker-util]
+  (:require [frontend.worker.shared-service :as shared-service]
             [logseq.db.frontend.validate :as db-validate]))
             [logseq.db.frontend.validate :as db-validate]))
 
 
 (defn validate-db
 (defn validate-db
@@ -8,16 +8,16 @@
   (let [{:keys [errors datom-count entities]} (db-validate/validate-db! db)]
   (let [{:keys [errors datom-count entities]} (db-validate/validate-db! db)]
     (if errors
     (if errors
       (do
       (do
-        (worker-util/post-message :log [:db-invalid :error
-                                        {:msg "Validation errors"
-                                         :errors errors}])
-        (worker-util/post-message :notification
-                                  [(str "Validation detected " (count errors) " invalid block(s). These blocks may be buggy. Attempting to fix invalid blocks. Run validation again to see if they were fixed.")
-                                   :warning false]))
+        (shared-service/broadcast-to-clients! :log [:db-invalid :error
+                                                    {:msg "Validation errors"
+                                                     :errors errors}])
+        (shared-service/broadcast-to-clients! :notification
+                                              [(str "Validation detected " (count errors) " invalid block(s). These blocks may be buggy. Attempting to fix invalid blocks. Run validation again to see if they were fixed.")
+                                               :warning false]))
 
 
-      (worker-util/post-message :notification
-                                [(str "Your graph is valid! " (assoc (db-validate/graph-counts db entities) :datoms datom-count))
-                                 :success false]))
+      (shared-service/broadcast-to-clients! :notification
+                                            [(str "Your graph is valid! " (assoc (db-validate/graph-counts db entities) :datoms datom-count))
+                                             :success false]))
     {:errors errors
     {:errors errors
      :datom-count datom-count
      :datom-count datom-count
      :invalid-entity-ids (distinct (map (fn [e] (:db/id (:entity e))) errors))}))
      :invalid-entity-ids (distinct (map (fn [e] (:db/id (:entity e))) errors))}))

+ 2 - 2
src/main/frontend/worker/db_listener.cljs

@@ -4,8 +4,8 @@
             [frontend.common.thread-api :as thread-api]
             [frontend.common.thread-api :as thread-api]
             [frontend.worker.pipeline :as worker-pipeline]
             [frontend.worker.pipeline :as worker-pipeline]
             [frontend.worker.search :as search]
             [frontend.worker.search :as search]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [logseq.common.util :as common-util]
             [logseq.common.util :as common-util]
             [logseq.outliner.batch-tx :as batch-tx]
             [logseq.outliner.batch-tx :as batch-tx]
             [promesa.core :as p]))
             [promesa.core :as p]))
@@ -26,7 +26,7 @@
                    :tx-data (:tx-data tx-report')
                    :tx-data (:tx-data tx-report')
                    :tx-meta tx-meta}
                    :tx-meta tx-meta}
                   (dissoc result :tx-report))]
                   (dissoc result :tx-report))]
-        (worker-util/post-message :sync-db-changes data))
+        (shared-service/broadcast-to-clients! :sync-db-changes data))
 
 
       (when-not from-disk?
       (when-not from-disk?
         (p/do!
         (p/do!

+ 106 - 65
src/main/frontend/worker/db_worker.cljs

@@ -11,6 +11,7 @@
             [datascript.storage :refer [IStorage] :as storage]
             [datascript.storage :refer [IStorage] :as storage]
             [frontend.common.cache :as common.cache]
             [frontend.common.cache :as common.cache]
             [frontend.common.graph-view :as graph-view]
             [frontend.common.graph-view :as graph-view]
+            [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :as thread-api :refer [def-thread-api]]
             [frontend.common.thread-api :as thread-api :refer [def-thread-api]]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.db.fix :as db-fix]
             [frontend.worker.db.fix :as db-fix]
@@ -22,12 +23,11 @@
             [frontend.worker.handler.page.file-based.rename :as file-worker-page-rename]
             [frontend.worker.handler.page.file-based.rename :as file-worker-page-rename]
             [frontend.worker.rtc.asset-db-listener]
             [frontend.worker.rtc.asset-db-listener]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.client-op :as client-op]
-            [frontend.worker.rtc.core]
+            [frontend.worker.rtc.core :as rtc.core]
             [frontend.worker.rtc.db-listener]
             [frontend.worker.rtc.db-listener]
             [frontend.worker.search :as search]
             [frontend.worker.search :as search]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.thread-atom]
-            [frontend.worker.undo-redo :as undo-redo]
             [frontend.worker.util :as worker-util]
             [frontend.worker.util :as worker-util]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [lambdaisland.glogi.console :as glogi-console]
             [lambdaisland.glogi.console :as glogi-console]
@@ -44,6 +44,7 @@
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.op :as outliner-op]
             [logseq.outliner.op :as outliner-op]
             [me.tonsky.persistent-sorted-set :as set :refer [BTSet]]
             [me.tonsky.persistent-sorted-set :as set :refer [BTSet]]
+            [missionary.core :as m]
             [promesa.core :as p]))
             [promesa.core :as p]))
 
 
 (defonce *sqlite worker-state/*sqlite)
 (defonce *sqlite worker-state/*sqlite)
@@ -449,24 +450,36 @@
   [repo]
   [repo]
   (worker-state/get-sqlite-conn repo :search))
   (worker-state/get-sqlite-conn repo :search))
 
 
-(def-thread-api :thread-api/get-version
-  []
-  (when-let [sqlite @*sqlite]
-    (.-version sqlite)))
+(comment
+  (def-thread-api :thread-api/get-version
+    []
+    (when-let [sqlite @*sqlite]
+      (.-version sqlite))))
 
 
 (def-thread-api :thread-api/init
 (def-thread-api :thread-api/init
   [rtc-ws-url]
   [rtc-ws-url]
   (reset! worker-state/*rtc-ws-url rtc-ws-url)
   (reset! worker-state/*rtc-ws-url rtc-ws-url)
   (init-sqlite-module!))
   (init-sqlite-module!))
 
 
+;; [graph service]
+(defonce *service (atom []))
+(defonce fns {"remoteInvoke" thread-api/remote-function})
+(declare <init-service!)
+
+(defn- start-db!
+  [repo {:keys [close-other-db?]
+         :or {close-other-db? true}
+         :as opts}]
+  (p/do!
+   (when close-other-db?
+     (close-other-dbs! repo))
+   (when @shared-service/*master-client?
+     (create-or-open-db! repo (dissoc opts :close-other-db?)))
+   nil))
+
 (def-thread-api :thread-api/create-or-open-db
 (def-thread-api :thread-api/create-or-open-db
   [repo opts]
   [repo opts]
-  (let [{:keys [close-other-db?] :or {close-other-db? true} :as opts} opts]
-    (p/do!
-     (when close-other-db?
-       (close-other-dbs! repo))
-     (create-or-open-db! repo (dissoc opts :close-other-db?))
-     nil)))
+  (start-db! repo opts))
 
 
 (def-thread-api :thread-api/q
 (def-thread-api :thread-api/q
   [repo inputs]
   [repo inputs]
@@ -595,16 +608,6 @@
   (when-let [conn (worker-state/get-datascript-conn repo)]
   (when-let [conn (worker-state/get-datascript-conn repo)]
     (sqlite-common-db/get-initial-data @conn)))
     (sqlite-common-db/get-initial-data @conn)))
 
 
-(def-thread-api :thread-api/get-page-refs-count
-  [repo]
-  (when-let [conn (worker-state/get-datascript-conn repo)]
-    (sqlite-common-db/get-page->refs-count @conn)))
-
-(def-thread-api :thread-api/close-db
-  [repo]
-  (close-db! repo)
-  nil)
-
 (def-thread-api :thread-api/reset-db
 (def-thread-api :thread-api/reset-db
   [repo db-transit]
   [repo db-transit]
   (reset-db! repo db-transit)
   (reset-db! repo db-transit)
@@ -685,7 +688,7 @@
               {:keys [type payload]} (when (map? data) data)]
               {:keys [type payload]} (when (map? data) data)]
           (case type
           (case type
             :notification
             :notification
-            (worker-util/post-message type [(:message payload) (:type payload)])
+            (shared-service/broadcast-to-clients! :notification [(:message payload) (:type payload)])
             (throw e)))))))
             (throw e)))))))
 
 
 (def-thread-api :thread-api/file-writes-finished?
 (def-thread-api :thread-api/file-writes-finished?
@@ -714,11 +717,6 @@
   (worker-state/set-new-state! new-state)
   (worker-state/set-new-state! new-state)
   nil)
   nil)
 
 
-(def-thread-api :thread-api/sync-ui-state
-  [repo state]
-  (undo-redo/record-ui-state! repo (ldb/write-transit-str state))
-  nil)
-
 (def-thread-api :thread-api/export-get-debug-datoms
 (def-thread-api :thread-api/export-get-debug-datoms
   [repo]
   [repo]
   (when-let [db (worker-state/get-sqlite-conn repo)]
   (when-let [db (worker-state/get-sqlite-conn repo)]
@@ -735,21 +733,6 @@
   (when-let [conn (worker-state/get-datascript-conn repo)]
   (when-let [conn (worker-state/get-datascript-conn repo)]
     (worker-export/get-all-page->content repo @conn options)))
     (worker-export/get-all-page->content repo @conn options)))
 
 
-(def-thread-api :thread-api/undo
-  [repo _page-block-uuid-str]
-  (when-let [conn (worker-state/get-datascript-conn repo)]
-    (undo-redo/undo repo conn)))
-
-(def-thread-api :thread-api/redo
-  [repo _page-block-uuid-str]
-  (when-let [conn (worker-state/get-datascript-conn repo)]
-    (undo-redo/redo repo conn)))
-
-(def-thread-api :thread-api/record-editor-info
-  [repo _page-block-uuid-str editor-info]
-  (undo-redo/record-editor-info! repo editor-info)
-  nil)
-
 (def-thread-api :thread-api/validate-db
 (def-thread-api :thread-api/validate-db
   [repo]
   [repo]
   (when-let [conn (worker-state/get-datascript-conn repo)]
   (when-let [conn (worker-state/get-datascript-conn repo)]
@@ -804,11 +787,6 @@
   [repo]
   [repo]
   (get-all-page-titles-with-cache repo))
   (get-all-page-titles-with-cache repo))
 
 
-(def-thread-api :thread-api/update-auth-tokens
-  [id-token access-token refresh-token]
-  (worker-state/set-auth-tokens! id-token access-token refresh-token)
-  nil)
-
 (comment
 (comment
   (def-thread-api :general/dangerousRemoveAllDbs
   (def-thread-api :general/dangerousRemoveAllDbs
     []
     []
@@ -858,25 +836,88 @@
              (file/write-files! conn col (worker-state/get-context)))
              (file/write-files! conn col (worker-state/get-context)))
            (js/console.error (str "DB is not found for " repo))))))))
            (js/console.error (str "DB is not found for " repo))))))))
 
 
+(defn- on-become-master
+  [repo]
+  (js/Promise.
+   (m/sp
+     (c.m/<? (init-sqlite-module!))
+     (c.m/<? (start-db! repo {}))
+     (assert (some? (worker-state/get-datascript-conn repo)))
+     (m/? (rtc.core/new-task--rtc-start true)))))
+
+(def broadcast-data-types
+  (set (map
+        common-util/keyword->string
+        [:sync-db-changes
+         :notification
+         :log
+         :add-repo
+         :rtc-log
+         :rtc-sync-state])))
+
+(defn- <init-service!
+  [graph]
+  (let [[prev-graph service] @*service]
+    (some-> prev-graph close-db!)
+    (when graph
+      (if (= graph prev-graph)
+        service
+        (p/let [service (shared-service/<create-service graph
+                                                        (bean/->js fns)
+                                                        #(on-become-master graph)
+                                                        broadcast-data-types)]
+          (assert (p/promise? (get-in service [:status :ready])))
+          (reset! *service [graph service])
+          service)))))
+
 (defn init
 (defn init
   "web worker entry"
   "web worker entry"
   []
   []
-  (glogi-console/install!)
-  (check-worker-scope!)
-  (outliner-register-op-handlers!)
-  (<ratelimit-file-writes!)
-  (js/setInterval #(.postMessage js/self "keepAliveResponse") (* 1000 25))
-  (Comlink/expose #js{"remoteInvoke" thread-api/remote-function})
-  (let [^js wrapped-main-thread* (Comlink/wrap js/self)
-        wrapped-main-thread (fn [qkw direct-pass-args? & args]
-                              (-> (.remoteInvoke wrapped-main-thread*
-                                                 (str (namespace qkw) "/" (name qkw))
-                                                 direct-pass-args?
-                                                 (if direct-pass-args?
-                                                   (into-array args)
-                                                   (ldb/write-transit-str args)))
-                                  (p/chain ldb/read-transit-str)))]
-    (reset! worker-state/*main-thread wrapped-main-thread)))
+  (let [proxy-object (->>
+                      fns
+                      (map
+                       (fn [[k f]]
+                         [k
+                          (fn [& args]
+                            (let [[_graph service] @*service
+                                  method-k (keyword (first args))]
+                              (cond
+                                (= :thread-api/create-or-open-db method-k)
+                                ;; because shared-service operates at the graph level,
+                                ;; creating a new database or switching to another one requires re-initializing the service.
+                                (p/let [method-args (ldb/read-transit-str (last args))
+                                        service (<init-service! (first method-args))]
+                                  ;; wait for service ready
+                                  (get-in service [:status :ready])
+                                  (js-invoke (:proxy service) k args))
+
+                                (or (contains? #{:thread-api/sync-app-state} method-k)
+                                    (nil? service))
+                                ;; only proceed down this branch before shared-service is initialized
+                                (apply f args)
+
+                                :else
+                                ;; ensure service is ready
+                                (p/let [_ready-value (get-in service [:status :ready])]
+                                  (js-invoke (:proxy service) k args)))))]))
+                      (into {})
+                      bean/->js)]
+    (glogi-console/install!)
+    (check-worker-scope!)
+    (outliner-register-op-handlers!)
+    (<ratelimit-file-writes!)
+    (js/setInterval #(.postMessage js/self "keepAliveResponse") (* 1000 25))
+    (Comlink/expose proxy-object)
+    (let [^js wrapped-main-thread* (Comlink/wrap js/self)
+          wrapped-main-thread (fn [qkw direct-pass-args? & args]
+                                (-> (.remoteInvoke wrapped-main-thread*
+                                                   (str (namespace qkw) "/" (name qkw))
+                                                   direct-pass-args?
+                                                   (if direct-pass-args?
+                                                     (into-array args)
+                                                     (ldb/write-transit-str args)))
+                                    (p/chain ldb/read-transit-str)))]
+      (reset! worker-state/*main-thread wrapped-main-thread))))
 
 
 (comment
 (comment
   (defn <remove-all-files!
   (defn <remove-all-files!

+ 3 - 2
src/main/frontend/worker/pipeline.cljs

@@ -5,6 +5,7 @@
             [frontend.worker.commands :as commands]
             [frontend.worker.commands :as commands]
             [frontend.worker.file :as file]
             [frontend.worker.file :as file]
             [frontend.worker.react :as worker-react]
             [frontend.worker.react :as worker-react]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [frontend.worker.util :as worker-util]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.defkeywords :refer [defkeywords]]
@@ -106,8 +107,8 @@
                    true
                    true
                    (db-validate/validate-tx-report! tx-report (:validate-db-options context)))]
                    (db-validate/validate-tx-report! tx-report (:validate-db-options context)))]
       (when (and (get-in context [:validate-db-options :fail-invalid?]) (not valid?))
       (when (and (get-in context [:validate-db-options :fail-invalid?]) (not valid?))
-        (worker-util/post-message :notification
-                                  [["Invalid DB!"] :error]))))
+        (shared-service/broadcast-to-clients! :notification
+                                              [["Invalid DB!"] :error]))))
 
 
   ;; Ensure :block/order is unique for any block that has :block/parent
   ;; Ensure :block/order is unique for any block that has :block/parent
   (when (or (:dev? context) (exists? js/process))
   (when (or (:dev? context) (exists? js/process))

+ 40 - 14
src/main/frontend/worker/rtc/core.cljs

@@ -17,6 +17,7 @@
             [frontend.worker.rtc.skeleton]
             [frontend.worker.rtc.skeleton]
             [frontend.worker.rtc.ws :as ws]
             [frontend.worker.rtc.ws :as ws]
             [frontend.worker.rtc.ws-util :as ws-util :refer [gen-get-ws-create-map--memoized]]
             [frontend.worker.rtc.ws-util :as ws-util :refer [gen-get-ws-create-map--memoized]]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [frontend.worker.util :as worker-util]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
@@ -337,7 +338,7 @@
                                   :repo repo})))
                                   :repo repo})))
 
 
 ;;; ================ API ================
 ;;; ================ API ================
-(defn new-task--rtc-start
+(defn- new-task--rtc-start*
   [repo token]
   [repo token]
   (m/sp
   (m/sp
     ;; ensure device metadata existing first
     ;; ensure device metadata existing first
@@ -345,7 +346,7 @@
     (let [{:keys [conn user-uuid graph-uuid schema-version remote-schema-version date-formatter] :as r}
     (let [{:keys [conn user-uuid graph-uuid schema-version remote-schema-version date-formatter] :as r}
           (validate-rtc-start-conditions repo token)]
           (validate-rtc-start-conditions repo token)]
       (if (instance? ExceptionInfo r)
       (if (instance? ExceptionInfo r)
-        (do (log/info :e r) (r.ex/->map r))
+        r
         (let [{:keys [rtc-state-flow *rtc-auto-push? *rtc-remote-profile? rtc-loop-task *online-users onstarted-task]}
         (let [{:keys [rtc-state-flow *rtc-auto-push? *rtc-remote-profile? rtc-loop-task *online-users onstarted-task]}
               (create-rtc-loop graph-uuid schema-version repo conn date-formatter token)
               (create-rtc-loop graph-uuid schema-version repo conn date-formatter token)
               *last-stop-exception (atom nil)
               *last-stop-exception (atom nil)
@@ -355,8 +356,8 @@
                                  (reset! *last-stop-exception e)
                                  (reset! *last-stop-exception e)
                                  (log/info :rtc-loop-task e)))
                                  (log/info :rtc-loop-task e)))
               start-ex (m/? onstarted-task)]
               start-ex (m/? onstarted-task)]
-          (if-let [start-ex (:ex-data start-ex)]
-            (do (log/info :start-ex start-ex) (r.ex/->map start-ex))
+          (if (instance? ExceptionInfo start-ex)
+            start-ex
             (do (reset! *rtc-loop-metadata {:repo repo
             (do (reset! *rtc-loop-metadata {:repo repo
                                             :graph-uuid graph-uuid
                                             :graph-uuid graph-uuid
                                             :local-graph-schema-version schema-version
                                             :local-graph-schema-version schema-version
@@ -371,6 +372,29 @@
                                             :*last-stop-exception *last-stop-exception})
                                             :*last-stop-exception *last-stop-exception})
                 nil)))))))
                 nil)))))))
 
 
+(declare rtc-stop)
+(defn new-task--rtc-start
+  [stop-before-start?]
+  (m/sp
+    (let [repo (worker-state/get-current-repo)
+          token (worker-state/get-id-token)
+          conn (worker-state/get-datascript-conn repo)]
+      (when (and repo token conn)
+        (when stop-before-start? (rtc-stop))
+        (let [ex (m/? (new-task--rtc-start* repo token))]
+          (when-let [ex-data* (ex-data ex)]
+            (case (:type ex-data*)
+              (:rtc.exception/not-rtc-graph
+               :rtc.exception/major-schema-version-mismatched
+               :rtc.exception/lock-failed)
+              (log/info :rtc-start-failed ex)
+
+              :rtc.exception/not-found-db-conn
+              (log/error :rtc-start-failed ex)
+
+              (log/error :BUG-unknown-error ex))
+            (r.ex/->map ex)))))))
+
 (defn rtc-stop
 (defn rtc-stop
   []
   []
   (when-let [canceler (:canceler @*rtc-loop-metadata)]
   (when-let [canceler (:canceler @*rtc-loop-metadata)]
@@ -520,10 +544,11 @@
   (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
   (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (r.upload-download/new-task--request-download-graph get-ws-create-task graph-uuid schema-version)))
     (r.upload-download/new-task--request-download-graph get-ws-create-task graph-uuid schema-version)))
 
 
-(defn new-task--download-info-list
-  [token graph-uuid schema-version]
-  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
-    (r.upload-download/new-task--download-info-list get-ws-create-task graph-uuid schema-version)))
+(comment
+  (defn new-task--download-info-list
+    [token graph-uuid schema-version]
+    (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
+      (r.upload-download/new-task--download-info-list get-ws-create-task graph-uuid schema-version))))
 
 
 (defn new-task--wait-download-info-ready
 (defn new-task--wait-download-info-ready
   [token download-info-uuid graph-uuid schema-version timeout-ms]
   [token download-info-uuid graph-uuid schema-version timeout-ms]
@@ -534,8 +559,8 @@
 (def new-task--download-graph-from-s3 r.upload-download/new-task--download-graph-from-s3)
 (def new-task--download-graph-from-s3 r.upload-download/new-task--download-graph-from-s3)
 
 
 (def-thread-api :thread-api/rtc-start
 (def-thread-api :thread-api/rtc-start
-  [repo token]
-  (new-task--rtc-start repo token))
+  [stop-before-start?]
+  (new-task--rtc-start stop-before-start?))
 
 
 (def-thread-api :thread-api/rtc-stop
 (def-thread-api :thread-api/rtc-stop
   []
   []
@@ -595,9 +620,10 @@
   [graph-uuid graph-name s3-url]
   [graph-uuid graph-name s3-url]
   (new-task--download-graph-from-s3 graph-uuid graph-name s3-url))
   (new-task--download-graph-from-s3 graph-uuid graph-name s3-url))
 
 
-(def-thread-api :thread-api/rtc-download-info-list
-  [token graph-uuid schema-version]
-  (new-task--download-info-list token graph-uuid schema-version))
+(comment
+  (def-thread-api :thread-api/rtc-download-info-list
+    [token graph-uuid schema-version]
+    (new-task--download-info-list token graph-uuid schema-version)))
 
 
 (def-thread-api :thread-api/rtc-add-migration-client-ops
 (def-thread-api :thread-api/rtc-add-migration-client-ops
   [repo server-schema-version]
   [repo server-schema-version]
@@ -611,7 +637,7 @@
   (c.m/run-background-task
   (c.m/run-background-task
    ::subscribe-state
    ::subscribe-state
    (m/reduce
    (m/reduce
-    (fn [_ v] (worker-util/post-message :rtc-sync-state v))
+    (fn [_ v] (shared-service/broadcast-to-clients! :rtc-sync-state v))
     create-get-state-flow)))
     create-get-state-flow)))
 
 
 (comment
 (comment

+ 9 - 7
src/main/frontend/worker/rtc/full_upload_download_graph.cljs

@@ -13,6 +13,7 @@
             [frontend.worker.rtc.const :as rtc-const]
             [frontend.worker.rtc.const :as rtc-const]
             [frontend.worker.rtc.log-and-state :as rtc-log-and-state]
             [frontend.worker.rtc.log-and-state :as rtc-log-and-state]
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.rtc.ws-util :as ws-util]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [frontend.worker.util :as worker-util]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
@@ -395,7 +396,7 @@
                          :persist-op? false} (worker-state/get-context))
                          :persist-op? false} (worker-state/get-context))
           (transact-remote-schema-version! repo)
           (transact-remote-schema-version! repo)
           (transact-block-refs! repo))))
           (transact-block-refs! repo))))
-      (worker-util/post-message :add-repo {:repo repo}))))
+      (shared-service/broadcast-to-clients! :add-repo {:repo repo}))))
 
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; async download-graph ;;
 ;; async download-graph ;;
@@ -412,12 +413,13 @@
                                                  :graph-uuid graph-uuid
                                                  :graph-uuid graph-uuid
                                                  :schema-version (str schema-version)})))
                                                  :schema-version (str schema-version)})))
 
 
-(defn new-task--download-info-list
-  [get-ws-create-task graph-uuid schema-version]
-  (m/join :download-info-list
-          (ws-util/send&recv get-ws-create-task {:action "download-info-list"
-                                                 :graph-uuid graph-uuid
-                                                 :schema-version (str schema-version)})))
+(comment
+  (defn new-task--download-info-list
+    [get-ws-create-task graph-uuid schema-version]
+    (m/join :download-info-list
+            (ws-util/send&recv get-ws-create-task {:action "download-info-list"
+                                                   :graph-uuid graph-uuid
+                                                   :schema-version (str schema-version)}))))
 
 
 (defn new-task--wait-download-info-ready
 (defn new-task--wait-download-info-ready
   [get-ws-create-task download-info-uuid graph-uuid schema-version timeout-ms]
   [get-ws-create-task download-info-uuid graph-uuid schema-version timeout-ms]

+ 3 - 2
src/main/frontend/worker/rtc/log_and_state.cljs

@@ -1,7 +1,7 @@
 (ns frontend.worker.rtc.log-and-state
 (ns frontend.worker.rtc.log-and-state
   "Fns to generate rtc related logs"
   "Fns to generate rtc related logs"
   (:require [frontend.common.missionary :as c.m]
   (:require [frontend.common.missionary :as c.m]
-            [frontend.worker.util :as worker-util]
+            [frontend.worker.shared-service :as shared-service]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.defkeywords :refer [defkeywords]]
             [malli.core :as ma]
             [malli.core :as ma]
@@ -86,9 +86,10 @@
   (swap! *graph-uuid->remote-t assoc (ensure-uuid graph-uuid) remote-t))
   (swap! *graph-uuid->remote-t assoc (ensure-uuid graph-uuid) remote-t))
 
 
 ;;; subscribe-logs, push to frontend
 ;;; subscribe-logs, push to frontend
+;;; TODO: refactor by using c.m/run-background-task
 (defn- subscribe-logs
 (defn- subscribe-logs
   []
   []
   (remove-watch *rtc-log :subscribe-logs)
   (remove-watch *rtc-log :subscribe-logs)
   (add-watch *rtc-log :subscribe-logs
   (add-watch *rtc-log :subscribe-logs
-             (fn [_ _ _ n] (when n (worker-util/post-message :rtc-log n)))))
+             (fn [_ _ _ n] (when n (shared-service/broadcast-to-clients! :rtc-log n)))))
 (subscribe-logs)
 (subscribe-logs)

+ 13 - 13
src/main/frontend/worker/rtc/skeleton.cljs

@@ -3,7 +3,7 @@
   (:require [clojure.data :as data]
   (:require [clojure.data :as data]
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.rtc.ws-util :as ws-util]
-            [frontend.worker.util :as worker-util]
+            [frontend.worker.shared-service :as shared-service]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.schema :as db-schema]
@@ -37,19 +37,19 @@
               client-builtin-db-idents (set (get-builtin-db-idents db))
               client-builtin-db-idents (set (get-builtin-db-idents db))
               client-schema-version (ldb/get-graph-schema-version db)]
               client-schema-version (ldb/get-graph-schema-version db)]
           (when-not (zero? (db-schema/compare-schema-version client-schema-version server-schema-version))
           (when-not (zero? (db-schema/compare-schema-version client-schema-version server-schema-version))
-            (worker-util/post-message :notification
-                                      [[:div
-                                        [:p (str :client-schema-version client-schema-version)]
-                                        [:p (str :server-schema-version server-schema-version)]]
-                                       :error]))
+            (shared-service/broadcast-to-clients! :notification
+                                                  [[:div
+                                                    [:p (str :client-schema-version client-schema-version)]
+                                                    [:p (str :server-schema-version server-schema-version)]]
+                                                   :error]))
           (let [[client-only server-only _]
           (let [[client-only server-only _]
                 (data/diff client-builtin-db-idents server-builtin-db-idents)]
                 (data/diff client-builtin-db-idents server-builtin-db-idents)]
             (when (or (seq client-only) (seq server-only))
             (when (or (seq client-only) (seq server-only))
-              (worker-util/post-message :notification
-                                        [(cond-> [:div]
-                                           (seq client-only)
-                                           (conj [:p (str :client-only-db-idents client-only)])
-                                           (seq server-only)
-                                           (conj [:p (str :server-only-db-idents server-only)]))
-                                         :error])))
+              (shared-service/broadcast-to-clients! :notification
+                                                    [(cond-> [:div]
+                                                       (seq client-only)
+                                                       (conj [:p (str :client-only-db-idents client-only)])
+                                                       (seq server-only)
+                                                       (conj [:p (str :server-only-db-idents server-only)]))
+                                                     :error])))
           r)))))
           r)))))

+ 360 - 0
src/main/frontend/worker/shared_service.cljs

@@ -0,0 +1,360 @@
+(ns frontend.worker.shared-service
+  "This allows multiple workers to share some resources (e.g. db access)"
+  (:require [cljs-bean.core :as bean]
+            [goog.object :as gobj]
+            [lambdaisland.glogi :as log]
+            [logseq.common.util :as common-util]
+            [logseq.db :as ldb]
+            [promesa.core :as p]))
+
+;; Idea and code copied from https://github.com/Matt-TOTW/shared-service/blob/master/src/sharedService.ts
+;; Related thread: https://github.com/rhashimoto/wa-sqlite/discussions/81
+
+(log/set-level 'frontend.worker.shared-service :debug)
+
+(defonce *master-client? (atom false))
+
+(defonce *master-re-check-trigger (atom nil))
+
+;;; common-channel - Communication related to master-client election.
+;;; client-channel - For API request-response data communication.
+;;; master-slave-channels - Registered slave channels for master, all the slave
+;;;                         channels need to be closed to not receive further
+;;;                         messages when the master has been changed to slave.
+(defonce *common-channel (atom nil))
+(defonce *client-channel (atom nil))
+(defonce *master-slave-channels (atom #{}))
+
+;;; record channel-listener here, to able to remove old listener before we addEventListener new one
+(defonce *common-channel-listener (atom nil))
+(defonce *client-channel-listener (atom nil))
+
+(defonce *current-request-id (volatile! 0))
+(defonce *requests-in-flight (volatile! (sorted-map))) ;sort by request-id
+;;; The unique identity of the context where `js/navigator.locks.request` is called
+(defonce *client-id (atom nil))
+(defonce *master-client-lock (atom nil))
+
+(defn- next-request-id
+  []
+  (vswap! *current-request-id inc))
+
+(defn- release-master-client-lock!
+  []
+  (when-let [d @*master-client-lock]
+    (p/resolve! d)
+    nil))
+
+(defn- get-broadcast-channel-name [client-id service-name]
+  (str client-id "-" service-name))
+
+(defn- random-id
+  []
+  (str (random-uuid)))
+
+(defn- do-not-wait
+  [promise]
+  promise
+  nil)
+
+(defn- <get-client-id
+  []
+  (let [id (random-id)]
+    (p/let [client-id (js/navigator.locks.request id #js {:mode "exclusive"}
+                                                  (fn [_]
+                                                    (p/let [^js locks (js/navigator.locks.query)]
+                                                      (->> (.-held locks)
+                                                           (some #(when (= (.-name %) id) %))
+                                                           .-clientId))))]
+      (assert (some? client-id))
+      (do-not-wait
+       (js/navigator.locks.request client-id #js {:mode "exclusive"}
+                                   ;; never release it
+                                   (fn [_] (p/deferred))))
+      (log/debug :client-id client-id)
+      client-id)))
+
+(defn- <ensure-client-id
+  []
+  (or @*client-id
+      (p/let [client-id (<get-client-id)]
+        (reset! *client-id client-id))))
+
+(defn- ensure-common-channel
+  [service-name]
+  (or @*common-channel
+      (reset! *common-channel (js/BroadcastChannel. (str "shared-service-common-channel-" service-name)))))
+
+(defn- ensure-client-channel
+  [slave-client-id service-name]
+  (or @*client-channel
+      (reset! *client-channel (js/BroadcastChannel. (get-broadcast-channel-name slave-client-id service-name)))))
+
+(defn- listen-common-channel
+  [common-channel listener-fn]
+  (when-let [old-listener @*common-channel-listener]
+    (.removeEventListener common-channel "message" old-listener))
+  (reset! *common-channel-listener listener-fn)
+  (.addEventListener common-channel "message" listener-fn))
+
+(defn- listen-client-channel
+  [client-channel listener-fn]
+  (when-let [old-listener @*client-channel-listener]
+    (.removeEventListener client-channel "message" old-listener))
+  (reset! *client-channel-listener listener-fn)
+  (.addEventListener client-channel "message" listener-fn))
+
+(defn- <apply-target-f!
+  [target method args]
+  (let [f (gobj/get target method)]
+    (assert (some? f) {:method method})
+    (apply f args)))
+
+(defn- <check-master-or-slave-client!
+  "Check if the current client is the master (otherwise, it is a slave)"
+  [service-name <on-become-master <on-become-slave]
+  (p/let [client-id (<ensure-client-id)]
+    (do-not-wait
+     (js/navigator.locks.request
+      service-name #js {:mode "exclusive", :ifAvailable true}
+      (fn [lock]
+        (p/let [^js locks (js/navigator.locks.query)
+                locked? (some #(when (and (= (.-name %) service-name)
+                                          (= (.-clientId %) client-id))
+                                 true)
+                              (.-held locks))]
+          (cond
+            (and locked? lock) ;become master
+            (p/do!
+             (reset! *master-client? true)
+             (<on-become-master)
+             (reset! *master-client-lock (p/deferred))
+              ;; Keep lock until context destroyed
+             @*master-client-lock)
+
+            (and locked? (nil? lock)) ;already locked by this client, do nothing
+            (assert (true? @*master-client?))
+
+            (not locked?) ;become slave
+            (p/do!
+             (reset! *master-client? false)
+             (<on-become-slave)))))))))
+
+(defn- clear-old-service!
+  []
+  (release-master-client-lock!)
+  (reset! *master-client? false)
+  (let [channels (into @*master-slave-channels [@*common-channel @*client-channel])]
+    (doseq [^js channel channels]
+      (when channel
+        (.close channel))))
+  (reset! *common-channel nil)
+  (reset! *client-channel nil)
+  (reset! *master-slave-channels #{})
+  (reset! *common-channel-listener nil)
+  (reset! *client-channel-listener nil)
+  (vreset! *requests-in-flight (sorted-map))
+  (remove-watch *master-re-check-trigger :check-master))
+
+(defn- on-response-handler
+  [event]
+  (let [{:keys [id type error result]} (bean/->clj (.-data event))]
+    (when (identical? "response" type)
+      (when-let [{:keys [resolve-fn reject-fn]} (get @*requests-in-flight id)]
+        (vswap! *requests-in-flight dissoc id)
+        (if error
+          (do (log/error :error-process-request error)
+              (reject-fn error))
+          (resolve-fn result))))))
+
+(defn- create-on-request-handler
+  [client-channel target]
+  (fn [event]
+    (let [{:keys [type method args id]} (bean/->clj (.-data event))]
+      (when (identical? "request" type)
+        (p/let [[result error]
+                (-> (p/then (<apply-target-f! target method args)
+                            (fn [res] [res nil]))
+                    (p/catch
+                     (fn [e] [nil (if (instance? js/Error e)
+                                    (bean/->clj e)
+                                    e)])))]
+          (.postMessage client-channel (bean/->js
+                                        {:id id
+                                         :type "response"
+                                         :result result
+                                         :error error
+                                         :method-key (first args)})))))))
+
+(defn- <slave-registered-handler
+  [service-name slave-client-id event *register-finish-promise?]
+  (let [slave-client-id* (:slave-client-id event)]
+    (when (= slave-client-id slave-client-id*)
+      (p/let [^js locks (js/navigator.locks.query)
+              already-watching?
+              (some
+               (fn [l] (and (= service-name (.-name l))
+                            (= slave-client-id (.-clientId l))))
+               (.-pending locks))]
+        (when-not already-watching?     ;dont watch multiple times
+          (do-not-wait
+           (js/navigator.locks.request service-name #js {:mode "exclusive"}
+                                       (fn [_lock]
+                                         ;; The master has gone, elect the new master
+                                         (log/debug "master has gone" nil)
+                                         (reset! *master-re-check-trigger :re-check)))))
+        (p/resolve! @*register-finish-promise?)))))
+
+(defn- <re-requests-in-flight-on-slave!
+  [client-channel]
+  (when (seq @*requests-in-flight)
+    (log/debug "Requests were in flight when master changed. Requeuing..." (count @*requests-in-flight))
+    (->>
+     @*requests-in-flight
+     (p/run!
+      (fn [[id {:keys [method args _resolve-fn _reject-fn]}]]
+        (.postMessage client-channel (bean/->js {:id id
+                                                 :type "request"
+                                                 :method method
+                                                 :args args})))))))
+
+(defn- <re-requests-in-flight-on-master!
+  [target]
+  (when (seq @*requests-in-flight)
+    (log/debug "Requests were in flight when tab became master. Requeuing..." (count @*requests-in-flight))
+    (->>
+     @*requests-in-flight
+     (p/run!
+      (fn [[id {:keys [method args resolve-fn reject-fn]}]]
+        (->
+         (p/let [result (<apply-target-f! target method args)]
+           (resolve-fn result))
+         (p/catch (fn [e]
+                    (log/error "Error processing request" e)
+                    (reject-fn e)))
+         (p/finally (fn []
+                      (vswap! *requests-in-flight dissoc id)))))))))
+
+(defn- <on-become-slave
+  [slave-client-id service-name common-channel broadcast-data-types status-ready-promise]
+  (let [client-channel (ensure-client-channel slave-client-id service-name)
+        *register-finish-promise? (atom nil)
+        <register #(do (.postMessage common-channel #js {:type "slave-register"
+                                                         :slave-client-id slave-client-id})
+                       (reset! *register-finish-promise? (p/deferred))
+                       @*register-finish-promise?)]
+    (listen-client-channel client-channel on-response-handler)
+    (listen-common-channel
+     common-channel
+     (fn [event]
+       (let [{:keys [type data] :as event*} (bean/->clj (.-data event))]
+         (if (contains? broadcast-data-types type)
+           (.postMessage js/self data)
+           (case type
+             "master-changed"
+             (p/do!
+              (log/debug "master-client change detected. Re-registering..." nil)
+              (<register)
+              (<re-requests-in-flight-on-slave! client-channel))
+             "slave-registered"
+             (<slave-registered-handler service-name slave-client-id event* *register-finish-promise?)
+
+             "slave-register"
+             (log/debug :ignored-event event*)
+
+             (log/error :unknown-event event*))))))
+    (->
+     (p/do!
+      (<register)
+      (p/resolve! status-ready-promise))
+     (p/catch (fn [e]
+                (log/error :on-become-slave e)
+                (p/rejected e))))))
+
+(defn- <on-become-master
+  [master-client-id service-name common-channel target on-become-master-handler status-ready-deferred-p]
+  (log/debug :become-master master-client-id :service service-name)
+  (listen-common-channel
+   common-channel
+   (fn [event]
+     (let [{:keys [slave-client-id type]} (bean/->clj (.-data event))]
+       (when (= type "slave-register")
+         (let [client-channel (js/BroadcastChannel. (get-broadcast-channel-name slave-client-id service-name))]
+           (swap! *master-slave-channels conj client-channel)
+           (do-not-wait
+            (js/navigator.locks.request slave-client-id #js {:mode "exclusive"}
+                                        (fn [_]
+                                          (log/debug :slave-has-gone slave-client-id)
+                                          (.close client-channel))))
+           (listen-client-channel client-channel (create-on-request-handler client-channel target))
+           (.postMessage common-channel (bean/->js {:type "slave-registered"
+                                                    :slave-client-id slave-client-id
+                                                    :master-client-id master-client-id
+                                                    :serviceName service-name})))))))
+  (.postMessage common-channel #js {:type "master-changed"
+                                    :master-client-id master-client-id
+                                    :serviceName service-name})
+  (p/do!
+   (on-become-master-handler service-name)
+   (<re-requests-in-flight-on-master! target)
+   (p/resolve! status-ready-deferred-p)))
+
+(defn <create-service
+  "broadcast-data-types - For data matching these types,
+                          forward the data broadcast from the master client directly to the UI thread."
+  [service-name target on-become-master-handler broadcast-data-types]
+  (clear-old-service!)
+  (p/let [broadcast-data-types (set broadcast-data-types)
+          status {:ready (p/deferred)}
+          common-channel (ensure-common-channel service-name)
+          client-id (<ensure-client-id)
+          <check-master-slave-fn!
+          (fn []
+            (<check-master-or-slave-client!
+             service-name
+             #(<on-become-master
+               client-id service-name common-channel target
+               on-become-master-handler (:ready status))
+             #(<on-become-slave
+               client-id service-name common-channel broadcast-data-types (:ready status))))]
+    (<check-master-slave-fn!)
+
+    (add-watch *master-re-check-trigger :check-master
+               (fn [_ _ _ new-value]
+                 (when (= new-value :re-check)
+                   (p/do!
+                    (p/delay 100)      ; why need delay here?
+                    (<check-master-slave-fn!)))))
+
+    {:proxy (js/Proxy. target
+                       #js {:get (fn [target method]
+                                   (assert (identical? "remoteInvoke" method) method)
+                                   (fn [args]
+                                     (cond
+                                       @*master-client?
+                                       (<apply-target-f! target method args)
+
+                                       :else
+                                       (let [request-id (next-request-id)
+                                             client-channel (ensure-client-channel client-id service-name)]
+                                         (p/create
+                                          (fn [resolve-fn reject-fn]
+                                            (vswap! *requests-in-flight assoc request-id {:method method
+                                                                                          :args args
+                                                                                          :resolve-fn resolve-fn
+                                                                                          :reject-fn reject-fn})
+                                            (.postMessage client-channel (bean/->js
+                                                                          {:id request-id
+                                                                           :type "request"
+                                                                           :method method
+                                                                           :args args}))))))))})
+     :status status}))
+
+(defn broadcast-to-clients!
+  [type' data]
+  (let [transit-payload (ldb/write-transit-str [type' data])]
+    (when (exists? js/self) (.postMessage js/self transit-payload))
+    (when-let [common-channel @*common-channel]
+      (let [str-type' (common-util/keyword->string type')]
+        (.postMessage common-channel #js {:type str-type'
+                                          :data transit-payload})))))

+ 0 - 20
src/main/frontend/worker/state.cljs

@@ -1,13 +1,8 @@
 (ns frontend.worker.state
 (ns frontend.worker.state
   "State hub for worker"
   "State hub for worker"
   (:require [logseq.common.config :as common-config]
   (:require [logseq.common.config :as common-config]
-            [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.util :as common-util]))
             [logseq.common.util :as common-util]))
 
 
-(defkeywords
-  :undo/repo->page-block-uuid->undo-ops {:doc "{repo {<page-block-uuid> [op1 op2 ...]}}"}
-  :undo/repo->page-block-uuid->redo-ops {:doc "{repo {<page-block-uuid> [op1 op2 ...]}}"})
-
 (defonce *main-thread (atom nil))
 (defonce *main-thread (atom nil))
 
 
 (defn- <invoke-main-thread*
 (defn- <invoke-main-thread*
@@ -43,14 +38,6 @@
 
 
                        :rtc/downloading-graph? false
                        :rtc/downloading-graph? false
 
 
-                       :undo/repo->page-block-uuid->undo-ops (atom {})
-                       :undo/repo->page-block-uuid->redo-ops (atom {})
-
-                       ;; new implementation
-                       :undo/repo->ops (atom {})
-                       :redo/repo->ops (atom {})
-
-
                        ;; thread atoms, these atoms' value are syncing from ui-thread
                        ;; thread atoms, these atoms' value are syncing from ui-thread
                        :thread-atom/online-event (atom nil)
                        :thread-atom/online-event (atom nil)
                        }))
                        }))
@@ -137,13 +124,6 @@
   [value]
   [value]
   (swap! *state assoc :rtc/downloading-graph? value))
   (swap! *state assoc :rtc/downloading-graph? value))
 
 
-(defn set-auth-tokens!
-  [id-token access-token refresh-token]
-  (swap! *state assoc
-         :auth/id-token id-token
-         :auth/access-token access-token
-         :auth/refresh-token refresh-token))
-
 (defn get-id-token
 (defn get-id-token
   []
   []
   (:auth/id-token @*state))
   (:auth/id-token @*state))

+ 5 - 5
src/rtc_e2e_test/client_steps.cljs

@@ -35,12 +35,12 @@
   client2: start rtc, wait page1, remote->client2"
   client2: start rtc, wait page1, remote->client2"
   {:client1
   {:client1
    (m/sp
    (m/sp
-     (let [r (m/? (rtc-core/new-task--rtc-start const/downloaded-test-repo const/test-token))]
+     (let [r (m/? (rtc-core/new-task--rtc-start false))]
        (is (nil? r))
        (is (nil? r))
        (m/? (helper/new-task--wait-all-client-ops-sent))))
        (m/? (helper/new-task--wait-all-client-ops-sent))))
    :client2
    :client2
    (m/sp
    (m/sp
-     (let [r (m/? (rtc-core/new-task--rtc-start const/downloaded-test-repo const/test-token))]
+     (let [r (m/? (rtc-core/new-task--rtc-start false))]
        (is (nil? r)))
        (is (nil? r)))
      (m/?
      (m/?
       (c.m/backoff
       (c.m/backoff
@@ -162,7 +162,7 @@
        (m/? (helper/new-task--client1-sync-barrier-2->1 "move-blocks-concurrently-signal"))
        (m/? (helper/new-task--client1-sync-barrier-2->1 "move-blocks-concurrently-signal"))
        (m/? helper/new-task--stop-rtc)
        (m/? helper/new-task--stop-rtc)
        (helper/transact! conn tx-data2)
        (helper/transact! conn tx-data2)
-       (is (nil? (m/? (rtc-core/new-task--rtc-start const/downloaded-test-repo const/test-token))))
+       (is (nil? (m/? (rtc-core/new-task--rtc-start false))))
        (m/? (helper/new-task--wait-all-client-ops-sent))
        (m/? (helper/new-task--wait-all-client-ops-sent))
        (m/? (helper/new-task--client1-sync-barrier-2->1 "step5"))
        (m/? (helper/new-task--client1-sync-barrier-2->1 "step5"))
        (let [message (m/? (helper/new-task--wait-message-from-other-client
        (let [message (m/? (helper/new-task--wait-message-from-other-client
@@ -189,7 +189,7 @@
        (m/? (helper/new-task--client2-sync-barrier-2->1 "move-blocks-concurrently-signal"))
        (m/? (helper/new-task--client2-sync-barrier-2->1 "move-blocks-concurrently-signal"))
        (m/? helper/new-task--stop-rtc)
        (m/? helper/new-task--stop-rtc)
        (helper/transact! conn (const/tx-data-map :move-blocks-concurrently-client2))
        (helper/transact! conn (const/tx-data-map :move-blocks-concurrently-client2))
-       (is (nil? (m/? (rtc-core/new-task--rtc-start const/downloaded-test-repo const/test-token))))
+       (is (nil? (m/? (rtc-core/new-task--rtc-start false))))
        (m/? (helper/new-task--wait-all-client-ops-sent))
        (m/? (helper/new-task--wait-all-client-ops-sent))
        (m/? (helper/new-task--client2-sync-barrier-2->1 "step5"))
        (m/? (helper/new-task--client2-sync-barrier-2->1 "step5"))
        (m/? (helper/new-task--send-message-to-other-client
        (m/? (helper/new-task--send-message-to-other-client
@@ -222,7 +222,7 @@ client2:
        (m/? (helper/new-task--client1-sync-barrier-1->2 "step6"))
        (m/? (helper/new-task--client1-sync-barrier-1->2 "step6"))
        (m/? helper/new-task--stop-rtc)
        (m/? helper/new-task--stop-rtc)
        (helper/transact! conn tx-data2)
        (helper/transact! conn tx-data2)
-       (let [r (m/? (rtc-core/new-task--rtc-start const/downloaded-test-repo const/test-token))]
+       (let [r (m/? (rtc-core/new-task--rtc-start false))]
          (is (nil? r))
          (is (nil? r))
          (m/? (helper/new-task--wait-all-client-ops-sent)))))
          (m/? (helper/new-task--wait-all-client-ops-sent)))))
    :client2
    :client2

+ 16 - 12
src/test/frontend/worker/undo_redo_test.cljs → src/test/frontend/undo_redo_test.cljs

@@ -1,4 +1,4 @@
-(ns frontend.worker.undo-redo-test
+(ns frontend.undo-redo-test
   (:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
   (:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.db :as db]
             [frontend.db :as db]
@@ -6,20 +6,24 @@
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.test.fixtures :as fixtures]
             [frontend.test.fixtures :as fixtures]
             [frontend.test.helper :as test-helper]
             [frontend.test.helper :as test-helper]
-            [frontend.worker.db-listener :as worker-db-listener]
-            [frontend.worker.undo-redo :as undo-redo]))
+            [frontend.undo-redo :as undo-redo]
+            [frontend.worker.db-listener :as worker-db-listener]))
 
 
 ;; TODO: random property ops test
 ;; TODO: random property ops test
 
 
 (def test-db test-helper/test-db)
 (def test-db test-helper/test-db)
 
 
+(defmethod worker-db-listener/listen-db-changes :gen-undo-ops
+  [_ {:keys [repo]} tx-report]
+  (undo-redo/gen-undo-ops! repo
+                           (assoc-in tx-report [:tx-meta :client-id] (:client-id @state/state))))
+
 (defn listen-db-fixture
 (defn listen-db-fixture
   [f]
   [f]
   (let [test-db-conn (db/get-db test-db false)]
   (let [test-db-conn (db/get-db test-db false)]
     (assert (some? test-db-conn))
     (assert (some? test-db-conn))
     (worker-db-listener/listen-db-changes! test-db test-db-conn
     (worker-db-listener/listen-db-changes! test-db test-db-conn
                                            {:handler-keys [:gen-undo-ops]})
                                            {:handler-keys [:gen-undo-ops]})
-
     (f)
     (f)
     (d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
     (d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
 
 
@@ -36,18 +40,18 @@
   listen-db-fixture)
   listen-db-fixture)
 
 
 (defn- undo-all!
 (defn- undo-all!
-  [conn]
+  []
   (loop [i 0]
   (loop [i 0]
-    (let [r (undo-redo/undo test-db conn)]
-      (if (not= :frontend.worker.undo-redo/empty-undo-stack r)
+    (let [r (undo-redo/undo test-db)]
+      (if (not= :frontend.undo-redo/empty-undo-stack r)
         (recur (inc i))
         (recur (inc i))
         (prn :undo-count i)))))
         (prn :undo-count i)))))
 
 
 (defn- redo-all!
 (defn- redo-all!
-  [conn]
+  []
   (loop [i 0]
   (loop [i 0]
-    (let [r (undo-redo/redo test-db conn)]
-      (if (not= :frontend.worker.undo-redo/empty-redo-stack r)
+    (let [r (undo-redo/redo test-db)]
+      (if (not= :frontend.undo-redo/empty-redo-stack r)
         (recur (inc i))
         (recur (inc i))
         (prn :redo-count i)))))
         (prn :redo-count i)))))
 
 
@@ -64,10 +68,10 @@
             _ (outliner-test/run-random-mixed-ops! *random-blocks)
             _ (outliner-test/run-random-mixed-ops! *random-blocks)
             db-after @conn]
             db-after @conn]
 
 
-        (undo-all! conn)
+        (undo-all!)
 
 
         (is (= (get-datoms @conn) #{}))
         (is (= (get-datoms @conn) #{}))
 
 
-        (redo-all! conn)
+        (redo-all!)
 
 
         (is (= (get-datoms @conn) (get-datoms db-after)))))))
         (is (= (get-datoms @conn) (get-datoms db-after)))))))