Browse Source

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

charlie 6 months ago
parent
commit
e024857d1e
62 changed files with 1515 additions and 564 deletions
  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.thingatpt thingatpt
              frontend.util.url url-util
+             frontend.worker.shared-service shared-service
              frontend.worker.handler.page worker-page
              frontend.worker.pipeline worker-pipeline
              frontend.worker.state worker-state

+ 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/.clj-kondo
 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
 - `scripts` - Dev scripts
 - `e2e-tests/` - end to end frontend tests
+- `clj-e2e/` - end to end clj frontend tests
 - `android/` -  Android 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)))
         nil)
       (tc/to-long (tc/to-date value)))))
+
+(defn keyword->string
+  [x]
+  (if (keyword? x)
+    (if-let [nn (namespace x)]
+      (str nn "/" (name x))
+      (name x))
+    x))

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

@@ -1,14 +1,14 @@
 (ns logseq.outliner.validate
   "Reusable DB graph validations for outliner level and above. Most validations
   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]
-            [logseq.db :as ldb]
-            [logseq.db.frontend.entity-util :as entity-util]
             [logseq.common.date :as common-date]
             [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
   "Validates characters that must not be in a page title"
@@ -164,10 +164,10 @@
     (when (and (:logseq.property/built-in? 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)))
-                    {: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
   [db block-eids v]
@@ -196,4 +196,4 @@
   [db block-eids v]
   (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-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?
   [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?
   [row-selection rows]
@@ -122,7 +121,8 @@
            ;; fns
            :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)))
-           :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))
            :selected-some? (select-some? row-selection filtered-rows)
            :row-selected? (fn [row] (row-selected? row row-selection))

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

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

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

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

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

