1
0
Эх сурвалжийг харах

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

feat: multiple tabs/windows support
Tienson Qin 6 сар өмнө
parent
commit
d42621864a
34 өөрчлөгдсөн 801 нэмэгдсэн , 390 устгасан
  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.thingatpt thingatpt
              frontend.util.url url-util
+             frontend.worker.shared-service shared-service
              frontend.worker.handler.page worker-page
              frontend.worker.pipeline worker-pipeline
              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)))
         nil)
       (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
                        {:role "fileMenu"
                         :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?
                                                   "CommandOrControl+N"
                                                   ;; 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
                  ;; No db cache persisting ensured. Should be handled by the caller
                  (fn [repo]
-                   (ui-handler/open-new-window-or-tab! nil repo)))
+                   (ui-handler/open-new-window-or-tab! repo)))
 
   (safe-api-call "invokeLogseqAPI"
                  (fn [^js data]

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

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

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

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

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

@@ -111,9 +111,7 @@
        (shui/button
         {:variant :outline
          :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")
 
        [:div.my-2.flex

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

@@ -2,8 +2,8 @@
   "Provides async transact for use with ldb/transact!"
   (:require [clojure.core.async :as async]
             [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 requests (async/chan 1000))
@@ -55,4 +55,4 @@
                         ;; not from remote(rtc)
                         :local-tx? true)]
     (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))]
     (p/do!
      (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
   []

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

@@ -3,6 +3,7 @@
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [goog.dom :as gdom]))
 
@@ -33,8 +34,7 @@
       (let [page-id (:block/uuid (:block/page (db/entity (:db/id (state/get-edit-block)))))
             repo (state/get-current-repo)]
         (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)
 

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

@@ -142,8 +142,7 @@
       (graph-switch-on-persisted graph opts))))
 
 (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]]
   (js/alert "Graph migrated."))

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

@@ -42,8 +42,7 @@
             [goog.dom :as gdom]
             [logseq.common.util :as common-util]
             [logseq.shui.ui :as shui]
