From d5e0833c64a31d0807c2cdee2ee2634ae2ba2a6b Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 9 Apr 2026 03:28:05 +0800 Subject: [PATCH] feat(dev): export client-ops sqlite from db worker --- .../frontend/handler/common/developer.cljs | 48 +++++++++++++++ .../frontend/modules/shortcut/config.cljs | 5 ++ src/main/frontend/worker/db_worker.cljs | 60 ++++++++++++++++++- src/resources/dicts/en.edn | 1 + src/test/frontend/worker/db_worker_test.cljs | 28 +++++++++ 5 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/handler/common/developer.cljs b/src/main/frontend/handler/common/developer.cljs index 03469a6fa6..5bb29a8c50 100644 --- a/src/main/frontend/handler/common/developer.cljs +++ b/src/main/frontend/handler/common/developer.cljs @@ -95,6 +95,31 @@ (string/replace #"[\\/]+" "_") (str "_checksum_" (quot (util/time-ms) 1000)))) +(defn- client-ops-export-file-name + [repo] + (-> (or repo "graph") + (string/replace #"^/+" "") + (string/replace #"[\\/]+" "_") + (str "_client_ops_" (quot (util/time-ms) 1000)))) + +(defn- ->uint8array + [data] + (cond + (instance? js/Uint8Array data) + data + + (js/ArrayBuffer.isView data) + (js/Uint8Array. (.-buffer data) (.-byteOffset data) (.-byteLength data)) + + (instance? js/ArrayBuffer data) + (js/Uint8Array. data) + + (array? data) + (js/Uint8Array. data) + + :else + nil)) + (defn- (state/uint8array data)] + (let [filename (client-ops-export-file-name repo) + blob (js/Blob. #js [payload] (clj->js {:type "application/octet-stream"}))] + (utils/saveToFile blob filename "sqlite") + (notification/show! + (str "Client ops SQLite exported: " filename ".sqlite") + :success + false)) + (notification/show! + (str "Client ops SQLite export failed: invalid payload type " + (pr-str (type data)) + ".") + :warning)))) + (p/catch (fn [error] + (js/console.error "export-client-ops-sqlite failed:" error) + (notification/show! "Failed to export client ops SQLite." :error)))) + (notification/show! "No graph found" :warning))) + (defn import-chosen-graph [repo] (p/let [_ (persist-db/uint8array + [data] + (cond + (instance? js/Uint8Array data) + data + + (js/ArrayBuffer.isView data) + (js/Uint8Array. (.-buffer data) (.-byteOffset data) (.-byteLength data)) + + (instance? js/ArrayBuffer data) + (js/Uint8Array. data) + + (array? data) + (js/Uint8Array. data) + + :else + data)) + +(defn- export-db-file-with-paths + [repo path-candidates] + (p/let [^js pool (> path-candidates + (filter string?) + (remove string/blank?) + distinct + vec)] + (when-let [path (first paths)] + (let [result (try + (.exportFile ^js pool path) + (catch :default _e + nil)) + payload (->uint8array result)] + (if (instance? js/Uint8Array payload) + payload + (recur (subvec paths 1))))))))) (defn- uint8array data)] + (Comlink/transfer payload #js [(.-buffer payload)]))) + +(def-thread-api :thread-api/export-client-ops-db + [repo] + (when-let [^js db (worker-state/get-sqlite-conn repo :client-ops)] + (.exec db "PRAGMA wal_checkpoint(2)")) + (let [^js client-ops-db (worker-state/get-sqlite-conn repo :client-ops) + db-filename (some-> client-ops-db (gobj/get "filename")) + export-paths [db-filename + client-ops-repo-path + (str "/" client-ops-repo-path) + (str "client-ops" repo-path) + (str "/client-ops" repo-path)]] + (p/let [payload (export-db-file-with-paths repo export-paths)] + (when (instance? js/Uint8Array payload) + (Comlink/transfer payload #js [(.-buffer payload)]))))) (def-thread-api :thread-api/import-db [repo data] diff --git a/src/resources/dicts/en.edn b/src/resources/dicts/en.edn index 15d35619d8..e18489de15 100644 --- a/src/resources/dicts/en.edn +++ b/src/resources/dicts/en.edn @@ -581,6 +581,7 @@ :dev/replace-graph-with-db-file "(Dev) Replace graph with its db.sqlite file" :dev/validate-db "(Dev) Validate current graph" :dev/recompute-checksum "(Dev) Recompute graph checksum" + :dev/export-client-ops-sqlite "(Dev) Export client ops sqlite" :dev/gc-graph "(Dev) Garbage collect graph (remove unused data in SQLite)" :dev/rtc-stop "(Dev) RTC Stop" :dev/rtc-start "(Dev) RTC Start" diff --git a/src/test/frontend/worker/db_worker_test.cljs b/src/test/frontend/worker/db_worker_test.cljs index 3ad5afe8bb..5d3ea08c83 100644 --- a/src/test/frontend/worker/db_worker_test.cljs +++ b/src/test/frontend/worker/db_worker_test.cljs @@ -352,3 +352,31 @@ (finally (reset! db-sync/*repo->latest-remote-tx latest-tx-prev) (reset! db-sync/*repo->latest-remote-checksum latest-checksum-prev))))))) + +(deftest thread-api-export-client-ops-db-checkpoints-and-exports-client-ops-file-test + (async done + (restoring-worker-state + (fn [] + (let [export-client-ops-db (@thread-api/*thread-apis :thread-api/export-client-ops-db) + sql-calls (atom []) + export-calls (atom []) + expected-data (js/Uint8Array. #js [1 2 3]) + expected-buffer (.-buffer expected-data) + fake-pool #js {:exportFile (fn [path] + (swap! export-calls conj path) + expected-buffer)}] + (reset! worker-state/*opfs-pools {test-repo fake-pool}) + (with-redefs [worker-state/get-sqlite-conn (fn [_repo which-db] + (when (= :client-ops which-db) + #js {:exec (fn [sql] + (swap! sql-calls conj sql))}))] + (-> (export-client-ops-db test-repo) + (p/then (fn [result] + (is (= ["PRAGMA wal_checkpoint(2)"] @sql-calls)) + (is (= ["client-ops-/db.sqlite"] @export-calls)) + (is (instance? js/Uint8Array result)) + (is (= [1 2 3] (vec result))) + (done))) + (p/catch (fn [error] + (is false (str error)) + (done))))))))))