@@ -108,6 +108,12 @@
     (let [x (m/?> (m/relieve {} >in))]
       (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
   [e]
   (when-not (instance? Cancelled e)

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

@@ -613,7 +613,8 @@
 (defn open-page-ref
   [config page-entity e page-name contents-page?]
   (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
         (gobj/get e "shiftKey")
         (when page
@@ -635,8 +636,9 @@
         ((:on-pointer-down config) e)
 
         :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?
              (util/mobile?)
              (state/get-left-sidebar-open?))

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

@@ -7,6 +7,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
+            [logseq.db :as ldb]
             [logseq.db.frontend.property :as db-property]
             [logseq.outliner.property :as outliner-property]
             [logseq.shui.ui :as shui]
@@ -45,33 +46,38 @@
                   (remove #(contains? #{:logseq.property.asset/checksum} (:id %)) columns*)
                   :else
                   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
                   (let [[before-cols after-cols] (split-with #(not (db-property/logseq-property? (:id %))) columns)]
                     (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
                  :view-parent class
                  :view-feature-type :class-objects
                  :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-items-count? true
                  :add-property! (fn []
@@ -114,8 +120,9 @@
                                     (p/let [set-data! (get-in table [:data-fns :set-data!])
                                             full-data (:full-data table)
                                             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
                  :show-add-property? false})))
 

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

@@ -555,7 +555,7 @@
     [:div.page-tabs
      (shui/tabs
       {:defaultValue default-tab
-       :class (str "w-full")}
+       :class "w-full"}
       (when (or both? property?)
         [:div.flex.flex-row.gap-1.items-center
          (shui/tabs-list
@@ -751,14 +751,13 @@
 
 (rum/defcs page-cp
   [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]))
 
@@ -918,25 +917,25 @@
                  (ui/tooltip
                    ;; Slider keeps track off the range from min created-at to max created-at
                    ;; 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)
                 [:div.flex.flex-col.mb-2
                  [:p {:title "N hops from selected nodes"}
                   "N hops from selected nodes"]
                  (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 []
                                                       (swap! *graph-reset? not)
@@ -983,37 +982,37 @@
                [:p {:title "Link Distance"}
                 "Link Distance"]
                (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
                [:p {:title "Charge Strength"}
                 "Charge Strength"]
                (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
                [:p {:title "Charge Range"}
                 "Charge Range"]
                (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
                {:on-click (fn []

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

@@ -703,22 +703,26 @@
                     (remove #(= :logseq.property/empty-placeholder (:db/ident %))
                             (if (every? entity-map? v) v [v])))
                   (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)))
 
         options (map (fn [node]
@@ -1071,7 +1075,8 @@
                      :tag? tag?
                      :property-position property-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)))
 
        (contains? #{:node :class :property :page} type)

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

@@ -1,6 +1,5 @@
 (ns frontend.components.repo
   (:require [clojure.string :as string]
-            [electron.ipc :as ipc]
             [frontend.common.async-util :as async-util]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
@@ -203,12 +202,6 @@
                          (repo-handler/refresh-repos!))))]]
          (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}]
   (let [switch-repos (if-not (nil? current-repo)
                        (remove (fn [repo] (= current-repo (:url repo))) repos) repos) ; exclude current repo
@@ -362,50 +355,6 @@
                     icon [:div title]]))))))]
      (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
   [_state]
   (let [current-repo (state/get-current-repo)

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

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

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

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

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

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

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

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

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

@@ -19,6 +19,8 @@
 (def ^:private current-login-user-validator (ma/validator current-login-user-schema))
 (def *current-login-user (atom nil :validator current-login-user-validator))
 
+(def *network-online? (atom nil))
+
 ;; Public Flows
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
@@ -42,3 +44,7 @@
             (fn dtor [] (.removeEventListener ^js js/document "visibilitychange" callback-fn)))))
        (m/eduction (dedupe))
        (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))]
     (p/do!
      (js/Promise. user-handler/task--ensure-id&access-token)
-     (when stop-before-start? (<rtc-stop!))
-     (let [token (state/get-auth-id-token)]
-       (p/let [start-ex (state/<invoke-db-worker :thread-api/rtc-start repo token)
-               ex-data* (:ex-data start-ex)
-               _ (case (:type ex-data*)
-                   (:rtc.exception/not-rtc-graph
-                    :rtc.exception/not-found-db-conn)
-                   (notification/show! (:ex-message start-ex) :error)
+     (p/let [start-ex (state/<invoke-db-worker :thread-api/rtc-start stop-before-start?)
+             ex-data* (:ex-data start-ex)
+             _ (case (:type ex-data*)
+                 (:rtc.exception/not-rtc-graph
+                  :rtc.exception/not-found-db-conn)
+                 (notification/show! (:ex-message start-ex) :error)
 
-                   :rtc.exception/major-schema-version-mismatched
-                   (case (:sub-type ex-data*)
-                     :download
-                     (notification-download-higher-schema-graph! repo graph-uuid (:remote ex-data*))
-                     :create-branch
-                     (notification-upload-higher-schema-graph! repo)
-                        ;; else
-                     (do (log/info :start-ex start-ex)
-                         (notification/show! [:div
-                                              [:div (:ex-message start-ex)]
-                                              [:div (-> ex-data*
-                                                        (select-keys [:app :local :remote])
-                                                        pp/pprint
-                                                        with-out-str)]]
-                                             :error)))
+                 :rtc.exception/major-schema-version-mismatched
+                 (case (:sub-type ex-data*)
+                   :download
+                   (notification-download-higher-schema-graph! repo graph-uuid (:remote ex-data*))
+                   :create-branch
+                   (notification-upload-higher-schema-graph! repo)
+                   ;; else
+                   (do (log/info :start-ex start-ex)
+                       (notification/show! [:div
+                                            [:div (:ex-message start-ex)]
+                                            [:div (-> ex-data*
+                                                      (select-keys [:app :local :remote])
+                                                      pp/pprint
+                                                      with-out-str)]]
+                                           :error)))
 
-                   :rtc.exception/lock-failed
-                   (js/setTimeout #(<rtc-start! repo) 1000)
+                 :rtc.exception/lock-failed
+                 (js/setTimeout #(<rtc-start! repo) 1000)
 
-                      ;; else
-                   nil)]
-         nil)))))
+                 ;; else
+                 nil)]
+       nil))))
 
 (defn <get-remote-graphs
   []

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

@@ -98,7 +98,7 @@ conditions:
     (let [visibility (m/?< flows/document-visibility-state-flow)]
       (try
         (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)
               :document-visible&rtc-not-running
               (m/amb)))
@@ -106,20 +106,41 @@ conditions:
         (catch Cancelled _
           (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
   (->>
-   [(m/eduction
+   [;; login-user changed
+    (m/eduction
      (keep (fn [user] (when (:email user) [:login])))
      flows/current-login-user-flow)
+    ;; repo changed
     (m/eduction
      (keep (fn [repo] (when repo [:graph-switch repo])))
      flows/current-repo-flow)
+    ;; trigger-rtc by somewhere else
     (m/eduction
      (keep (fn [repo] (when repo [:trigger-rtc repo])))
      (m/watch *rtc-start-trigger))
+    ;; document visibilitychange->true
+    (m/eduction
+     (map vector)
+     document-visible&rtc-not-running-flow)
+    ;; network online->true
     (m/eduction
      (map vector)
-     document-visible&rtc-not-running-flow)]
+     network-online&rtc-not-running-flow)]
    (apply c.m/mix)
    (m/eduction (filter (fn [_] (some? (state/get-auth-id-token)))))
    (c.m/debounce 200)))

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

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

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

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

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

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

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

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

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

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

+ 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]
    (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}
                :as opts}]
    (when (or (uuid? page-name)
@@ -85,7 +85,7 @@
                 (or (ldb/hidden? page)
                     (and (ldb/built-in? page) (ldb/private-built-in-page? page))))
          (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)
            (do
            ;; 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!
   "Open a new Electron window."
-  [repo target-repo]
-  (when-not (= repo target-repo)        ; TODO: remove this once we support multi-tabs OPFS access
-    (when target-repo
-      (if (util/electron?)
-        (ipc/ipc "openNewWindow" target-repo)
-        (js/window.open (str config/app-website "#/?graph=" target-repo) "_blank")))))
+  [target-repo]
+  (when target-repo
+    (if (util/electron?)
+      (ipc/ipc "openNewWindow" target-repo)
+      (js/window.open (str config/app-website "#/?graph=" target-repo) "_blank"))))
 
 (defn toggle-show-empty-hidden-properties!
   []

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

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

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

@@ -1,13 +1,26 @@
 (ns frontend.modules.outliner.pipeline
-  (:require [frontend.db :as db]
-            [frontend.db.react :as react]
-            [frontend.state :as state]
+  (:require [clojure.string :as string]
             [datascript.core :as d]
+            [frontend.config :as config]
+            [frontend.db :as db]
+            [frontend.db.react :as react]
+            [frontend.fs :as fs]
             [frontend.handler.ui :as ui-handler]
+            [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.fs :as fs]
-            [logseq.common.path :as path]
-            [frontend.config :as config]))
+            [logseq.common.path :as path]))
+
+(defn- update-editing-block-title-if-changed!
+  [tx-data]
+  (when-let [editing-block (state/get-edit-block)]
+    (let [editing-title (state/get-edit-content)]
+      (when-let [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
   [{:keys [_request-id repo tx-meta tx-data deleted-block-uuids deleted-assets affected-keys blocks]}]
@@ -55,6 +68,9 @@
                               tx-data))]
               (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)
               (doseq [asset deleted-assets]
                 (fs/unlink! repo (path/path-join (config/get-current-repo-assets-root) (str (:block/uuid asset) "." (:ext asset))) {})))

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

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

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

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

+ 20 - 12
src/main/frontend/state.cljs

@@ -33,6 +33,7 @@
 (defonce *profile-state (volatile! {}))
 
 (defonce *db-worker (atom nil))
+(defonce *editor-info (atom nil))
 
 (defn- <invoke-db-worker*
   [qkw direct-pass-args? args-list]
@@ -61,7 +62,8 @@
                          (when graph (ipc/ipc "setCurrentGraph" graph))
                          graph)]
     (atom
-     {:route-match                           nil
+     {:client-id                             (str (random-uuid))
+      :route-match                           nil
       :today                                 nil
       :system/events                         (async/chan 1000)
       :file/unlinked-dirs                    #{}
@@ -73,6 +75,7 @@
       :nfs/refreshing?                       nil
       :instrument/disabled?                  (storage/get "instrument-disabled")
       ;; TODO: how to detect the network reliably?
+      ;; NOTE: prefer to use flows/network-online-event-flow
       :network/online?         true
       :indexeddb/support?      true
       :me                      nil
@@ -1036,16 +1039,6 @@ Similar to re-frame subscriptions"
   []
   @(get @state :editor/block))
 
-(defn set-edit-content!
-  ([input-id value] (set-edit-content! input-id value true))
-  ([input-id value set-input-value?]
-   (when input-id
-     (when set-input-value?
-       (when-let [input (gdom/getElement input-id)]
-         (util/set-change-value input value)))
-     (set-state! :editor/content value :path-in-sub-atom
-                 (or (:block/uuid (get-edit-block)) input-id)))))
-
 (defn editing?
   []
   (seq @(:editor/editing? @state)))
@@ -1064,6 +1057,17 @@ Similar to re-frame subscriptions"
                 id))))
         (catch :default _e)))))
 
