浏览代码

Merge branch 'feat/db' into feat/capacitor-new

charlie 6 月之前
父节点
当前提交
e024857d1e
共有 62 个文件被更改,包括 1515 次插入564 次删除
  1. 1 0
      .clj-kondo/config.edn
  2. 88 0
      .github/workflows/clj-e2e.yml
  3. 3 0
      .gitignore
  4. 1 0
      CODEBASE_OVERVIEW.md
  5. 31 0
      clj-e2e/.gitignore
  6. 2 0
      clj-e2e/.lsp/config.edn
  7. 15 0
      clj-e2e/README.md
  8. 21 0
      clj-e2e/bb.edn
  9. 18 0
      clj-e2e/build.clj
  10. 18 0
      clj-e2e/deps.edn
  11. 38 0
      clj-e2e/src/logseq/e2e/repl.clj
  12. 149 0
      clj-e2e/src/logseq/e2e/util.clj
  13. 42 0
      clj-e2e/test/logseq/e2e/editor_test.clj
  14. 17 0
      clj-e2e/test/logseq/e2e/fixtures.clj
  15. 76 0
      clj-e2e/test/logseq/e2e/outliner_test.clj
  16. 8 0
      deps/common/src/logseq/common/util.cljs
  17. 10 10
      deps/outliner/src/logseq/outliner/validate.cljs
  18. 7 7
      deps/shui/src/logseq/shui/table/core.cljc
  19. 1 6
      src/electron/electron/core.cljs
  20. 1 1
      src/main/electron/listener.cljs
  21. 6 0
      src/main/frontend/common/missionary.cljs
  22. 5 3
      src/main/frontend/components/block.cljs
  23. 28 21
      src/main/frontend/components/objects.cljs
  24. 42 43
      src/main/frontend/components/page.cljs
  25. 22 17
      src/main/frontend/components/property/value.cljs
  26. 0 51
      src/main/frontend/components/repo.cljs
  27. 1 1
      src/main/frontend/db/conn.cljs
  28. 2 0
      src/main/frontend/db/restore.cljs
  29. 1 3
      src/main/frontend/db/rtc/debug_ui.cljs
  30. 3 3
      src/main/frontend/db/transact.cljs
  31. 6 0
      src/main/frontend/flows.cljs
  32. 26 28
      src/main/frontend/handler/db_based/rtc.cljs
  33. 24 3
      src/main/frontend/handler/db_based/rtc_flows.cljs
  34. 2 2
      src/main/frontend/handler/editor/lifecycle.cljs
  35. 1 2
      src/main/frontend/handler/events.cljs
  36. 1 21
      src/main/frontend/handler/events/ui.cljs
  37. 16 24
      src/main/frontend/handler/history.cljs
  38. 5 3
      src/main/frontend/handler/repo.cljs
  39. 2 2
      src/main/frontend/handler/route.cljs
  40. 5 6
      src/main/frontend/handler/ui.cljs
  41. 4 8
      src/main/frontend/handler/user.cljs
  42. 22 6
      src/main/frontend/modules/outliner/pipeline.cljs
  43. 27 26
      src/main/frontend/modules/outliner/ui.cljc
  44. 24 27
      src/main/frontend/persist_db/browser.cljs
  45. 20 12
      src/main/frontend/state.cljs
  46. 68 48
      src/main/frontend/undo_redo.cljs
  47. 3 12
      src/main/frontend/util/page.cljs
  48. 10 0
      src/main/frontend/worker/db/migrate.cljs
  49. 10 10
      src/main/frontend/worker/db/validate.cljs
  50. 2 2
      src/main/frontend/worker/db_listener.cljs
  51. 106 65
      src/main/frontend/worker/db_worker.cljs
  52. 2 2
      src/main/frontend/worker/export.cljs
  53. 1 3
      src/main/frontend/worker/flows.cljs
  54. 24 12
      src/main/frontend/worker/pipeline.cljs
  55. 41 15
      src/main/frontend/worker/rtc/core.cljs
  56. 9 7
      src/main/frontend/worker/rtc/full_upload_download_graph.cljs
  57. 3 2
      src/main/frontend/worker/rtc/log_and_state.cljs
  58. 13 13
      src/main/frontend/worker/rtc/skeleton.cljs
  59. 360 0
      src/main/frontend/worker/shared_service.cljs
  60. 0 20
      src/main/frontend/worker/state.cljs
  61. 5 5
      src/rtc_e2e_test/client_steps.cljs
  62. 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

+ 88 - 0
.github/workflows/clj-e2e.yml

