feat: add Connection API and make SyncedAtom a promise
Add `connect` to manage store + shared config, and `synced-atom` now
accepts a Connection or raw store (backwards compatible).
SyncedAtom implements IPromiseFactory so it works directly with p/let:
(def conn (pb/connect {:db "my-app" :server "..."}))
(def !todos (pb/synced-atom conn "todo"))
(p/let [todos !todos] (swap! todos assoc "k" v))
- Connection record holds store-promise + default opts (server, interval)
- `connect` accepts :store (CLJ/CLJS) or :db (CLJS, opens IndexedDB)
- `ready?` now resolves to the SyncedAtom itself instead of `true`
- Add `close!` to close a Connection's store
- Store field is now an atom (set when store-promise resolves)
- Simplify TodoMVC example: no more !conn atom or double-deref
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
(ns atomsync.todomvc
|
(ns atomsync.todomvc
|
||||||
"TodoMVC built on Atomsync — offline-first, synced, Clojure-native."
|
"TodoMVC built on Atomsync — offline-first, synced, Clojure-native."
|
||||||
(:require [atomsync.core :as pb]
|
(:require [atomsync.core :as pb]
|
||||||
[atomsync.store.idb :as idb]
|
|
||||||
[atomsync.hiccup :refer [html]]
|
[atomsync.hiccup :refer [html]]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
@@ -10,8 +9,10 @@
|
|||||||
;; State
|
;; State
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(defonce !conn (atom nil))
|
(defonce conn (pb/connect {:db "atomsync-todomvc"
|
||||||
(defonce !todos (atom nil)) ;; the SyncedAtom
|
:server "http://localhost:8090/sync"
|
||||||
|
:interval 15000}))
|
||||||
|
(defonce !todos (pb/synced-atom conn "todo"))
|
||||||
(defonce !filter (atom :all)) ;; :all | :active | :completed
|
(defonce !filter (atom :all)) ;; :all | :active | :completed
|
||||||
(defonce !editing (atom nil)) ;; id of todo being edited, or nil
|
(defonce !editing (atom nil)) ;; id of todo being edited, or nil
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
(defn- todos-list
|
(defn- todos-list
|
||||||
"Return todos as sorted vec of [id doc] pairs."
|
"Return todos as sorted vec of [id doc] pairs."
|
||||||
[]
|
[]
|
||||||
(->> @@!todos
|
(->> @!todos
|
||||||
(sort-by (fn [[_ doc]] (:created doc)))
|
(sort-by (fn [[_ doc]] (:created doc)))
|
||||||
vec))
|
vec))
|
||||||
|
|
||||||
@@ -50,33 +51,33 @@
|
|||||||
(let [text (str/trim text)]
|
(let [text (str/trim text)]
|
||||||
(when (seq text)
|
(when (seq text)
|
||||||
(let [id (str "todo:" (random-uuid))]
|
(let [id (str "todo:" (random-uuid))]
|
||||||
(swap! @!todos assoc id
|
(swap! !todos assoc id
|
||||||
{:text text
|
{:text text
|
||||||
:completed false
|
:completed false
|
||||||
:created (.now js/Date)})))))
|
:created (.now js/Date)})))))
|
||||||
|
|
||||||
(defn- toggle-todo! [id]
|
(defn- toggle-todo! [id]
|
||||||
(swap! @!todos update-in [id :completed] not))
|
(swap! !todos update-in [id :completed] not))
|
||||||
|
|
||||||
(defn- toggle-all! []
|
(defn- toggle-all! []
|
||||||
(let [target (not (all-completed?))]
|
(let [target (not (all-completed?))]
|
||||||
(swap! @!todos
|
(swap! !todos
|
||||||
(fn [m]
|
(fn [m]
|
||||||
(reduce-kv (fn [acc k v] (assoc acc k (assoc v :completed target)))
|
(reduce-kv (fn [acc k v] (assoc acc k (assoc v :completed target)))
|
||||||
{} m)))))
|
{} m)))))
|
||||||
|
|
||||||
(defn- destroy-todo! [id]
|
(defn- destroy-todo! [id]
|
||||||
(swap! @!todos dissoc id))
|
(swap! !todos dissoc id))
|
||||||
|
|
||||||
(defn- edit-todo! [id new-text]
|
(defn- edit-todo! [id new-text]
|
||||||
(let [text (str/trim new-text)]
|
(let [text (str/trim new-text)]
|
||||||
(if (seq text)
|
(if (seq text)
|
||||||
(swap! @!todos assoc-in [id :text] text)
|
(swap! !todos assoc-in [id :text] text)
|
||||||
(destroy-todo! id))
|
(destroy-todo! id))
|
||||||
(reset! !editing nil)))
|
(reset! !editing nil)))
|
||||||
|
|
||||||
(defn- clear-completed! []
|
(defn- clear-completed! []
|
||||||
(swap! @!todos
|
(swap! !todos
|
||||||
(fn [m]
|
(fn [m]
|
||||||
(into {} (remove (fn [[_ v]] (:completed v))) m))))
|
(into {} (remove (fn [[_ v]] (:completed v))) m))))
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@
|
|||||||
"Clear completed"])]))
|
"Clear completed"])]))
|
||||||
|
|
||||||
(defn- render-sync-status []
|
(defn- render-sync-status []
|
||||||
(let [pending (when @!todos (pb/pending-count @!todos))
|
(let [pending (pb/pending-count !todos)
|
||||||
online? (.-onLine js/navigator)]
|
online? (.-onLine js/navigator)]
|
||||||
[:div.sync-bar
|
[:div.sync-bar
|
||||||
[:span {:class (str "sync-dot " (if online? "online" "offline"))}]
|
[:span {:class (str "sync-dot " (if online? "online" "offline"))}]
|
||||||
@@ -232,22 +233,16 @@
|
|||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(defn ^:export init []
|
(defn ^:export init []
|
||||||
(p/let [store (idb/open "atomsync-todomvc")]
|
;; Render + bind immediately (empty or stale is fine)
|
||||||
(let [todos (pb/synced-atom store "todo"
|
(render!)
|
||||||
{:server "http://localhost:8090/sync"
|
(bind-events!)
|
||||||
:interval 15000})]
|
;; Re-render on any data change (fires when IDB loads + server syncs)
|
||||||
(reset! !conn store)
|
(add-watch !todos :render (fn [_ _ _ _] (render!)))
|
||||||
(reset! !todos todos)
|
(add-watch !filter :render (fn [_ _ _ _] (render!)))
|
||||||
;; Render + bind immediately (empty or stale is fine)
|
(.addEventListener js/window "online" (fn [_] (render!)))
|
||||||
(render!)
|
(.addEventListener js/window "offline" (fn [_] (render!)))
|
||||||
(bind-events!)
|
;; Wait for IDB — watch triggers render automatically
|
||||||
;; Re-render on any data change (fires when IDB loads + server syncs)
|
(p/let [todos !todos]
|
||||||
(add-watch todos :render (fn [_ _ _ _] (render!)))
|
(js/console.log "🔶 Atomsync TodoMVC loaded —" (count @todos) "todos")))
|
||||||
(add-watch !filter :render (fn [_ _ _ _] (render!)))
|
|
||||||
(.addEventListener js/window "online" (fn [_] (render!)))
|
|
||||||
(.addEventListener js/window "offline" (fn [_] (render!)))
|
|
||||||
;; Wait for IDB — watch triggers render automatically
|
|
||||||
(p/let [_ (pb/ready? todos)]
|
|
||||||
(js/console.log "🔶 Atomsync TodoMVC loaded —" (count @todos) "todos")))))
|
|
||||||
|
|
||||||
(init)
|
(init)
|
||||||
|
|||||||
@@ -2,19 +2,42 @@
|
|||||||
"Atomsync: a Clojure-native synced atom.
|
"Atomsync: a Clojure-native synced atom.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
(def store @(idb/open \"my-app\")) ;; or (memory/create)
|
(def conn (connect {:db \"my-app\"
|
||||||
(def todos (pb/synced-atom store \"todo\"
|
:server \"http://localhost:8090/sync\"}))
|
||||||
{:server \"http://localhost:8090/sync\"}))
|
(def todos (synced-atom conn \"todo\"))
|
||||||
(.then (ready? todos)
|
(p/let [todos todos]
|
||||||
(fn [_] (swap! todos assoc \"todo:1\" {:text \"Buy milk\"})))
|
(swap! todos assoc \"todo:1\" {:text \"Buy milk\"}))
|
||||||
@todos ;=> {\"todo:1\" {:text \"Buy milk\"}}
|
@todos ;=> {\"todo:1\" {:text \"Buy milk\"}}
|
||||||
"
|
"
|
||||||
(:require [atomsync.store :as store]
|
(:require [atomsync.store :as store]
|
||||||
[atomsync.sync :as sync]
|
[atomsync.sync :as sync]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[promesa.core :as p])
|
[promesa.core :as p]
|
||||||
|
[promesa.protocols :as pt]
|
||||||
|
#?(:cljs [atomsync.store.idb :as idb]))
|
||||||
#?(:clj (:import [java.util.concurrent Executors ScheduledExecutorService TimeUnit])))
|
#?(:clj (:import [java.util.concurrent Executors ScheduledExecutorService TimeUnit])))
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Connection
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defrecord Connection [store-promise opts])
|
||||||
|
|
||||||
|
(defn connect
|
||||||
|
"Create a connection for synced atoms.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
:db — database name (CLJS: opens IndexedDB)
|
||||||
|
:store — pre-opened store or promise of a store
|
||||||
|
:server — server URL for sync
|
||||||
|
:interval — sync interval in ms (default 30000)"
|
||||||
|
[{:keys [db store server interval] :or {interval 30000}}]
|
||||||
|
(let [store-pr (cond
|
||||||
|
(some? store) (p/promise store)
|
||||||
|
#?@(:cljs [(some? db) (idb/open db)])
|
||||||
|
:else (throw (ex-info "connect requires :store or :db" {})))]
|
||||||
|
(->Connection store-pr {:server server :interval interval})))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; Internal helpers
|
;; Internal helpers
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
@@ -64,7 +87,7 @@
|
|||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(deftype SyncedAtom [group ;; string prefix, e.g. "todo"
|
(deftype SyncedAtom [group ;; string prefix, e.g. "todo"
|
||||||
store ;; PStore implementation
|
store ;; atom containing PStore implementation
|
||||||
cache ;; atom containing {id -> value}
|
cache ;; atom containing {id -> value}
|
||||||
versions ;; atom containing {id -> version}
|
versions ;; atom containing {id -> version}
|
||||||
pending ;; atom containing #{id} — unsynced ids
|
pending ;; atom containing #{id} — unsynced ids
|
||||||
@@ -92,7 +115,7 @@
|
|||||||
|
|
||||||
clojure.lang.IAtom
|
clojure.lang.IAtom
|
||||||
(reset [this newval]
|
(reset [this newval]
|
||||||
(do-reset!* store cache versions pending this newval))
|
(do-reset!* @store cache versions pending this newval))
|
||||||
(swap [this f]
|
(swap [this f]
|
||||||
(.reset this (f @cache)))
|
(.reset this (f @cache)))
|
||||||
(swap [this f arg]
|
(swap [this f arg]
|
||||||
@@ -104,7 +127,10 @@
|
|||||||
(compareAndSet [this old new]
|
(compareAndSet [this old new]
|
||||||
(if (= @cache old)
|
(if (= @cache old)
|
||||||
(do (.reset this new) true)
|
(do (.reset this new) true)
|
||||||
false))]
|
false))
|
||||||
|
|
||||||
|
pt/IPromiseFactory
|
||||||
|
(-promise [_] ready-pr)]
|
||||||
|
|
||||||
:cljs
|
:cljs
|
||||||
[IAtom
|
[IAtom
|
||||||
@@ -114,7 +140,7 @@
|
|||||||
|
|
||||||
IReset
|
IReset
|
||||||
(-reset! [this new-val]
|
(-reset! [this new-val]
|
||||||
(do-reset!* store cache versions pending this new-val))
|
(do-reset!* @store cache versions pending this new-val))
|
||||||
|
|
||||||
ISwap
|
ISwap
|
||||||
(-swap! [o f]
|
(-swap! [o f]
|
||||||
@@ -140,6 +166,9 @@
|
|||||||
IWithMeta
|
IWithMeta
|
||||||
(-with-meta [_ m] (reset! _meta m))
|
(-with-meta [_ m] (reset! _meta m))
|
||||||
|
|
||||||
|
pt/IPromiseFactory
|
||||||
|
(-promise [_] ready-pr)
|
||||||
|
|
||||||
IPrintWithWriter
|
IPrintWithWriter
|
||||||
(-pr-writer [_ writer _opts]
|
(-pr-writer [_ writer _opts]
|
||||||
(-write writer (str "#<SyncedAtom[" group "] " (count @cache) " docs>")))]))
|
(-write writer (str "#<SyncedAtom[" group "] " (count @cache) " docs>")))]))
|
||||||
@@ -153,7 +182,7 @@
|
|||||||
Returns a promise that resolves when done."
|
Returns a promise that resolves when done."
|
||||||
[sa]
|
[sa]
|
||||||
(p/let [prefix (prefix-str (.-group sa))
|
(p/let [prefix (prefix-str (.-group sa))
|
||||||
docs (store/docs-by-prefix (.-store sa) prefix)
|
docs (store/docs-by-prefix @(.-store sa) prefix)
|
||||||
state (into {}
|
state (into {}
|
||||||
(comp
|
(comp
|
||||||
(remove :deleted)
|
(remove :deleted)
|
||||||
@@ -164,7 +193,7 @@
|
|||||||
docs)
|
docs)
|
||||||
_ (do (reset! (.-cache sa) state)
|
_ (do (reset! (.-cache sa) state)
|
||||||
(reset! (.-versions sa) vers))
|
(reset! (.-versions sa) vers))
|
||||||
ls (store/get-meta (.-store sa)
|
ls (store/get-meta @(.-store sa)
|
||||||
(str "last-sync:" (.-group sa)))]
|
(str "last-sync:" (.-group sa)))]
|
||||||
(reset! (.-last_sync sa) (or ls 0))
|
(reset! (.-last_sync sa) (or ls 0))
|
||||||
true))
|
true))
|
||||||
@@ -176,7 +205,7 @@
|
|||||||
(swap! (.-versions sa) assoc id version)
|
(swap! (.-versions sa) assoc id version)
|
||||||
(swap! (.-pending sa) disj id)
|
(swap! (.-pending sa) disj id)
|
||||||
(let [value (get @(.-cache sa) id)]
|
(let [value (get @(.-cache sa) id)]
|
||||||
(store/put-doc! (.-store sa)
|
(store/put-doc! @(.-store sa)
|
||||||
{:id id
|
{:id id
|
||||||
:value value
|
:value value
|
||||||
:version version
|
:version version
|
||||||
@@ -206,17 +235,17 @@
|
|||||||
(do
|
(do
|
||||||
(swap! (.-cache sa) dissoc id)
|
(swap! (.-cache sa) dissoc id)
|
||||||
(swap! (.-versions sa) assoc id (:version doc))
|
(swap! (.-versions sa) assoc id (:version doc))
|
||||||
(store/put-doc! (.-store sa)
|
(store/put-doc! @(.-store sa)
|
||||||
{:id id :value nil :version (:version doc)
|
{:id id :value nil :version (:version doc)
|
||||||
:updated (:updated doc) :deleted true :synced true}))
|
:updated (:updated doc) :deleted true :synced true}))
|
||||||
(do
|
(do
|
||||||
(swap! (.-cache sa) assoc id (:value doc))
|
(swap! (.-cache sa) assoc id (:value doc))
|
||||||
(swap! (.-versions sa) assoc id (:version doc))
|
(swap! (.-versions sa) assoc id (:version doc))
|
||||||
(store/put-doc! (.-store sa)
|
(store/put-doc! @(.-store sa)
|
||||||
{:id id :value (:value doc) :version (:version doc)
|
{:id id :value (:value doc) :version (:version doc)
|
||||||
:updated (:updated doc) :deleted false :synced true})))))]
|
:updated (:updated doc) :deleted false :synced true})))))]
|
||||||
(reset! (.-last_sync sa) max-ts)
|
(reset! (.-last_sync sa) max-ts)
|
||||||
(store/set-meta! (.-store sa)
|
(store/set-meta! @(.-store sa)
|
||||||
(str "last-sync:" (.-group sa)) max-ts)))))
|
(str "last-sync:" (.-group sa)) max-ts)))))
|
||||||
(p/resolved nil)))
|
(p/resolved nil)))
|
||||||
|
|
||||||
@@ -248,7 +277,7 @@
|
|||||||
(swap! (.-cache sa) assoc (:id r) (:value r)))
|
(swap! (.-cache sa) assoc (:id r) (:value r)))
|
||||||
(swap! (.-versions sa) assoc (:id r) (:current-version r))
|
(swap! (.-versions sa) assoc (:id r) (:current-version r))
|
||||||
(swap! (.-pending sa) disj (:id r))
|
(swap! (.-pending sa) disj (:id r))
|
||||||
(store/put-doc! (.-store sa)
|
(store/put-doc! @(.-store sa)
|
||||||
{:id (:id r)
|
{:id (:id r)
|
||||||
:value (or (:value r) (get @(.-cache sa) (:id r)))
|
:value (or (:value r) (get @(.-cache sa) (:id r)))
|
||||||
:version (:current-version r)
|
:version (:current-version r)
|
||||||
@@ -356,13 +385,22 @@
|
|||||||
(defn synced-atom
|
(defn synced-atom
|
||||||
"Create a synced atom for a document group.
|
"Create a synced atom for a document group.
|
||||||
|
|
||||||
Options:
|
Accepts a Connection (from `connect`) or a raw store for backwards compat.
|
||||||
|
|
||||||
|
Options (per-atom overrides, merged with connection defaults):
|
||||||
:server — server URL (e.g. \"http://localhost:8090/sync\")
|
:server — server URL (e.g. \"http://localhost:8090/sync\")
|
||||||
:cache — custom atom to use (e.g. reagent/atom). Default: cljs.core/atom
|
:cache — custom atom to use (e.g. reagent/atom). Default: cljs.core/atom
|
||||||
:interval — sync interval in ms (default 30000)"
|
:interval — sync interval in ms (default 30000)
|
||||||
[store group & [{:keys [server cache interval]
|
|
||||||
:or {interval 30000}}]]
|
Returns a SyncedAtom that implements IPromiseFactory, so it works with p/let:
|
||||||
(let [cache-atom (or cache (atom {}))
|
(p/let [sa (synced-atom conn \"todo\")] ...)"
|
||||||
|
[conn-or-store group & [opts]]
|
||||||
|
(let [[store-pr conn-opts] (if (instance? Connection conn-or-store)
|
||||||
|
[(:store-promise conn-or-store) (:opts conn-or-store)]
|
||||||
|
[(p/resolved conn-or-store) {}])
|
||||||
|
{:keys [server cache interval] :or {interval 30000}} (merge conn-opts opts)
|
||||||
|
store-atom (atom nil)
|
||||||
|
cache-atom (or cache (atom {}))
|
||||||
versions (atom {})
|
versions (atom {})
|
||||||
pending (atom #{})
|
pending (atom #{})
|
||||||
server-opts (when server {:server server})
|
server-opts (when server {:server server})
|
||||||
@@ -374,12 +412,14 @@
|
|||||||
syncing? (atom false)
|
syncing? (atom false)
|
||||||
cleanup-fn (atom nil)
|
cleanup-fn (atom nil)
|
||||||
meta-atom (atom nil)
|
meta-atom (atom nil)
|
||||||
sa (SyncedAtom. group store cache-atom versions pending
|
sa (SyncedAtom. group store-atom cache-atom versions pending
|
||||||
server-opts last-sync ready-pr sync-timer push-timer
|
server-opts last-sync ready-pr sync-timer push-timer
|
||||||
pushing? syncing? cleanup-fn interval meta-atom)]
|
pushing? syncing? cleanup-fn interval meta-atom)]
|
||||||
(-> (load-from-store! sa)
|
(-> (p/let [store store-pr]
|
||||||
|
(reset! store-atom store)
|
||||||
|
(load-from-store! sa))
|
||||||
(p/then (fn [_]
|
(p/then (fn [_]
|
||||||
(p/resolve! ready-pr true)
|
(p/resolve! ready-pr sa)
|
||||||
(when server-opts
|
(when server-opts
|
||||||
(-> (do-sync! sa)
|
(-> (do-sync! sa)
|
||||||
(p/then (fn [_] (start-sync-loop! sa)))))))
|
(p/then (fn [_] (start-sync-loop! sa)))))))
|
||||||
@@ -388,7 +428,8 @@
|
|||||||
sa))
|
sa))
|
||||||
|
|
||||||
(defn ready?
|
(defn ready?
|
||||||
"Returns a promise that yields true when the atom has finished loading from the store."
|
"Returns a promise that resolves to the SyncedAtom when initial load is complete.
|
||||||
|
The SyncedAtom itself implements IPromiseFactory, so (p/let [sa atom] ...) also works."
|
||||||
[sa]
|
[sa]
|
||||||
(.-ready_pr sa))
|
(.-ready_pr sa))
|
||||||
|
|
||||||
@@ -411,3 +452,9 @@
|
|||||||
:cljs (js/clearTimeout t)))
|
:cljs (js/clearTimeout t)))
|
||||||
;; Run all cleanup fns (timer, connectivity, SSE)
|
;; Run all cleanup fns (timer, connectivity, SSE)
|
||||||
(doseq [f @(.-cleanup_fn sa)] (f)))
|
(doseq [f @(.-cleanup_fn sa)] (f)))
|
||||||
|
|
||||||
|
(defn close!
|
||||||
|
"Close the underlying store of a Connection."
|
||||||
|
[conn]
|
||||||
|
(p/let [store (:store-promise conn)]
|
||||||
|
(store/close-store! store)))
|
||||||
|
|||||||
@@ -57,6 +57,99 @@
|
|||||||
;; Tests
|
;; Tests
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Connection API tests
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(deftest connect-with-store
|
||||||
|
(testing "connect accepts a :store option"
|
||||||
|
(let [store (memory/create)
|
||||||
|
conn (pb/connect {:store store})]
|
||||||
|
(is (instance? atomsync.core.Connection conn))
|
||||||
|
(is (some? (:store-promise conn))))))
|
||||||
|
|
||||||
|
(deftest connect-synced-atom-local
|
||||||
|
(testing "synced-atom works with a Connection (no server)"
|
||||||
|
(let [conn (pb/connect {:store (memory/create)})
|
||||||
|
sa (pb/synced-atom conn "todo")]
|
||||||
|
(await! (pb/ready? sa) 1000)
|
||||||
|
(is (= {} @sa))
|
||||||
|
|
||||||
|
(swap! sa assoc "todo:1" {:text "Buy milk"})
|
||||||
|
(is (= {:text "Buy milk"} (get @sa "todo:1")))
|
||||||
|
|
||||||
|
(pb/destroy! sa))))
|
||||||
|
|
||||||
|
(deftest connect-synced-atom-with-server
|
||||||
|
(testing "synced-atom with Connection inherits server opts"
|
||||||
|
(let [conn (pb/connect {:store (memory/create)
|
||||||
|
:server (server-url)
|
||||||
|
:interval 60000})
|
||||||
|
sa (pb/synced-atom conn "todo")]
|
||||||
|
(await! (pb/ready? sa) 2000)
|
||||||
|
|
||||||
|
(swap! sa assoc "todo:c1" {:text "Via conn"})
|
||||||
|
(Thread/sleep 500)
|
||||||
|
|
||||||
|
(is (zero? (pb/pending-count sa)))
|
||||||
|
(pb/destroy! sa))))
|
||||||
|
|
||||||
|
(deftest connect-shared-across-atoms
|
||||||
|
(testing "Multiple synced atoms share a Connection"
|
||||||
|
(let [conn (pb/connect {:store (memory/create)})
|
||||||
|
todos (pb/synced-atom conn "todo")
|
||||||
|
notes (pb/synced-atom conn "note")]
|
||||||
|
(await! (pb/ready? todos) 1000)
|
||||||
|
(await! (pb/ready? notes) 1000)
|
||||||
|
|
||||||
|
(swap! todos assoc "todo:1" {:text "Do thing"})
|
||||||
|
(swap! notes assoc "note:1" {:text "A note"})
|
||||||
|
|
||||||
|
(is (= 1 (count @todos)))
|
||||||
|
(is (= 1 (count @notes)))
|
||||||
|
(is (nil? (get @todos "note:1")) "Groups are isolated")
|
||||||
|
|
||||||
|
(pb/destroy! todos)
|
||||||
|
(pb/destroy! notes))))
|
||||||
|
|
||||||
|
(deftest synced-atom-promise-protocol
|
||||||
|
(testing "SyncedAtom works with p/let directly"
|
||||||
|
(let [conn (pb/connect {:store (memory/create)})
|
||||||
|
sa (pb/synced-atom conn "todo")]
|
||||||
|
;; p/let on the SA itself should resolve to the SA
|
||||||
|
(let [result (await! (p/let [resolved-sa sa]
|
||||||
|
(swap! resolved-sa assoc "todo:1" {:text "Hi"})
|
||||||
|
resolved-sa)
|
||||||
|
1000)]
|
||||||
|
(is (= sa result) "p/let resolves to the SyncedAtom itself")
|
||||||
|
(is (= {:text "Hi"} (get @sa "todo:1"))))
|
||||||
|
(pb/destroy! sa))))
|
||||||
|
|
||||||
|
(deftest close-connection
|
||||||
|
(testing "close! closes the store"
|
||||||
|
(let [store (memory/create)
|
||||||
|
conn (pb/connect {:store store})]
|
||||||
|
;; Should not throw
|
||||||
|
(await! (pb/close! conn) 1000))))
|
||||||
|
|
||||||
|
(deftest per-atom-overrides
|
||||||
|
(testing "synced-atom can override connection defaults"
|
||||||
|
(let [conn (pb/connect {:store (memory/create)
|
||||||
|
:server "http://localhost:9999/sync"
|
||||||
|
:interval 60000})
|
||||||
|
;; Override: no server for this particular atom
|
||||||
|
sa (pb/synced-atom conn "todo" {:server nil})]
|
||||||
|
(await! (pb/ready? sa) 1000)
|
||||||
|
|
||||||
|
(swap! sa assoc "todo:1" {:text "Local only"})
|
||||||
|
(is (= {:text "Local only"} (get @sa "todo:1")))
|
||||||
|
|
||||||
|
(pb/destroy! sa))))
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Original tests (raw store API — backwards compat)
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(deftest synced-atom-local-only
|
(deftest synced-atom-local-only
|
||||||
(testing "SyncedAtom works without a server (local store only)"
|
(testing "SyncedAtom works without a server (local store only)"
|
||||||
(let [store (memory/create)
|
(let [store (memory/create)
|
||||||
|
|||||||
Reference in New Issue
Block a user