-            [promesa.core :as p]
-            [rum.core :as rum]))
+            [promesa.core :as p]))
 
 (defmethod events/handle :class/configure [[_ page]]
   (shui/dialog-open!
@@ -303,25 +302,6 @@
 (defmethod events/handle :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 []
   (let [selection (state/get-selection-blocks)
         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.persist-db.browser :as db-browser]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
-            [frontend.util.page :as page-util]
             [goog.functions :refer [debounce]]
             [logseq.db :as ldb]
             [promesa.core :as p]))
@@ -48,19 +48,15 @@
       (p/do!
        @*last-request
        (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))
 
 (let [*last-request (atom nil)]
@@ -71,14 +67,10 @@
       (p/do!
        @*last-request
        (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))

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

@@ -22,6 +22,7 @@
             [frontend.persist-db :as persist-db]
             [frontend.search :as search]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [frontend.util.fs :as util-fs]
             [frontend.util.text :as text-util]
@@ -59,9 +60,10 @@
 (defn start-repo-db-if-not-exists!
   [repo & {:as opts}]
   (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!
   "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!
   "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!
   []

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

@@ -113,8 +113,7 @@
    (state/set-auth-access-token nil)
    (state/set-auth-refresh-token nil)
    (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?]
    (state/set-auth-id-token nil)
    (state/set-auth-access-token nil)
@@ -122,21 +121,18 @@
      (state/set-auth-refresh-token nil))
    (if except-refresh-token?
      (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!
   ([id-token access-token]
    (state/set-auth-id-token id-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]
    (state/set-auth-id-token id-token)
    (state/set-auth-access-token access-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
   "return refreshed id-token, access-token"

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

@@ -1,13 +1,25 @@
 (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]
+            [frontend.config :as config]
+            [frontend.db :as db]
+            [frontend.db.react :as react]
+            [frontend.fs :as fs]
             [frontend.handler.ui :as ui-handler]
+            [frontend.state :as state]
             [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
   [{:keys [_request-id repo tx-meta tx-data deleted-block-uuids deleted-assets affected-keys blocks]}]
@@ -55,6 +67,8 @@
                               tx-data))]
               (d/transact! conn tx-data' tx-meta))
 
+            (update-editing-block-title-if-changed! tx-data)
+
             (when (seq deleted-assets)
               (doseq [asset deleted-assets]
                 (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!
   [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!")
             ;;  (prn :ops r#)
             ;;  (js/console.trace)
             ;;  (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."
   (:require ["comlink" :as Comlink]
             [electron.ipc :as ipc]
+            [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :as thread-api]
             [frontend.config :as config]
             [frontend.date :as date]
@@ -12,8 +13,10 @@
             [frontend.handler.worker :as worker-handler]
             [frontend.persist-db.protocol :as protocol]
             [frontend.state :as state]
+            [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
             [logseq.db :as ldb]
+            [missionary.core :as m]
             [promesa.core :as p]))
 
 (defn- ask-persist-permission!
@@ -25,17 +28,21 @@
 
 (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
   [route-match]
@@ -56,9 +63,7 @@
                        old-state (f prev)
                        new-state (f current)]
                    (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!
   [repo tx-data tx-meta]
@@ -97,12 +102,9 @@
       (Comlink/expose #js{"remoteInvoke" thread-api/remote-function} worker)
       (worker-handler/handle-message! 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"))
-                  _ (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!)
                   _ (ask-persist-permission!)
                   _ (state/pub-event! [:graph/sync-context])]
@@ -112,12 +114,11 @@
                (db-transact/transact transact!
                                      (if (string? repo) repo (state/get-current-repo))
                                      tx-data
-                                     tx-meta)))
+                                     (assoc tx-meta :client-id (:client-id @state/state)))))
             (db-transact/listen-for-requests))
           (p/catch (fn [error]
                      (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!
   [repo data]
@@ -133,11 +134,7 @@
 
 (defn- sqlite-error-handler
   [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 []
   protocol/PersistentDB

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

@@ -33,6 +33,7 @@
 (defonce *profile-state (volatile! {}))
 
 (defonce *db-worker (atom nil))
+(defonce *editor-info (atom nil))
 
 (defn- <invoke-db-worker*
   [qkw direct-pass-args? args-list]
@@ -61,7 +62,8 @@
                          (when graph (ipc/ipc "setCurrentGraph" graph))
                          graph)]
     (atom
-     {:route-match                           nil
+     {:client-id                             (str (random-uuid))
+      :route-match                           nil
       :today                                 nil
       :system/events                         (async/chan 1000)
       :file/unlinked-dirs                    #{}
@@ -1037,16 +1039,6 @@ Similar to re-frame subscriptions"
   []
   @(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?
   []
   (seq @(:editor/editing? @state)))
@@ -1065,6 +1057,17 @@ Similar to re-frame subscriptions"
                 id))))
         (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
   []
   (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"
   (:require [clojure.set :as set]
             [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.db :as ldb]
             [malli.core :as m]
-            [malli.util :as mu]))
+            [malli.util :as mu]
+            [promesa.core :as p]))
 
 (defkeywords
   ::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]))
 
 (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
   [col op]
@@ -250,51 +252,60 @@
         (throw e)))))
 
 (defn- undo-redo-aux
-  [repo conn undo?]
+  [repo undo?]
   (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)
       (prn (str "No further " (if undo? "undo" "redo") " information"))
       (if undo? ::empty-undo-stack ::empty-redo-stack))))
 
 (defn undo
-  [repo conn]
-  (undo-redo-aux repo conn true))
+  [repo]
+  (undo-redo-aux repo true))
 
 (defn redo
-  [repo conn]
-  (undo-redo-aux repo conn false))
+  [repo]
+  (undo-redo-aux repo false))
 
 (defn record-editor-info!
   [repo editor-info]
@@ -312,13 +323,15 @@
   (when 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]
-    (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
                            (filter
                             (fn [id] (and (nil? (d/entity db-after id)) (d/entity db-before id)))
@@ -329,6 +342,8 @@
                         all-ids))
             tx-data' (->> (remove (fn [d] (contains? #{:block/path-refs} (:a d))) tx-data)
                           vec)
+            editor-info @state/*editor-info
+            _ (reset! state/*editor-info nil)
             op (->> [(when editor-info [::record-editor-info editor-info])
                      [::db-transact
                       {:tx-data tx-data'
@@ -338,3 +353,8 @@
                     (remove nil?)
                     vec)]
         (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
   "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
   "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)]
     (: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
   "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"

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

@@ -1,6 +1,6 @@
 (ns frontend.worker.db.validate
   "Validate db"
-  (:require [frontend.worker.util :as worker-util]
+  (:require [frontend.worker.shared-service :as shared-service]
             [logseq.db.frontend.validate :as db-validate]))
 
 (defn validate-db
@@ -8,16 +8,16 @@
   (let [{:keys [errors datom-count entities]} (db-validate/validate-db! db)]
     (if errors
       (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
      :datom-count datom-count
      :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.worker.pipeline :as worker-pipeline]
             [frontend.worker.search :as search]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [logseq.common.util :as common-util]
             [logseq.outliner.batch-tx :as batch-tx]
             [promesa.core :as p]))
@@ -26,7 +26,7 @@
                    :tx-data (:tx-data tx-report')
                    :tx-meta tx-meta}
                   (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?
         (p/do!

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

@@ -11,6 +11,7 @@
             [datascript.storage :refer [IStorage] :as storage]
             [frontend.common.cache :as common.cache]
             [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.worker.db-listener :as db-listener]
             [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.rtc.asset-db-listener]
             [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.search :as search]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.thread-atom]
-            [frontend.worker.undo-redo :as undo-redo]
             [frontend.worker.util :as worker-util]
             [goog.object :as gobj]
             [lambdaisland.glogi.console :as glogi-console]
@@ -44,6 +44,7 @@
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.op :as outliner-op]
             [me.tonsky.persistent-sorted-set :as set :refer [BTSet]]
+            [missionary.core :as m]
             [promesa.core :as p]))
 
 (defonce *sqlite worker-state/*sqlite)
@@ -449,24 +450,36 @@
   [repo]
   (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
   [rtc-ws-url]
   (reset! worker-state/*rtc-ws-url rtc-ws-url)
   (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
   [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
   [repo inputs]
@@ -595,16 +608,6 @@
   (when-let [conn (worker-state/get-datascript-conn repo)]
     (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
   [repo db-transit]
   (reset-db! repo db-transit)
@@ -685,7 +688,7 @@
               {:keys [type payload]} (when (map? data) data)]
           (case type
             :notification
-            (worker-util/post-message type [(:message payload) (:type payload)])
+            (shared-service/broadcast-to-clients! :notification [(:message payload) (:type payload)])
             (throw e)))))))
 
 (def-thread-api :thread-api/file-writes-finished?
@@ -714,11 +717,6 @@
   (worker-state/set-new-state! new-state)
   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
   [repo]
   (when-let [db (worker-state/get-sqlite-conn repo)]
@@ -735,21 +733,6 @@
   (when-let [conn (worker-state/get-datascript-conn repo)]
     (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
   [repo]
   (when-let [conn (worker-state/get-datascript-conn repo)]
@@ -804,11 +787,6 @@
   [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
   (def-thread-api :general/dangerousRemoveAllDbs
     []
@@ -858,25 +836,88 @@
              (file/write-files! conn col (worker-state/get-context)))
            (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
   "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
   (defn <remove-all-files!

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

@@ -5,6 +5,7 @@
             [frontend.worker.commands :as commands]
             [frontend.worker.file :as file]
             [frontend.worker.react :as worker-react]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [logseq.common.defkeywords :refer [defkeywords]]
@@ -106,8 +107,8 @@
                    true
                    (db-validate/validate-tx-report! tx-report (:validate-db-options context)))]
       (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
   (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.ws :as ws]
             [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.util :as worker-util]
             [lambdaisland.glogi :as log]
@@ -337,7 +338,7 @@
                                   :repo repo})))
 
 ;;; ================ API ================
-(defn new-task--rtc-start
+(defn- new-task--rtc-start*
   [repo token]
   (m/sp
     ;; 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}
           (validate-rtc-start-conditions repo token)]
       (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]}
               (create-rtc-loop graph-uuid schema-version repo conn date-formatter token)
               *last-stop-exception (atom nil)
@@ -355,8 +356,8 @@
                                  (reset! *last-stop-exception e)
                                  (log/info :rtc-loop-task e)))
               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
                                             :graph-uuid graph-uuid
                                             :local-graph-schema-version schema-version
@@ -371,6 +372,29 @@
                                             :*last-stop-exception *last-stop-exception})
                 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
   []
   (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))]
     (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
   [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-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
   []
@@ -595,9 +620,10 @@
   [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
   [repo server-schema-version]
@@ -611,7 +637,7 @@
   (c.m/run-background-task
    ::subscribe-state
    (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)))
 
 (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.log-and-state :as rtc-log-and-state]
             [frontend.worker.rtc.ws-util :as ws-util]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [logseq.db :as ldb]
@@ -395,7 +396,7 @@
                          :persist-op? false} (worker-state/get-context))
           (transact-remote-schema-version! 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 ;;
@@ -412,12 +413,13 @@
                                                  :graph-uuid graph-uuid
                                                  :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
   [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
   "Fns to generate rtc related logs"
   (:require [frontend.common.missionary :as c.m]
-            [frontend.worker.util :as worker-util]
+            [frontend.worker.shared-service :as shared-service]
             [lambdaisland.glogi :as log]
             [logseq.common.defkeywords :refer [defkeywords]]
             [malli.core :as ma]
@@ -86,9 +86,10 @@
   (swap! *graph-uuid->remote-t assoc (ensure-uuid graph-uuid) remote-t))
 
 ;;; subscribe-logs, push to frontend
+;;; TODO: refactor by using c.m/run-background-task
 (defn- subscribe-logs
   []
   (remove-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)

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

@@ -3,7 +3,7 @@
   (:require [clojure.data :as data]
             [datascript.core :as d]
             [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]
             [logseq.db :as ldb]
             [logseq.db.frontend.schema :as db-schema]
@@ -37,19 +37,19 @@
               client-builtin-db-idents (set (get-builtin-db-idents db))
               client-schema-version (ldb/get-graph-schema-version db)]
           (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 _]
                 (data/diff client-builtin-db-idents server-builtin-db-idents)]
             (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)))))

+ 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
   "State hub for worker"
   (:require [logseq.common.config :as common-config]
-            [logseq.common.defkeywords :refer [defkeywords]]
             [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))
 
 (defn- <invoke-main-thread*
@@ -43,14 +38,6 @@
 
                        :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-atom/online-event (atom nil)
                        }))
@@ -137,13 +124,6 @@
   [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
   []
   (:auth/id-token @*state))

+ 5 - 5
src/rtc_e2e_test/client_steps.cljs

@@ -35,12 +35,12 @@
   client2: start rtc, wait page1, remote->client2"
   {:client1
    (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))
        (m/? (helper/new-task--wait-all-client-ops-sent))))
    :client2
    (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)))
      (m/?
       (c.m/backoff
@@ -162,7 +162,7 @@
        (m/? (helper/new-task--client1-sync-barrier-2->1 "move-blocks-concurrently-signal"))
        (m/? helper/new-task--stop-rtc)
        (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--client1-sync-barrier-2->1 "step5"))
        (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--stop-rtc)
        (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--client2-sync-barrier-2->1 "step5"))
        (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--stop-rtc)
        (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))
          (m/? (helper/new-task--wait-all-client-ops-sent)))))
    :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]]
             [datascript.core :as d]
             [frontend.db :as db]
@@ -6,20 +6,24 @@
             [frontend.state :as state]
             [frontend.test.fixtures :as fixtures]
             [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
 
 (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
   [f]
   (let [test-db-conn (db/get-db test-db false)]
     (assert (some? test-db-conn))
     (worker-db-listener/listen-db-changes! test-db test-db-conn
                                            {:handler-keys [:gen-undo-ops]})
-
     (f)
     (d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
 
@@ -36,18 +40,18 @@
   listen-db-fixture)
 
 (defn- undo-all!
-  [conn]
+  []
   (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))
         (prn :undo-count i)))))
 
 (defn- redo-all!
-  [conn]
+  []
   (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))
         (prn :redo-count i)))))
 
@@ -64,10 +68,10 @@
             _ (outliner-test/run-random-mixed-ops! *random-blocks)
             db-after @conn]
 
-        (undo-all! conn)
+        (undo-all!)
 
         (is (= (get-datoms @conn) #{}))
 
-        (redo-all! conn)
+        (redo-all!)
 
         (is (= (get-datoms @conn) (get-datoms db-after)))))))