+(defn set-edit-content!
+  ([value] (set-edit-content! (get-edit-input-id) value))
+  ([input-id value] (set-edit-content! input-id value true))
+  ([input-id value set-input-value?]
+   (when input-id
+     (when set-input-value?
+       (when-let [input (gdom/getElement input-id)]
+         (util/set-change-value input value)))
+     (set-state! :editor/content value :path-in-sub-atom
+                 (or (:block/uuid (get-edit-block)) input-id)))))
+
 (defn get-input
   []
   (when-let [id (get-edit-input-id)]
@@ -1638,7 +1642,11 @@ Similar to re-frame subscriptions"
 
 (defn set-online!
   [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
   []

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

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

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

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

+ 10 - 0
src/main/frontend/worker/db/migrate.cljs

@@ -998,6 +998,16 @@
 
 (defn- build-invalid-tx [entity eid]
   (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))))
     [[:db/retractEntity (:db/id entity)]]
 

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

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

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

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

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

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

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

@@ -4,10 +4,10 @@
             [datascript.core :as d]
             [frontend.common.file.core :as common-file]
             [logseq.db :as ldb]
+            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.util :as sqlite-util]
             [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
   [block]

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

@@ -5,9 +5,7 @@
 
 (def online-event-flow
   (->> (m/watch (get @worker-state/*state :thread-atom/online-event))
-       (m/eduction
-        (drop-while nil?)
-        (filter true?))))
+       (m/eduction (filter true?))))
 
 (comment
   ((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
   "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.file :as file]
             [frontend.worker.react :as worker-react]
+            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [logseq.common.defkeywords :refer [defkeywords]]
@@ -105,8 +107,8 @@
                    true
                    (db-validate/validate-tx-report! tx-report (:validate-db-options context)))]
       (when (and (get-in context [:validate-db-options :fail-invalid?]) (not valid?))
-        (worker-util/post-message :notification
-                                  [["Invalid DB!"] :error]))))
+        (shared-service/broadcast-to-clients! :notification
+                                              [["Invalid DB!"] :error]))))
 
   ;; Ensure :block/order is unique for any block that has :block/parent
   (when (or (:dev? context) (exists? js/process))
@@ -176,7 +178,7 @@
      :logseq.property.user/email email}))
 
 (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)))
              (seq tx-data))
     (when-let [decoded-id-token (some-> (worker-state/get-id-token) worker-util/parse-jwt)]
@@ -187,24 +189,34 @@
             add-created-by-tx-data
             (keep
              (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)]
         (cond->> add-created-by-tx-data
           (nil? created-by-ent) (cons created-by-block))))))
 
 (defn- compute-extra-tx-data
   [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)
         commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta))
                       (commands/run-commands 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)))
 
 (defn- invoke-hooks-default

+ 41 - 15
src/main/frontend/worker/rtc/core.cljs

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

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

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

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

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

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

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

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

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

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

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

+ 5 - 5
src/rtc_e2e_test/client_steps.cljs

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

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

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