@@ -0,0 +1,88 @@
+name: Clojure E2E
+
+on:
+  push:
+    branches: ["feat/db"]
+    paths:
+      - 'clj-e2e/**'
+      - '.github/workflows/clj-e2e.yml'
+      - src/**
+      - deps/**
+      - packages/**
+  pull_request:
+    branches: ["feat/db"]
+    paths:
+      - 'clj-e2e/**'
+      - '.github/workflows/clj-e2e.yml'
+      - src/**
+      - deps/**
+      - packages/**
+
+env:
+  CLOJURE_VERSION: '1.11.1.1413'
+  # This is the latest node version we can run.
+  NODE_VERSION: '20'
+  BABASHKA_VERSION: '1.0.168'
+
+jobs:
+  e2e-test-build:
+    name: Test
+    runs-on: ubuntu-22.04
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          cache: 'yarn'
+          cache-dependency-path: |
+            yarn.lock
+
+      - name: Set up Clojure
+        uses: DeLaGuardo/[email protected]
+        with:
+          cli: ${{ env.CLOJURE_VERSION }}
+          bb: ${{ env.BABASHKA_VERSION }}
+
+      - name: Clojure cache
+        uses: actions/cache@v3
+        id: clojure-deps
+        with:
+          path: |
+            ~/.m2/repository
+            ~/.gitlibs
+          key: ${{ runner.os }}-clojure-deps-${{ hashFiles('deps.edn') }}
+          restore-keys: ${{ runner.os }}-clojure-deps-
+
+      - name: Fetch Clojure deps
+        if: steps.clojure-deps.outputs.cache-hit != 'true'
+        run: clojure -A:cljs -P
+
+      - name: Shadow-cljs cache
+        uses: actions/cache@v3
+        with:
+          path: .shadow-cljs
+          # ensure update cache every time
+          key: ${{ runner.os }}-shadow-cljs-${{ github.sha }}
+          # will match most recent upload
+          restore-keys: |
+            ${{ runner.os }}-shadow-cljs-
+
+      - name: Fetch yarn deps
+        run: yarn install
+        env:
+          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
+
+      # NOTE: require the app to be build with DEV-RELEASE flag
+      - name: Prepare E2E test build
+        run: |
+          yarn gulp:build && clojure -M:cljs release app --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug
+          rsync -avz --exclude node_modules --exclude android --exclude ios ./static/ ./public/static/
+          ls -lR ./public
+
+      - name: Run e2e tests
+        run: cd clj-e2e && bb dev
+        env:
+          DEBUG: "pw:api"

+ 3 - 0
.gitignore

@@ -68,3 +68,6 @@ deps/shui/.lsp
 deps/shui/.lsp-cache
 deps/shui/.lsp-cache
 deps/shui/.clj-kondo
 deps/shui/.clj-kondo
 tx-log*
 tx-log*
+clj-e2e/.wally
+clj-e2e/resources
+.dir-locals.el

+ 1 - 0
CODEBASE_OVERVIEW.md

@@ -62,6 +62,7 @@ This is overview of this repository's most important directories and files.
   - `packags/tldraw/` - Custom fork of tldraw which powers whiteboards
   - `packags/tldraw/` - Custom fork of tldraw which powers whiteboards
 - `scripts` - Dev scripts
 - `scripts` - Dev scripts
 - `e2e-tests/` - end to end frontend tests
 - `e2e-tests/` - end to end frontend tests
+- `clj-e2e/` - end to end clj frontend tests
 - `android/` -  Android app
 - `android/` -  Android app
 - `ios/` - iOS app
 - `ios/` - iOS app
 
 

+ 31 - 0
clj-e2e/.gitignore

@@ -0,0 +1,31 @@
+.calva/output-window/
+.calva/repl.calva-repl
+.classpath
+.clj-kondo/.cache
+.cpcache
+.DS_Store
+.eastwood
+.factorypath
+.hg/
+.hgignore
+.java-version
+.lein-*
+.lsp/.cache
+.lsp/sqlite.db
+.nrepl-history
+.nrepl-port
+.portal
+.project
+.rebel_readline_history
+.settings
+.socket-repl-port
+.sw*
+.vscode
+*.class
+*.jar
+*.swp
+*~
+/checkouts
+/classes
+/target
+.wally/

+ 2 - 0
clj-e2e/.lsp/config.edn

@@ -0,0 +1,2 @@
+{:source-aliases #{:test}
+ :clean {:ns-inner-blocks-indentation :same-line}}

+ 15 - 0
clj-e2e/README.md

@@ -0,0 +1,15 @@
+# e2e
+
+e2e tests for Logseq app.
+
+## Usage
+
+Before running tests, ensure the following:
+* The app's js and css assets are built and located at ../public/static/.
+* Those assets are served on http://localhost:3002/ via `bb serve`.
+
+Then, run the project's tests:
+
+    $ clojure -T:build test
+
+If you would like to run individual tests, pass options to the test runner through `clojure -M:test`. For example, add a `^:focus` on a test and then run `clojure -M:test -i focus`.

+ 21 - 0
clj-e2e/bb.edn

@@ -0,0 +1,21 @@
+{:deps {org.babashka/http-server {:mvn/version "0.1.13"}
+        org.babashka/cli {:mvn/version "0.2.23"}}
+ :tasks
+ {:requires ([babashka.cli :as cli])
+  :init (def cli-opts (cli/parse-opts *command-line-args* {:coerce {:port :int :headers :edn}}))
+
+  serve {:doc "Serve static assets"
+         :requires ([babashka.http-server :as server])
+         :task (server/exec (merge {:port 3002
+                                    :dir "../public"}
+                                   cli-opts))}
+
+  prn {:task (clojure "-X clojure.core/prn" cli-opts)}
+
+  test {:task (do
+                (clojure "-T:build test")
+                (System/exit 0))}
+
+  -dev {:depends [serve prn test]}
+
+  dev {:task (run '-dev {:parallel true})}}}

+ 18 - 0
clj-e2e/build.clj

@@ -0,0 +1,18 @@
+(ns build
+  (:refer-clojure :exclude [test])
+  (:require [clojure.tools.build.api :as b]
+            [clojure.tools.deps :as t]))
+
+(defn test "Run all the tests."
+  [_opts]
+  (println "\nRunning tests...")
+  (let [basis    (b/create-basis {:aliases [:test]})
+        combined (t/combine-aliases basis [:test])
+        cmds     (b/java-command
+                  {:basis basis
+                   :java-opts (:jvm-opts combined)
+                   :main      'clojure.main
+                   :main-args ["-m" "cognitect.test-runner"]})
+        {:keys [exit]} (b/process cmds)]
+    (when-not (zero? exit)
+      (throw (ex-info "Tests failed" {})))))

+ 18 - 0
clj-e2e/deps.edn

@@ -0,0 +1,18 @@
+{:paths ["src" "resources"]
+ :deps {org.clojure/clojure {:mvn/version "1.12.0"}
+        ;; io.github.pfeodrippe/wally {:local/root "../../../wally"}
+        io.github.pfeodrippe/wally {:git/url "https://github.com/logseq/wally"
+                                    :sha "6b0583701fc64ec5177eec6577e33bb8d9115d61"}
+        ;; io.github.zmedelis/bosquet {:mvn/version "2025.03.28"}
+        }
+ :aliases
+ {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}}
+          :ns-default build}
+  :serve {:deps {org.babashka/http-server {:mvn/version "0.1.13"}}
+          :main-opts ["-m" "babashka.http-server"]
+          :exec-fn babashka.http-server/exec}
+  :test {:extra-paths ["test"]
+         :main-opts ["-m" "cognitect.test-runner"]
+         :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
+                      io.github.cognitect-labs/test-runner
+                      {:git/tag "v0.5.1" :git/sha "dfb30dd"}}}}}

+ 38 - 0
clj-e2e/src/logseq/e2e/repl.clj

@@ -0,0 +1,38 @@
+(ns logseq.e2e.repl
+  "fns used on repl"
+  (:require [clojure.test :refer [run-tests run-test]]
+            [logseq.e2e.util :as util]
+            [wally.main :as w]
+            [wally.repl :as repl]))
+
+(comment
+
+  (future
+    (w/with-page-open
+      (w/make-page {:headless false
+                    :persistent false
+                    :slow-mo 20})
+      (w/navigate "http://localhost:3001")
+      (repl/pause)))
+
+  ;; You can put `(repl/pause)` in any test to pause the tests,
+  ;; this allows us to continue experimenting with the current page.
+  (repl/pause)
+
+  ;; To resume the tests, close the page/context/browser
+  (repl/resume)
+
+  ;; Run all the tests in specific ns with `future` to not block repl
+  (future (run-tests 'logseq.e2e.editor-test))
+  (future (run-tests 'logseq.e2e.outliner-test))
+
+  ;; Run specific test
+  (future (run-test logseq.e2e.editor-test/commands-test))
+
+  ;; after the test has been paused, you can do anything with the current page like this
+  (repl/with-page
+    (w/wait-for (first (util/get-edit-block-container))
+                {:state :detached}))
+
+  ;;
+  )

+ 149 - 0
clj-e2e/src/logseq/e2e/util.clj

@@ -0,0 +1,149 @@
+(ns logseq.e2e.util
+  (:refer-clojure :exclude [type])
+  (:require [clojure.string :as string]
+            [clojure.test :refer [is]]
+            [wally.main :as w]
+            [wally.selectors :as ws])
+  (:import (com.microsoft.playwright.assertions PlaywrightAssertions)))
+
+(def assert-that PlaywrightAssertions/assertThat)
+
+(defn wait-timeout
+  [ms]
+  (.waitForTimeout (w/get-page) ms))
+
+(defn get-active-element
+  []
+  (w/-query "*:focus"))
+
+(defn get-editor
+  []
+  (let [klass ".editor-wrapper textarea"
+        editor (w/-query klass)]
+    (when (w/visible? klass)
+      editor)))
+
+(defn get-edit-block-container
+  []
+  (first (w/query ".ls-block" {:has (w/-query ".editor-wrapper textarea")})))
+
+(defn input
+  "Notice this will replace the existing input value with `text`"
+  [text]
+  (w/fill "*:focus" text))
+
+(defn type
+  [text]
+  (let [input-node (w/-query "*:focus")]
+    (.type input-node text)))
+
+(def press w/keyboard-press)
+
+(defn search
+  [text]
+  (w/click :#search-button)
+  (w/fill ".cp__cmdk-search-input" text))
+
+(defn new-page
+  [title]
+  ;; Question: what's the best way to close all the popups?
+  ;; close popup, exit editing
+  (search title)
+  (w/click [(ws/text "Create page") (ws/nth= "0")])
+  (w/wait-for ".editor-wrapper textarea"))
+
+(defn count-elements
+  [q]
+  (w/count* (w/-query q)))
+
+(defn blocks-count
+  "Blocks count including page title"
+  []
+  (count-elements ".ls-block"))
+
+(defn page-blocks-count
+  []
+  (count-elements ".ls-page-blocks .ls-block"))
+
+(defn new-block
+  [title]
+  (press "Enter")
+  (input title))
+
+(defn save-block
+  [text]
+  (input text))
+
+(defn exit-edit
+  []
+  (press "Escape"))
+
+(defn delete-blocks
+  "Delete the current block if in editing mode, otherwise, delete all the selected blocks."
+  []
+  (let [editor (get-editor)]
+    (when editor (exit-edit))
+    (press "Backspace")))
+
+(defn get-text
+  [locator]
+  (if (string? locator)
+    (.textContent (w/-query locator))
+    (.textContent locator)))
+
+(defn get-edit-content
+  []
+  (when-let [editor (get-editor)]
+    (get-text editor)))
+
+;; TODO: support tree
+(defn new-blocks
+  [titles]
+  (let [value (get-edit-content)]
+    (if (string/blank? value)           ; empty block
+      (do
+        (save-block (first titles))
+        (doseq [title (rest titles)]
+          (new-block title)))
+      (doseq [title titles]
+        (new-block title)))))
+
+(defn bounding-xy
+  [locator]
+  (let [box (.boundingBox locator)]
+    [(.-x box) (.-y box)]))
+
+(defn indent-outdent
+  [indent?]
+  (let [editor (get-editor)
+        [x1 _] (bounding-xy editor)
+        _ (press (if indent? "Tab" "Shift+Tab"))
+        [x2 _] (bounding-xy editor)]
+    (if indent?
+      (is (< x1 x2))
+      (is (> x1 x2)))))
+
+(defn indent
+  []
+  (indent-outdent true))
+
+(defn outdent
+  []
+  (indent-outdent false))
+
+(defn open-last-block
+  []
+  (w/click (last (w/query ".ls-page-blocks .ls-block .block-content"))))
+
+(defn repeat-keyboard
+  [n shortcut]
+  (dotimes [_i n]
+    (press shortcut)))
+
+(defn get-page-blocks-contents
+  []
+  (w/all-text-contents ".ls-page-blocks .ls-block .block-title-wrap"))
+
+(def mac? (= "Mac OS X" (System/getProperty "os.name")))
+
+(def mod-key (if mac? "Meta" "Control"))

+ 42 - 0
clj-e2e/test/logseq/e2e/editor_test.clj

@@ -0,0 +1,42 @@
+(ns logseq.e2e.editor-test
+  (:require
+   [clojure.string :as string]
+   [clojure.test :refer [deftest testing is use-fixtures]]
+   [logseq.e2e.fixtures :as fixtures]
+   [logseq.e2e.util :as util :refer [press]]
+   [wally.main :as w]))
+
+(use-fixtures :once fixtures/open-page)
+
+(deftest commands-test
+  (testing "/command trigger popup"
+    (util/new-page "Test")
+    (util/save-block "b1")
+    (util/type " /")
+    (w/wait-for ".ui__popover-content")
+    (is (some? (w/find-one-by-text "span" "Node reference")))
+    (press "Backspace")
+    (w/wait-for-not-visible ".ui__popover-content"))
+
+  (testing "Node reference"
+    (testing "Page reference"
+      (util/new-block "/")
+      (util/type "Node eferen")
+      (w/wait-for ".ui__popover-content")
+      (press "Enter")
+      (util/type "Another page")
+      (press "Enter")
+      (is (= "[[Another page]]" (util/get-edit-content)))
+      (util/exit-edit)
+      (is (= "Another page" (util/get-text "a.page-ref"))))
+    (testing "Block reference"
+      (util/new-block "/")
+      (util/type "Node eferen")
+      (w/wait-for ".ui__popover-content")
+      (press "Enter")
+      (util/type "b1")
+      (util/wait-timeout 300)
+      (press "Enter")
+      (is (string/includes? (util/get-edit-content) "[["))
+      (util/exit-edit)
+      (is (= "b1" (util/get-text ".block-ref"))))))

+ 17 - 0
clj-e2e/test/logseq/e2e/fixtures.clj

@@ -0,0 +1,17 @@
+(ns logseq.e2e.fixtures
+  (:require [wally.main :as w]))
+
+;; TODO: save trace
+;; TODO: parallel support
+(defn open-page
+  [f & {:keys [headless]
+        :or {headless true}}]
+  (w/with-page-open
+    (w/make-page {:headless headless
+                  :persistent false
+                  :slow-mo 100
+                  ;; Set `slow-mo` lower to find more flaky tests
+                  ;; :slow-mo 30
+                  })
+    (w/navigate "http://localhost:3002")
+    (f)))

+ 76 - 0
clj-e2e/test/logseq/e2e/outliner_test.clj

@@ -0,0 +1,76 @@
+(ns logseq.e2e.outliner-test
+  (:require
+   [clojure.test :refer [deftest testing is use-fixtures]]
+   [logseq.e2e.fixtures :as fixtures]
+   [logseq.e2e.util :as util :refer [press]]
+   [wally.main :as w]))
+
+(use-fixtures :once fixtures/open-page)
+
+(deftest create-test-page-and-insert-blocks
+  (util/new-page "p1")
+  ;; a page block and a child block
+  (is (= 2 (util/blocks-count)))
+  (util/new-blocks ["first block" "second block"])
+  (util/exit-edit)
+  (is (= 3 (util/blocks-count))))
+
+(deftest indent-and-outdent-test
+  (util/new-page "p2")
+  (util/new-blocks ["b1" "b2"])
+  (testing "simple indent and outdent"
+    (util/indent)
+    (util/outdent))
+
+  (testing "indent a block with its children"
+    (util/new-block "b3")
+    (util/indent)
+    (press "ArrowUp")
+    (util/indent)
+    (util/exit-edit)
+    (let [[x1 x2 x3] (map (comp first util/bounding-xy #(w/find-one-by-text "span" %)) ["b1" "b2" "b3"])]
+      (is (< x1 x2 x3))))
+
+  (testing "unindent a block with its children"
+    (util/open-last-block)
+    (util/new-blocks ["b4" "b5"])
+    (util/indent)
+    (util/press "ArrowUp")
+    (util/outdent)
+    (util/exit-edit)
+    (let [[x2 x3 x4 x5] (map (comp first util/bounding-xy #(w/find-one-by-text "span" %)) ["b2" "b3" "b4" "b5"])]
+      (is (and (= x2 x4) (= x3 x5) (< x2 x3))))))
+
+(deftest move-up-down-test
+  (util/new-page "p3")
+  (util/new-blocks ["b1" "b2" "b3" "b4"])
+  (util/repeat-keyboard 2 "Shift+ArrowUp")
+  (let [contents (util/get-page-blocks-contents)]
+    (is (= contents ["b1" "b2" "b3" "b4"])))
+  (util/repeat-keyboard 2 (str (if util/mac? "Meta" "Alt") "+Shift+ArrowUp"))
+  (let [contents (util/get-page-blocks-contents)]
+    (is (= contents ["b3" "b4" "b1" "b2"])))
+  (util/repeat-keyboard 2 (str (if util/mac? "Meta" "Alt") "+Shift+ArrowDown"))
+  (let [contents (util/get-page-blocks-contents)]
+    (is (= contents ["b1" "b2" "b3" "b4"]))))
+
+(deftest delete-test
+  (testing "Delete blocks case 1"
+    (util/new-page "p4")
+    (util/new-blocks ["b1" "b2" "b3" "b4"])
+    (util/delete-blocks)                   ; delete b4
+    (util/repeat-keyboard 2 "Shift+ArrowUp") ; select b3 and b2
+    (util/delete-blocks)
+    (is (= "b1" (util/get-edit-content)))
+    (is (= 1 (util/page-blocks-count))))
+
+  (testing "Delete block with its children"
+    (util/new-page "p5")
+    (util/new-blocks ["b1" "b2" "b3" "b4"])
+    (util/indent)
+    (press "ArrowUp")
+    (util/indent)
+    (press "ArrowUp")
+    (util/delete-blocks)
+    (is (= "b1" (util/get-edit-content)))
+    (is (= 1 (util/page-blocks-count)))))

+ 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))

+ 10 - 10
deps/outliner/src/logseq/outliner/validate.cljs

@@ -1,14 +1,14 @@
 (ns logseq.outliner.validate
 (ns logseq.outliner.validate
   "Reusable DB graph validations for outliner level and above. Most validations
   "Reusable DB graph validations for outliner level and above. Most validations
   throw errors so the user action stops immediately to display a notification"
   throw errors so the user action stops immediately to display a notification"
-  (:require [clojure.string :as string]
+  (:require [clojure.set :as set]
+            [clojure.string :as string]
             [datascript.core :as d]
             [datascript.core :as d]
-            [logseq.db :as ldb]
-            [logseq.db.frontend.entity-util :as entity-util]
             [logseq.common.date :as common-date]
             [logseq.common.date :as common-date]
             [logseq.common.util.namespace :as ns-util]
             [logseq.common.util.namespace :as ns-util]
-            [clojure.set :as set]
-            [logseq.db.frontend.class :as db-class]))
+            [logseq.db :as ldb]
+            [logseq.db.frontend.class :as db-class]
+            [logseq.db.frontend.entity-util :as entity-util]))
 
 
 (defn ^:api validate-page-title-characters
 (defn ^:api validate-page-title-characters
   "Validates characters that must not be in a page title"
   "Validates characters that must not be in a page title"
@@ -164,10 +164,10 @@
     (when (and (:logseq.property/built-in? tag-ent)
     (when (and (:logseq.property/built-in? tag-ent)
                (not (ldb/class? tag-ent)))
                (not (ldb/class? tag-ent)))
       (throw (ex-info (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
       (throw (ex-info (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
-                    {:type :notification
-                     :payload {:message (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
-                               :type :error}
-                     :property-value v})))))
+                      {:type :notification
+                       :payload {:message (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
+                                 :type :error}
+                       :property-value v})))))
 
 
 (defn- disallow-node-cant-tag-with-private-tags
 (defn- disallow-node-cant-tag-with-private-tags
   [db block-eids v]
   [db block-eids v]
@@ -196,4 +196,4 @@
   [db block-eids v]
   [db block-eids v]
   (disallow-tagging-a-built-in-entity db block-eids)
   (disallow-tagging-a-built-in-entity db block-eids)
   (disallow-node-cant-tag-with-private-tags db block-eids v)
   (disallow-node-cant-tag-with-private-tags db block-eids v)
-  (disallow-node-cant-tag-with-built-in-non-tags db block-eids v))
+  (disallow-node-cant-tag-with-built-in-non-tags db block-eids v))

+ 7 - 7
deps/shui/src/logseq/shui/table/core.cljc

@@ -27,12 +27,11 @@
 
 
 (defn- select-some?
 (defn- select-some?
   [row-selection rows]
   [row-selection rows]
-  (boolean
-   (or
-    (and (seq (:selected-ids row-selection))
-         (some (:selected-ids row-selection) rows))
-    (and (seq (:exclude-ids row-selection))
-         (not= (count rows) (count (:exclude-ids row-selection)))))))
+  (or
+   (and (seq (:selected-ids row-selection))
+        (some (:selected-ids row-selection) rows))
+   (and (seq (:excluded-ids row-selection))
+        (not= (count rows) (count (:excluded-ids row-selection))))))
 
 
 (defn- select-all?
 (defn- select-all?
   [row-selection rows]
   [row-selection rows]
@@ -122,7 +121,8 @@
            ;; fns
            ;; fns
            :column-visible? (fn [column] (impl/column-visible? column visible-columns))
            :column-visible? (fn [column] (impl/column-visible? column visible-columns))
            :column-toggle-visibility (fn [column v] (set-visible-columns! (assoc visible-columns (impl/column-id column) v)))
            :column-toggle-visibility (fn [column v] (set-visible-columns! (assoc visible-columns (impl/column-id column) v)))
-           :selected-all? (or (:selected-all? row-selection)
+           :selected-all? (or (and (:selected-all? row-selection)
+                                   (nil? (seq (:excluded-ids row-selection))))
                               (select-all? row-selection filtered-rows))
                               (select-all? row-selection filtered-rows))
            :selected-some? (select-some? row-selection filtered-rows)
            :selected-some? (select-some? row-selection filtered-rows)
            :row-selected? (fn [row] (row-selected? row row-selection))
            :row-selected? (fn [row] (row-selected? row row-selection))

+ 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]

+ 6 - 0
src/main/frontend/common/missionary.cljs

@@ -108,6 +108,12 @@
     (let [x (m/?> (m/relieve {} >in))]
     (let [x (m/?> (m/relieve {} >in))]
       (m/amb x (do (m/? (m/sleep dur-ms)) (m/amb))))))
       (m/amb x (do (m/? (m/sleep dur-ms)) (m/amb))))))
 
 
+(defn snapshot-of-flow
+  "Return a task. take first value from f.
+  can be understood as `deref` in missionary"
+  [f]
+  (m/reduce {} nil (m/eduction (take 1) f)))
+
 (defn- fail-case-default-handler
 (defn- fail-case-default-handler
   [e]
   [e]
   (when-not (instance? Cancelled e)
   (when-not (instance? Cancelled e)

+ 5 - 3
src/main/frontend/components/block.cljs

@@ -613,7 +613,8 @@
 (defn open-page-ref
 (defn open-page-ref
   [config page-entity e page-name contents-page?]
   [config page-entity e page-name contents-page?]
   (when (not (util/right-click? e))
   (when (not (util/right-click? e))
-    (let [page (or (first (:block/_alias page-entity)) page-entity)]
+    (let [ignore-alias? (:ignore-alias? config)
+          page (or (and (not ignore-alias?) (first (:block/_alias page-entity))) page-entity)]
       (cond
       (cond
         (gobj/get e "shiftKey")
         (gobj/get e "shiftKey")
         (when page
         (when page
@@ -635,8 +636,9 @@
         ((:on-pointer-down config) e)
         ((:on-pointer-down config) e)
 
 
         :else
         :else
-        (-> (or (:on-redirect-to-page config) route-handler/redirect-to-page!)
-            (apply [(or (:block/uuid page) (:block/name page))])))))
+        (let [f (or (:on-redirect-to-page config) route-handler/redirect-to-page!)]
+          (f (or (:block/uuid page) (:block/name page))
+             {:ignore-alias? ignore-alias?})))))
   (when (and contents-page?
   (when (and contents-page?
              (util/mobile?)
              (util/mobile?)
              (state/get-left-sidebar-open?))
              (state/get-left-sidebar-open?))

+ 28 - 21
src/main/frontend/components/objects.cljs

@@ -7,6 +7,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
             [frontend.state :as state]
+            [logseq.db :as ldb]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property :as db-property]
             [logseq.outliner.property :as outliner-property]
             [logseq.outliner.property :as outliner-property]
             [logseq.shui.ui :as shui]
             [logseq.shui.ui :as shui]
@@ -45,33 +46,38 @@
                   (remove #(contains? #{:logseq.property.asset/checksum} (:id %)) columns*)
                   (remove #(contains? #{:logseq.property.asset/checksum} (:id %)) columns*)
                   :else
                   :else
                   columns*)
                   columns*)
-        columns (if (= (:db/ident class) :logseq.class/Asset)
+        db-ident (:db/ident class)
+        asset? (= db-ident :logseq.class/Asset)
+        columns (if asset?
                   ;; Insert in front of tag's properties
                   ;; Insert in front of tag's properties
                   (let [[before-cols after-cols] (split-with #(not (db-property/logseq-property? (:id %))) columns)]
                   (let [[before-cols after-cols] (split-with #(not (db-property/logseq-property? (:id %))) columns)]
                     (concat before-cols [(build-asset-file-column config)] after-cols))
                     (concat before-cols [(build-asset-file-column config)] after-cols))
-                  columns)]
+                  columns)
+        add-new-object! (when (or asset? (not (ldb/private-tags (:db/ident class))))
+                          (fn [_view table {:keys [properties]}]
+                            (let [set-data! (get-in table [:data-fns :set-data!])
+                                  full-data (:full-data table)]
+                              (if (= :logseq.class/Asset (:db/ident class))
+                                (shui/dialog-open!
+                                 (fn []
+                                   [:div.flex.flex-col.gap-2
+                                    [:div.font-medium "Add assets"]
+                                    (filepicker/picker
+                                     {:on-change (fn [_e files]
+                                                   (p/let [entities (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true)]
+                                                     (shui/dialog-close!)
+                                                     (when (seq entities)
+                                                       (set-data! (concat full-data (map :db/id entities))))))})]))
+                                (p/let [block (add-new-class-object! class properties)]
+                                  (when (:db/id block)
+                                    (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block)
+                                    (set-data! (conj (vec full-data) (:db/id block)))))))))]
 
 
     (views/view {:config config
     (views/view {:config config
                  :view-parent class
                  :view-parent class
                  :view-feature-type :class-objects
                  :view-feature-type :class-objects
                  :columns columns
                  :columns columns
-                 :add-new-object! (fn [_view table {:keys [properties]}]
-                                    (let [set-data! (get-in table [:data-fns :set-data!])
-                                          full-data (:full-data table)]
-                                      (if (= :logseq.class/Asset (:db/ident class))
-                                        (shui/dialog-open!
-                                         (fn []
-                                           [:div.flex.flex-col.gap-2
-                                            [:div.font-medium "Add assets"]
-                                            (filepicker/picker
-                                             {:on-change (fn [_e files]
-                                                           (p/let [entities (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true)]
-                                                             (shui/dialog-close!)
-                                                             (when (seq entities)
-                                                               (set-data! (concat full-data (map :db/id entities))))))})]))
-                                        (p/let [block (add-new-class-object! class properties)]
-                                          (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block)
-                                          (set-data! (conj (vec full-data) (:db/id block)))))))
+                 :add-new-object! add-new-object!
                  :show-add-property? true
                  :show-add-property? true
                  :show-items-count? true
                  :show-items-count? true
                  :add-property! (fn []
                  :add-property! (fn []
@@ -114,8 +120,9 @@
                                     (p/let [set-data! (get-in table [:data-fns :set-data!])
                                     (p/let [set-data! (get-in table [:data-fns :set-data!])
                                             full-data (:full-data table)
                                             full-data (:full-data table)
                                             block (add-new-property-object! property properties)]
                                             block (add-new-property-object! property properties)]
-                                      (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block)
-                                      (set-data! (conj (vec full-data) (:db/id block)))))
+                                      (when (:db/id block)
+                                        (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block)
+                                        (set-data! (conj (vec full-data) (:db/id block))))))
                  ;; TODO: Add support for adding column
                  ;; TODO: Add support for adding column
                  :show-add-property? false})))
                  :show-add-property? false})))
 
 

+ 42 - 43
src/main/frontend/components/page.cljs

@@ -555,7 +555,7 @@
     [:div.page-tabs
     [:div.page-tabs
      (shui/tabs
      (shui/tabs
       {:defaultValue default-tab
       {:defaultValue default-tab
-       :class (str "w-full")}
+       :class "w-full"}
       (when (or both? property?)
       (when (or both? property?)
         [:div.flex.flex-row.gap-1.items-center
         [:div.flex.flex-row.gap-1.items-center
          (shui/tabs-list
          (shui/tabs-list
@@ -751,14 +751,13 @@
 
 
 (rum/defcs page-cp
 (rum/defcs page-cp
   [state option]
   [state option]
-  (rum/with-key
-    (page-aux option)
-    (str
-     (state/get-current-repo)
-     "-"
-     (or (:db/id option)
-         (:page-name option)
-         (get-page-name state)))))
+  (let [page-name (or (:page-name option) (get-page-name state))]
+    (rum/with-key
+      (page-aux (assoc option :page-name page-name))
+      (str
+       (state/get-current-repo)
+       "-"
+       (or (:db/id option) page-name)))))
 
 
 (defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
 (defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
 
 
@@ -918,25 +917,25 @@
                  (ui/tooltip
                  (ui/tooltip
                    ;; Slider keeps track off the range from min created-at to max created-at
                    ;; Slider keeps track off the range from min created-at to max created-at
                    ;; because there were bugs with setting min and max directly
                    ;; because there were bugs with setting min and max directly
-                   (ui/slider created-at-filter
-                     {:min 0
-                      :max (- (get-in graph [:all-pages :created-at-max])
-                             (get-in graph [:all-pages :created-at-min]))
-                      :on-change #(do
-                                    (reset! *created-at-filter (int %))
-                                    (set-setting! :created-at-filter (int %)))})
-                   [:div.px-1 (str (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))])])
+                  (ui/slider created-at-filter
+                             {:min 0
+                              :max (- (get-in graph [:all-pages :created-at-max])
+                                      (get-in graph [:all-pages :created-at-min]))
+                              :on-change #(do
+                                            (reset! *created-at-filter (int %))
+                                            (set-setting! :created-at-filter (int %)))})
+                  [:div.px-1 (str (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))])])
 
 
               (when (seq focus-nodes)
               (when (seq focus-nodes)
                 [:div.flex.flex-col.mb-2
                 [:div.flex.flex-col.mb-2
                  [:p {:title "N hops from selected nodes"}
                  [:p {:title "N hops from selected nodes"}
                   "N hops from selected nodes"]
                   "N hops from selected nodes"]
                  (ui/tooltip
                  (ui/tooltip
-                   (ui/slider (or n-hops 10)
-                     {:min 1
-                      :max 10
-                      :on-change #(reset! *n-hops (int %))})
-                   [:div n-hops])])
+                  (ui/slider (or n-hops 10)
+                             {:min 1
+                              :max 10
+                              :on-change #(reset! *n-hops (int %))})
+                  [:div n-hops])])
 
 
               [:a.opacity-70.opacity-100 {:on-click (fn []
               [:a.opacity-70.opacity-100 {:on-click (fn []
                                                       (swap! *graph-reset? not)
                                                       (swap! *graph-reset? not)
@@ -983,37 +982,37 @@
                [:p {:title "Link Distance"}
                [:p {:title "Link Distance"}
                 "Link Distance"]
                 "Link Distance"]
                (ui/tooltip
                (ui/tooltip
-                 (ui/slider (/ link-dist 10)
-                   {:min 1                                  ;; 10
-                    :max 18                                 ;; 180
-                    :on-change #(let [value (int %)]
-                                  (reset! *link-dist (* value 10))
-                                  (set-forcesetting! :link-dist (* value 10)))})
-                 [:div link-dist])]
+                (ui/slider (/ link-dist 10)
+                           {:min 1                                  ;; 10
+                            :max 18                                 ;; 180
+                            :on-change #(let [value (int %)]
+                                          (reset! *link-dist (* value 10))
+                                          (set-forcesetting! :link-dist (* value 10)))})
+                [:div link-dist])]
 
 
               [:div.flex.flex-col.mb-2
               [:div.flex.flex-col.mb-2
                [:p {:title "Charge Strength"}
                [:p {:title "Charge Strength"}
                 "Charge Strength"]
                 "Charge Strength"]
                (ui/tooltip
                (ui/tooltip
-                 (ui/slider (/ charge-strength 100)
-                   {:min -10                                ;;-1000
-                    :max 10                                 ;;1000
-                    :on-change #(let [value (int %)]
-                                  (reset! *charge-strength (* value 100))
-                                  (set-forcesetting! :charge-strength (* value 100)))})
-                 [:div charge-strength])]
+                (ui/slider (/ charge-strength 100)
+                           {:min -10                                ;;-1000
+                            :max 10                                 ;;1000
+                            :on-change #(let [value (int %)]
+                                          (reset! *charge-strength (* value 100))
+                                          (set-forcesetting! :charge-strength (* value 100)))})
+                [:div charge-strength])]
 
 
               [:div.flex.flex-col.mb-2
               [:div.flex.flex-col.mb-2
                [:p {:title "Charge Range"}
                [:p {:title "Charge Range"}
                 "Charge Range"]
                 "Charge Range"]
                (ui/tooltip
                (ui/tooltip
-                 (ui/slider (/ charge-range 100)
-                   {:min 5                                  ;;500
-                    :max 40                                 ;;4000
-                    :on-change #(let [value (int %)]
-                                  (reset! *charge-range (* value 100))
-                                  (set-forcesetting! :charge-range (* value 100)))})
-                 [:div charge-range])]
+                (ui/slider (/ charge-range 100)
+                           {:min 5                                  ;;500
+                            :max 40                                 ;;4000
+                            :on-change #(let [value (int %)]
+                                          (reset! *charge-range (* value 100))
+                                          (set-forcesetting! :charge-range (* value 100)))})
+                [:div charge-range])]
 
 
               [:a
               [:a
                {:on-click (fn []
                {:on-click (fn []

+ 22 - 17
src/main/frontend/components/property/value.cljs

@@ -703,22 +703,26 @@
                     (remove #(= :logseq.property/empty-placeholder (:db/ident %))
                     (remove #(= :logseq.property/empty-placeholder (:db/ident %))
                             (if (every? entity-map? v) v [v])))
                             (if (every? entity-map? v) v [v])))
                   (remove (fn [node]
                   (remove (fn [node]
-                            (or (= (:db/id block) (:db/id node))
-                             ;; A page's alias can't be itself
-                                (and alias? (= (or (:db/id (:block/page block))
-                                                   (:db/id block))
-                                               (:db/id node)))
-                                (cond
-                                  (= property-type :class)
-                                  (ldb/private-tags (:db/ident node))
-
-                                  (and property-type (not= property-type :node))
-                                  (if (= property-type :page)
-                                    (not (db/page? node))
-                                    (not (contains? (ldb/get-entity-types node) property-type)))
-
-                                  :else
-                                  false)))
+                            (let [node' (if (:value node)
+                                          (assoc (:value node) :block/title (:label node))
+                                          node)
+                                  node (or (some-> (:db/id node') db/entity) node)]
+                              (or (= (:db/id block) (:db/id node))
+                                  ;; A page's alias can't be itself
+                                  (and alias? (= (or (:db/id (:block/page block))
+                                                     (:db/id block))
+                                                 (:db/id node)))
+                                  (cond
+                                    (= property-type :class)
+                                    (ldb/private-tags (:db/ident node))
+
+                                    (and property-type (not= property-type :node))
+                                    (if (= property-type :page)
+                                      (not (db/page? node))
+                                      (not (contains? (ldb/get-entity-types node) property-type)))
+
+                                    :else
+                                    false))))
                           result)))
                           result)))
 
 
         options (map (fn [node]
         options (map (fn [node]
@@ -1071,7 +1075,8 @@
                      :tag? tag?
                      :tag? tag?
                      :property-position property-position
                      :property-position property-position
                      :meta-click? other-position?
                      :meta-click? other-position?
-                     :table-view? table-view?} value)
+                     :table-view? table-view?
+                     :ignore-alias? (= :block/alias (:db/ident property))} value)
            (:db/id value)))
            (:db/id value)))
 
 
        (contains? #{:node :class :property :page} type)
        (contains? #{:node :class :property :page} type)

+ 0 - 51
src/main/frontend/components/repo.cljs

@@ -1,6 +1,5 @@
 (ns frontend.components.repo
 (ns frontend.components.repo
   (:require [clojure.string :as string]
   (:require [clojure.string :as string]
-            [electron.ipc :as ipc]
             [frontend.common.async-util :as async-util]
             [frontend.common.async-util :as async-util]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
@@ -203,12 +202,6 @@
                          (repo-handler/refresh-repos!))))]]
                          (repo-handler/refresh-repos!))))]]
          (repos-inner remote-graphs)])]]))
          (repos-inner remote-graphs)])]]))
 
 
-(defn- check-multiple-windows?
-  [state]
-  (when (util/electron?)
-    (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
-      (reset! (::electron-multiple-windows? state) multiple-windows?))))
-
 (defn- repos-dropdown-links [repos current-repo downloading-graph-id & {:as opts}]
 (defn- repos-dropdown-links [repos current-repo downloading-graph-id & {:as opts}]
   (let [switch-repos (if-not (nil? current-repo)
   (let [switch-repos (if-not (nil? current-repo)
                        (remove (fn [repo] (= current-repo (:url repo))) repos) repos) ; exclude current repo
                        (remove (fn [repo] (= current-repo (:url repo))) repos) repos) ; exclude current repo
@@ -362,50 +355,6 @@
                     icon [:div title]]))))))]
                     icon [:div title]]))))))]
      (repos-footer multiple-windows? db-based?)]))
      (repos-footer multiple-windows? db-based?)]))
 
 
-(rum/defcs repos-dropdown < rum/reactive
-  (rum/local false ::electron-multiple-windows?)
-  [state & {:as opts}]
-  (let [current-repo (state/sub :git/current-repo)
-        login? (boolean (state/sub :auth/id-token))]
-    (let [repos (state/sub [:me :repos])
-          remotes (state/sub [:file-sync/remote-graphs :graphs])
-          rtc-graphs (state/sub :rtc/graphs)
-          db-based? (config/db-based-graph? current-repo)
-          repos (sort-repos-with-metadata-local repos)
-          repos (distinct
-                 (if (and (or (seq remotes) (seq rtc-graphs)) login?)
-                   (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))]
-      (let [remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
-            repo-name (when current-repo (db/get-repo-name current-repo))
-            short-repo-name (if current-repo
-                              (db/get-short-repo-name repo-name)
-                              "Select a Graph")]
-        (shui/trigger-as :a
-                         {:tab-index 0
-                          :class "item cp__repos-select-trigger"
-                          :on-pointer-down
-                          (fn [^js e]
-                            (check-multiple-windows? state)
-                            (some-> (.-target e)
-                                    (.closest "a.item")
-                                    (shui/popup-show!
-                                     (fn [{:keys [id]}] (repos-dropdown-content (assoc opts :contentid id)))
-                                     {:as-dropdown? true
-                                      :auto-focus? false
-                                      :align "start"
-                                      :content-props {:class "repos-list"
-                                                      :data-mode (when db-based? "db")}})))
-                          :title repo-name}      ;; show full path on hover
-                         [:div.flex.relative.graph-icon.rounded
-                          (shui/tabler-icon "database" {:size 15})]
-
-                         [:div.repo-switch.pr-2.whitespace-nowrap
-                          [:span.repo-name.font-medium
-                           [:span.repo-text.overflow-hidden.text-ellipsis
-                            (if (config/demo-graph? short-repo-name) "Demo" short-repo-name)]
-                           (when remote? [:span.pl-1 (ui/icon "cloud")])]
-                          [:span.dropdown-caret]])))))
-
 (rum/defcs graphs-selector < rum/reactive
 (rum/defcs graphs-selector < rum/reactive
   [_state]
   [_state]
   (let [current-repo (state/get-current-repo)
   (let [current-repo (state/get-current-repo)

+ 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))
@@ -56,4 +56,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')))))

+ 6 - 0
src/main/frontend/flows.cljs

@@ -19,6 +19,8 @@
 (def ^:private current-login-user-validator (ma/validator current-login-user-schema))
 (def ^:private current-login-user-validator (ma/validator current-login-user-schema))
 (def *current-login-user (atom nil :validator current-login-user-validator))
 (def *current-login-user (atom nil :validator current-login-user-validator))
 
 
+(def *network-online? (atom nil))
+
 ;; Public Flows
 ;; Public Flows
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 
@@ -42,3 +44,7 @@
             (fn dtor [] (.removeEventListener ^js js/document "visibilitychange" callback-fn)))))
             (fn dtor [] (.removeEventListener ^js js/document "visibilitychange" callback-fn)))))
        (m/eduction (dedupe))
        (m/eduction (dedupe))
        (m/relieve)))
        (m/relieve)))
+
+(def network-online-event-flow
+  (->> (m/watch *network-online?)
+       (m/eduction (filter true?))))

+ 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
   []
   []

+ 24 - 3
src/main/frontend/handler/db_based/rtc_flows.cljs

@@ -98,7 +98,7 @@ conditions:
     (let [visibility (m/?< flows/document-visibility-state-flow)]
     (let [visibility (m/?< flows/document-visibility-state-flow)]
       (try
       (try
         (if (= "visible" visibility)
         (if (= "visible" visibility)
-          (let [rtc-lock (:rtc-lock (m/? (m/reduce {} nil (m/eduction (take 1) rtc-state-flow))))]
+          (let [rtc-lock (:rtc-lock (m/? (c.m/snapshot-of-flow rtc-state-flow)))]
             (if (not rtc-lock)
             (if (not rtc-lock)
               :document-visible&rtc-not-running
               :document-visible&rtc-not-running
               (m/amb)))
               (m/amb)))
@@ -106,20 +106,41 @@ conditions:
         (catch Cancelled _
         (catch Cancelled _
           (m/amb))))))
           (m/amb))))))
 
 
+(def ^:private network-online&rtc-not-running-flow
+  (m/ap
+    (let [online? (m/?< flows/network-online-event-flow)]
+      (try
+        (if online?
+          (let [rtc-lock (:rtc-lock (m/? (c.m/snapshot-of-flow rtc-state-flow)))]
+            (if (not rtc-lock)
+              :network-online&rtc-not-running
+              (m/amb)))
+          (m/amb))
+        (catch Cancelled _
+          (m/amb))))))
+
 (def trigger-start-rtc-flow
 (def trigger-start-rtc-flow
   (->>
   (->>
-   [(m/eduction
+   [;; login-user changed
+    (m/eduction
      (keep (fn [user] (when (:email user) [:login])))
      (keep (fn [user] (when (:email user) [:login])))
      flows/current-login-user-flow)
      flows/current-login-user-flow)
+    ;; repo changed
     (m/eduction
     (m/eduction
      (keep (fn [repo] (when repo [:graph-switch repo])))
      (keep (fn [repo] (when repo [:graph-switch repo])))
      flows/current-repo-flow)
      flows/current-repo-flow)
+    ;; trigger-rtc by somewhere else
     (m/eduction
     (m/eduction
      (keep (fn [repo] (when repo [:trigger-rtc repo])))
      (keep (fn [repo] (when repo [:trigger-rtc repo])))
      (m/watch *rtc-start-trigger))
      (m/watch *rtc-start-trigger))
+    ;; document visibilitychange->true
+    (m/eduction
+     (map vector)
+     document-visible&rtc-not-running-flow)
+    ;; network online->true
     (m/eduction
     (m/eduction
      (map vector)
      (map vector)
-     document-visible&rtc-not-running-flow)]
+     network-online&rtc-not-running-flow)]
    (apply c.m/mix)
    (apply c.m/mix)
    (m/eduction (filter (fn [_] (some? (state/get-auth-id-token)))))
    (m/eduction (filter (fn [_] (some? (state/get-auth-id-token)))))
    (c.m/debounce 200)))
    (c.m/debounce 200)))

+ 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

+ 2 - 2
src/main/frontend/handler/route.cljs

@@ -74,7 +74,7 @@
   "`page-name` can be a block uuid or name, prefer to use uuid than name when possible"
   "`page-name` can be a block uuid or name, prefer to use uuid than name when possible"
   ([page-name]
   ([page-name]
    (redirect-to-page! page-name {}))
    (redirect-to-page! page-name {}))
-  ([page-name {:keys [anchor push click-from-recent? block-id new-whiteboard?]
+  ([page-name {:keys [anchor push click-from-recent? block-id new-whiteboard? ignore-alias?]
                :or {click-from-recent? false}
                :or {click-from-recent? false}
                :as opts}]
                :as opts}]
    (when (or (uuid? page-name)
    (when (or (uuid? page-name)
@@ -85,7 +85,7 @@
                 (or (ldb/hidden? page)
                 (or (ldb/hidden? page)
                     (and (ldb/built-in? page) (ldb/private-built-in-page? page))))
                     (and (ldb/built-in? page) (ldb/private-built-in-page? page))))
          (notification/show! "Cannot go to an internal page." :warning)
          (notification/show! "Cannot go to an internal page." :warning)
-         (if-let [source (db/get-alias-source-page (state/get-current-repo) (:db/id page))]
+         (if-let [source (and (not ignore-alias?) (db/get-alias-source-page (state/get-current-repo) (:db/id page)))]
            (redirect-to-page! (:block/uuid source) opts)
            (redirect-to-page! (:block/uuid source) opts)
            (do
            (do
            ;; Always skip onboarding when loading an existing whiteboard
            ;; Always skip onboarding when loading an existing whiteboard

+ 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"

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

@@ -1,13 +1,26 @@
 (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 [d (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))
+                                   d)) tx-data)]
+        (when-let [new-title (:block/title (db/entity (:e d)))]
+          (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 +68,9 @@
                               tx-data))]
                               tx-data))]
               (d/transact! conn tx-data' tx-meta))
               (d/transact! conn tx-data' tx-meta))
 
 
+            (when-not (= (:client-id tx-meta) (:client-id @state/state))
+              (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

+ 20 - 12
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                    #{}
@@ -73,6 +75,7 @@
       :nfs/refreshing?                       nil
       :nfs/refreshing?                       nil
       :instrument/disabled?                  (storage/get "instrument-disabled")
       :instrument/disabled?                  (storage/get "instrument-disabled")
       ;; TODO: how to detect the network reliably?
       ;; TODO: how to detect the network reliably?
+      ;; NOTE: prefer to use flows/network-online-event-flow
       :network/online?         true
       :network/online?         true
       :indexeddb/support?      true
       :indexeddb/support?      true
       :me                      nil
       :me                      nil
@@ -1036,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)))
@@ -1064,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)]
@@ -1638,7 +1642,11 @@ Similar to re-frame subscriptions"
 
 
 (defn set-online!
 (defn set-online!
   [value]
   [value]
-  (set-state! :network/online? value))
+  (set-state! :network/online? value)
+  ;; to avoid watch whole big state atom,
+  ;; there's an atom flows/*network-online?,
+  ;; then we can use flows/network-online-event-flow
+  (reset! flows/*network-online? value))
 
 
 (defn get-plugins-slash-commands
 (defn get-plugins-slash-commands
   []
   []

+ 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 - 0
src/main/frontend/worker/db/migrate.cljs

@@ -998,6 +998,16 @@
 
 
 (defn- build-invalid-tx [entity eid]
 (defn- build-invalid-tx [entity eid]
   (cond
   (cond
+    (:block/schema entity)
+    [[:db/retract eid :block/schema]]
+
+    (and (nil? (:block/uuid entity))
+         (or (:block/title entity)
+             (:logseq.property.asset/size entity)
+             (:logseq.property.asset/type entity)
+             (:logseq.property.asset/checksum entity)))
+    [[:db/retractEntity eid]]
+
     (and (:db/ident entity) (= "logseq.property.attribute" (namespace (:db/ident entity))))
     (and (:db/ident entity) (= "logseq.property.attribute" (namespace (:db/ident entity))))
     [[:db/retractEntity (:db/id entity)]]
     [[:db/retractEntity (:db/id entity)]]
 
 

+ 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!

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

@@ -4,10 +4,10 @@
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.common.file.core :as common-file]
             [frontend.common.file.core :as common-file]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
+            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.property :as gp-property]
-            [logseq.outliner.tree :as otree]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]))
+            [logseq.outliner.tree :as otree]))
 
 
 (defn- safe-keywordize
 (defn- safe-keywordize
   [block]
   [block]

+ 1 - 3
src/main/frontend/worker/flows.cljs

@@ -5,9 +5,7 @@
 
 
 (def online-event-flow
 (def online-event-flow
   (->> (m/watch (get @worker-state/*state :thread-atom/online-event))
   (->> (m/watch (get @worker-state/*state :thread-atom/online-event))
-       (m/eduction
-        (drop-while nil?)
-        (filter true?))))
+       (m/eduction (filter true?))))
 
 
 (comment
 (comment
   ((m/reduce (fn [_ x] (prn :xxx x)) online-event-flow) prn prn))
   ((m/reduce (fn [_ x] (prn :xxx x)) online-event-flow) prn prn))

+ 24 - 12
src/main/frontend/worker/pipeline.cljs

@@ -1,9 +1,11 @@
 (ns frontend.worker.pipeline
 (ns frontend.worker.pipeline
   "Pipeline work after transaction"
   "Pipeline work after transaction"
-  (:require [datascript.core :as d]
+  (:require [clojure.string :as string]
+            [datascript.core :as d]
             [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]]
@@ -105,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))
@@ -176,7 +178,7 @@
      :logseq.property.user/email email}))
      :logseq.property.user/email email}))
 
 
 (defn- add-created-by-ref-hook
 (defn- add-created-by-ref-hook
-  [db-after tx-data tx-meta]
+  [db-before db-after tx-data tx-meta]
   (when (and (not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta)))
   (when (and (not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta)))
              (seq tx-data))
              (seq tx-data))
     (when-let [decoded-id-token (some-> (worker-state/get-id-token) worker-util/parse-jwt)]
     (when-let [decoded-id-token (some-> (worker-state/get-id-token) worker-util/parse-jwt)]
@@ -187,24 +189,34 @@
             add-created-by-tx-data
             add-created-by-tx-data
             (keep
             (keep
              (fn [datom]
              (fn [datom]
-               (when (and (keyword-identical? :block/uuid (:a datom))
-                          (:added datom))
-                 (let [e (:e datom)
-                       ent (d/entity db-after e)]
-                   (when-not (:logseq.property/created-by-ref ent)
-                     [:db/add e :logseq.property/created-by-ref created-by-id]))))
+               (let [attr (:a datom)
+                     value (:v datom)
+                     e (:e datom)]
+                 (cond
+                   ;; add created-by for new-block
+                   (and (keyword-identical? :block/uuid attr)
+                        (:added datom))
+                   (let [ent (d/entity db-after e)]
+                     (when-not (:logseq.property/created-by-ref ent)
+                       [:db/add e :logseq.property/created-by-ref created-by-id]))
+
+                   ;; update created-by when block change from empty-block-title to non-empty
+                   (and (keyword-identical? :block/title attr)
+                        (not (string/blank? value))
+                        (string/blank? (:block/title (d/entity db-before e))))
+                   [:db/add e :logseq.property/created-by-ref created-by-id])))
              tx-data)]
              tx-data)]
         (cond->> add-created-by-tx-data
         (cond->> add-created-by-tx-data
           (nil? created-by-ent) (cons created-by-block))))))
           (nil? created-by-ent) (cons created-by-block))))))
 
 
 (defn- compute-extra-tx-data
 (defn- compute-extra-tx-data
   [repo tx-report]
   [repo tx-report]
-  (let [{:keys [db-after tx-data tx-meta]} tx-report
+  (let [{:keys [db-before db-after tx-data tx-meta]} tx-report
         display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data)
         display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data)
         commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta))
         commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta))
                       (commands/run-commands tx-report))
                       (commands/run-commands tx-report))
         insert-templates-tx (insert-tag-templates repo tx-report)
         insert-templates-tx (insert-tag-templates repo tx-report)
-        created-by-tx (add-created-by-ref-hook db-after tx-data tx-meta)]
+        created-by-tx (add-created-by-ref-hook db-before db-after tx-data tx-meta)]
     (concat display-blocks-tx-data commands-tx insert-templates-tx created-by-tx)))
     (concat display-blocks-tx-data commands-tx insert-templates-tx created-by-tx)))
 
 
 (defn- invoke-hooks-default
 (defn- invoke-hooks-default

+ 41 - 15
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)]
@@ -479,7 +503,7 @@
 
 
 (defn new-task--get-debug-state
 (defn new-task--get-debug-state
   []
   []
-  (m/reduce {} nil (m/eduction (take 1) create-get-state-flow)))
+  (c.m/snapshot-of-flow create-get-state-flow))
 
 
 (defn new-task--upload-graph
 (defn new-task--upload-graph
   [token repo remote-graph-name]
   [token repo remote-graph-name]
@@ -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)))))))