From 296881c2469a7801c6963a1ae4a52400924a41df Mon Sep 17 00:00:00 2001 From: Nikita Prokopov Date: Sun, 22 Oct 2023 15:39:51 +0200 Subject: [PATCH] Extracted datascript.conn --- project.clj | 2 +- src/datascript/conn.cljc | 147 ++++++++++++++++++++++++++++++ src/datascript/core.cljc | 175 +++++++----------------------------- src/datascript/js.cljs | 13 +-- src/datascript/storage.clj | 5 ++ src/datascript/storage.cljs | 5 +- 6 files changed, 194 insertions(+), 153 deletions(-) create mode 100644 src/datascript/conn.cljc diff --git a/project.clj b/project.clj index d3059aab..5c5a7377 100644 --- a/project.clj +++ b/project.clj @@ -21,7 +21,7 @@ *print-namespace-maps* false ;; *unchecked-math* :warn-on-boxed } - :jvm-opts ["-Xmx2g" "-server"] + :jvm-opts ["-Xmx2g" "-server" "-Dclojure.main.report=stderr"] :cljsbuild { :builds [ diff --git a/src/datascript/conn.cljc b/src/datascript/conn.cljc new file mode 100644 index 00000000..0af3dbf2 --- /dev/null +++ b/src/datascript/conn.cljc @@ -0,0 +1,147 @@ +(ns datascript.conn + (:require + [datascript.db :as db #?@(:cljs [:refer [FilteredDB]])] + [datascript.storage :as storage] + [me.tonsky.persistent-sorted-set :as set]) + #?(:clj + (:import + [datascript.db FilteredDB]))) + +(defn with + ([db tx-data] (with db tx-data nil)) + ([db tx-data tx-meta] + {:pre [(db/db? db)]} + (if (instance? FilteredDB db) + (throw (ex-info "Filtered DB cannot be modified" {:error :transaction/filtered})) + (db/transact-tx-data (db/->TxReport db db [] {} tx-meta) tx-data)))) + +(defn conn? [conn] + (and #?(:clj (instance? clojure.lang.IDeref conn) + :cljs (satisfies? cljs.core/IDeref conn)) + (db/db? @conn))) + +(defn conn-from-db [db] + {:pre [(db/db? db)]} + (if-some [storage (storage/storage db)] + (do + (storage/store db) + (atom db + :meta {:listeners (atom {}) + :tx-tail (atom []) + :db-last-stored (atom db)})) + (atom db + :meta {:listeners (atom {})}))) + +(defn conn-from-datoms + ([datoms] + (conn-from-db (db/init-db datoms nil {}))) + ([datoms schema] + (conn-from-db (db/init-db datoms schema {}))) + ([datoms schema opts] + (conn-from-db (db/init-db datoms schema (storage/maybe-adapt-storage opts))))) + +(defn create-conn + ([] + (conn-from-db (db/empty-db nil {}))) + ([schema] + (conn-from-db (db/empty-db schema {}))) + ([schema opts] + (conn-from-db (db/empty-db schema (storage/maybe-adapt-storage opts))))) + +#?(:clj + (defn restore-conn + ([storage] + (restore-conn storage {})) + ([storage opts] + (when-some [[db tail] (storage/restore-impl storage opts)] + (atom (storage/db-with-tail db tail) + :meta {:listeners (atom {}) + :tx-tail (atom tail) + :db-last-stored (atom db)}))))) + +(defn ^:no-doc -transact! [conn tx-data tx-meta] + {:pre [(conn? conn)]} + (let [*report (atom nil)] + (swap! conn + (fn [db] + (let [r (with db tx-data tx-meta)] + (reset! *report r) + (:db-after r)))) + #?(:clj + (when-some [storage (storage/storage @conn)] + (let [{db :db-after + datoms :tx-data} @*report + settings (set/settings (:eavt db)) + *tx-tail (:tx-tail (meta conn)) + tx-tail' (swap! *tx-tail conj datoms)] + (if (> (transduce (map count) + 0 tx-tail') (:branching-factor settings)) + ;; overflow tail + (do + (storage/store-impl! db (storage/storage-adapter db) false) + (reset! *tx-tail []) + (reset! (:db-last-stored (meta conn)) db)) + ;; just update tail + (storage/store-tail db tx-tail'))))) + @*report)) + +(defn transact! + ([conn tx-data] (transact! conn tx-data nil)) + ([conn tx-data tx-meta] + {:pre [(conn? conn)]} + (let [report (-transact! conn tx-data tx-meta)] + (doseq [[_ callback] (some-> (:listeners (meta conn)) (deref))] + (callback report)) + report))) + +(defn reset-conn! + ([conn db] + (reset-conn! conn db nil)) + ([conn db tx-meta] + {:pre [(conn? conn) + (db/db? db)]} + (let [db-before @conn + report (db/map->TxReport + {:db-before db-before + :db-after db + :tx-data (concat + (map #(assoc % :added false) (db/-datoms db-before :eavt nil nil nil nil)) + (db/-datoms db :eavt nil nil nil nil)) + :tx-meta tx-meta})] + #?(:clj + (when-some [storage (storage/storage db-before)] + (storage/store db) + (reset! (:tx-tail (meta conn)) []) + (reset! (:db-last-stored (meta conn)) db))) + (reset! conn db) + (doseq [[_ callback] (some-> (:listeners (meta conn)) (deref))] + (callback report)) + db))) + +(defn reset-schema! [conn schema] + {:pre [(conn? conn)]} + (let [db (swap! conn db/with-schema schema)] + #?(:clj + (when-some [storage (storage/storage @conn)] + (storage/store-impl! db (storage/storage-adapter db) true) + (reset! (:tx-tail (meta conn)) []) + (reset! (:db-last-stored (meta conn)) db))) + db)) + +(defn- atom? [a] + #?(:cljs (instance? Atom a) + :clj (instance? clojure.lang.IAtom a))) + +(defn listen! + ([conn callback] + (listen! conn (rand) callback)) + ([conn key callback] + {:pre [(conn? conn) + (atom? (:listeners (meta conn)))]} + (swap! (:listeners (meta conn)) assoc key callback) + key)) + +(defn unlisten! + [conn key] + {:pre [(conn? conn) + (atom? (:listeners (meta conn)))]} + (swap! (:listeners (meta conn)) dissoc key)) diff --git a/src/datascript/core.cljc b/src/datascript/core.cljc index c8ec085a..b89e1977 100644 --- a/src/datascript/core.cljc +++ b/src/datascript/core.cljc @@ -2,6 +2,7 @@ (:refer-clojure :exclude [filter]) (:require [#?(:cljs cljs.reader :clj clojure.edn) :as edn] + [datascript.conn :as conn] [datascript.db :as db #?@(:cljs [:refer [Datom DB FilteredDB]])] #?(:clj [datascript.pprint]) [datascript.pull-api :as dp] @@ -146,13 +147,6 @@ ; Creating DB -(defn- maybe-adapt-storage [opts] - #?(:clj - (if-some [storage (:storage opts)] - (update opts :storage storage/make-storage-adapter opts) - opts) - :cljs opts)) - (defn ^DB empty-db "Creates an empty database with an optional schema. @@ -178,7 +172,7 @@ ([schema] (db/empty-db schema {})) ([schema opts] - (db/empty-db schema (maybe-adapt-storage opts)))) + (db/empty-db schema (storage/maybe-adapt-storage opts)))) (def ^{:arglists '([x]) :doc "Returns `true` if the given value is an immutable database, `false` otherwise."} @@ -207,7 +201,7 @@ ([datoms schema] (db/init-db datoms schema {})) ([datoms schema opts] - (db/init-db datoms schema (maybe-adapt-storage opts)))) + (db/init-db datoms schema (storage/maybe-adapt-storage opts)))) (def ^{:arglists '([db] [db opts]) :doc "Converts db into a data structure (not string!) that can be fed to serializer @@ -270,14 +264,9 @@ ; Changing DB -(defn with +(def ^{:arglists '([db tx-data] [db tx-data tx-meta])} with "Same as [[transact!]], but applies to an immutable database value. Returns transaction report (see [[transact!]])." - ([db tx-data] (with db tx-data nil)) - ([db tx-data tx-meta] - {:pre [(db/db? db)]} - (if (is-filtered db) - (throw (ex-info "Filtered DB cannot be modified" {:error :transaction/filtered})) - (db/transact-tx-data (db/->TxReport db db [] {} tx-meta) tx-data)))) + conn/with) (defn ^DB db-with "Applies transaction to an immutable db value, returning new immutable db value. Same as `(:db-after (with db tx-data))`." @@ -450,40 +439,21 @@ {:pre [(db/db? db)]} (db/-index-range db attr start end)) - ;; Conn -(defn conn? +(def ^{:arglists '([conn])} conn? "Returns `true` if this is a connection to a DataScript db, `false` otherwise." - [conn] - (and #?(:clj (instance? clojure.lang.IDeref conn) - :cljs (satisfies? cljs.core/IDeref conn)) - (db/db? @conn))) + conn/conn?) -(defn conn-from-db +(def ^{:arglists '([db])} conn-from-db "Creates a mutable reference to a given immutable database. See [[create-conn]]." - [db] - {:pre [(db/db? db)]} - (if-some [storage (storage/storage db)] - (do - (storage/store db) - (atom db - :meta {:listeners (atom {}) - :tx-tail (atom []) - :db-last-stored (atom db)})) - (atom db - :meta {:listeners (atom {})}))) - -(defn conn-from-datoms + conn/conn-from-db) + +(def ^{:arglists '([datoms] [datoms schema] [datoms schema opts])} conn-from-datoms "Creates an empty DB and a mutable reference to it. See [[create-conn]]." - ([datoms] - (conn-from-db (init-db datoms))) - ([datoms schema] - (conn-from-db (init-db datoms schema))) - ([datoms schema opts] - (conn-from-db (init-db datoms schema opts)))) + conn/conn-from-datoms) -(defn create-conn +(def ^{:arglists '([] [schema] [schema opts])} create-conn "Creates a mutable reference (a “connection”) to an empty immutable database. Connections are lightweight in-memory structures (~atoms) with direct support of transaction listeners ([[listen!]], [[unlisten!]]) and other handy DataScript APIs ([[transact!]], [[reset-conn!]], [[db]]). @@ -493,52 +463,15 @@ For list of options, see [[empty-db]]. If you specify `:storage` option, conn will be stored automatically after each transaction" - ([] - (conn-from-db (empty-db))) - ([schema] - (conn-from-db (empty-db schema))) - ([schema opts] - (conn-from-db (empty-db schema opts)))) + conn/create-conn) #?(:clj - (defn restore-conn + (def ^{:arglists '([storage] [storage opts])} restore-conn "Lazy-load database from storage and make conn out of it. Returns nil if there’s no database yet in storage" - ([storage] - (restore-conn storage {})) - ([storage opts] - (when-some [[db tail] (storage/restore-impl storage opts)] - (atom (storage/db-with-tail db tail) - :meta {:listeners (atom {}) - :tx-tail (atom tail) - :db-last-stored (atom db)}))))) - -(defn ^:no-doc -transact! [conn tx-data tx-meta] - {:pre [(conn? conn)]} - (let [*report (atom nil)] - (swap! conn - (fn [db] - (let [r (with db tx-data tx-meta)] - (reset! *report r) - (:db-after r)))) - #?(:clj - (when-some [storage (storage/storage @conn)] - (let [{db :db-after - datoms :tx-data} @*report - settings (set/settings (:eavt db)) - *tx-tail (:tx-tail (meta conn)) - tx-tail' (swap! *tx-tail conj datoms)] - (if (> (transduce (map count) + 0 tx-tail') (:branching-factor settings)) - ;; overflow tail - (do - (storage/store-impl! db (storage/storage-adapter db) false) - (reset! *tx-tail []) - (reset! (:db-last-stored (meta conn)) db)) - ;; just update tail - (storage/store-tail db tx-tail'))))) - @*report)) - -(defn transact! + conn/restore-conn)) + +(def ^{:arglists '([conn tx-data] [conn tx-data tx-meta])} transact! "Applies transaction the underlying database value and atomically updates connection reference to point to the result of that transaction, new db value. Returns transaction report, a map: @@ -623,75 +556,27 @@ ; equivalent to (transact! conn [[:db/add -1 :name \"Oleg\"] [:db/add 296 :friend -1]])" - ([conn tx-data] (transact! conn tx-data nil)) - ([conn tx-data tx-meta] - {:pre [(conn? conn)]} - (let [report (-transact! conn tx-data tx-meta)] - (doseq [[_ callback] (some-> (:listeners (meta conn)) (deref))] - (callback report)) - report))) + conn/transact!) -(defn reset-conn! +(def ^{:arglists '([conn db] [conn db tx-meta])} reset-conn! "Forces underlying `conn` value to become `db`. Will generate a tx-report that will remove everything from old value and insert everything from the new one." - ([conn db] - (reset-conn! conn db nil)) - ([conn db tx-meta] - {:pre [(conn? conn) - (db/db? db)]} - (let [db-before @conn - report (db/map->TxReport - {:db-before db-before - :db-after db - :tx-data (concat - (map #(assoc % :added false) (datoms db-before :eavt)) - (datoms db :eavt)) - :tx-meta tx-meta})] - #?(:clj - (when-some [storage (storage/storage db-before)] - (storage/store db) - (reset! (:tx-tail (meta conn)) []) - (reset! (:db-last-stored (meta conn)) db))) - (reset! conn db) - (doseq [[_ callback] (some-> (:listeners (meta conn)) (deref))] - (callback report)) - db))) - -(defn reset-schema! [conn schema] - "Warning! Does not perform any validation or data conversion. Only change schema in a compatible way" - {:pre [(conn? conn)]} - (let [db (swap! conn db/with-schema schema)] - #?(:clj - (when-some [storage (storage/storage @conn)] - (storage/store-impl! db (storage/storage-adapter db) true) - (reset! (:tx-tail (meta conn)) []) - (reset! (:db-last-stored (meta conn)) db))) - db)) - -(defn- atom? [a] - #?(:cljs (instance? Atom a) - :clj (instance? clojure.lang.IAtom a))) - -(defn listen! + conn/reset-conn!) + +(def ^{:arglists '([conn schema])} reset-schema! + conn/reset-schema!) + +(def ^{:arglists '([conn callback] [conn key callback])} listen! "Listen for changes on the given connection. Whenever a transaction is applied to the database via [[transact!]], the callback is called with the transaction report. `key` is any opaque unique value. Idempotent. Calling [[listen!]] with the same key twice will override old callback with the new value. Returns the key under which this listener is registered. See also [[unlisten!]]." - ([conn callback] - (listen! conn (rand) callback)) - ([conn key callback] - {:pre [(conn? conn) - (atom? (:listeners (meta conn)))]} - (swap! (:listeners (meta conn)) assoc key callback) - key)) - -(defn unlisten! + conn/listen!) + +(def ^{:arglists '([conn key])} unlisten! "Removes registered listener from connection. See also [[listen!]]." - [conn key] - {:pre [(conn? conn) - (atom? (:listeners (meta conn)))]} - (swap! (:listeners (meta conn)) dissoc key)) + conn/unlisten!) ; Data Readers diff --git a/src/datascript/js.cljs b/src/datascript/js.cljs index 87731849..b5176d02 100644 --- a/src/datascript/js.cljs +++ b/src/datascript/js.cljs @@ -1,11 +1,12 @@ (ns ^:no-doc datascript.js (:refer-clojure :exclude [filter]) (:require - [cljs.reader] - [goog.object :as go] - [clojure.walk :as walk] - [datascript.core :as d] - [datascript.serialize :as serialize])) + [cljs.reader] + [goog.object :as go] + [clojure.walk :as walk] + [datascript.conn :as conn] + [datascript.core :as d] + [datascript.serialize :as serialize])) ;; Conversions @@ -124,7 +125,7 @@ (defn ^:export transact [conn entities & [tx-meta]] (let [entities (entities->clj entities) - report (-> (d/-transact! conn entities tx-meta) + report (-> (conn/-transact! conn entities tx-meta) tx-report->js)] (doseq [[_ callback] @(:listeners (meta conn))] (callback report)) diff --git a/src/datascript/storage.clj b/src/datascript/storage.clj index 41eed13a..5e7ece7f 100644 --- a/src/datascript/storage.clj +++ b/src/datascript/storage.clj @@ -72,6 +72,11 @@ (let [settings (@#'set/map->settings opts)] (StorageAdapter. storage settings))) +(defn maybe-adapt-storage [opts] + (if-some [storage (:storage opts)] + (update opts :storage make-storage-adapter opts) + opts)) + (defn storage-adapter ^StorageAdapter [db] (when db (.-_storage ^PersistentSortedSet (:eavt db)))) diff --git a/src/datascript/storage.cljs b/src/datascript/storage.cljs index aa4a985d..c55a24a0 100644 --- a/src/datascript/storage.cljs +++ b/src/datascript/storage.cljs @@ -7,4 +7,7 @@ ([db] (throw (ex-info "Not implemented: (storage/store db)" {}))) ([db storage] - (throw (ex-info "Not implemented: (storage/store db storage)" {})))) \ No newline at end of file + (throw (ex-info "Not implemented: (storage/store db storage)" {})))) + +(defn maybe-adapt-storage [opts] + opts)