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:
Florian Schroedl
2026-04-18 16:51:09 +02:00
parent 267d52e4ce
commit 78b897cc25
3 changed files with 189 additions and 54 deletions

View File

@@ -1,7 +1,6 @@
(ns atomsync.todomvc
"TodoMVC built on Atomsync — offline-first, synced, Clojure-native."
(:require [atomsync.core :as pb]
[atomsync.store.idb :as idb]
[atomsync.hiccup :refer [html]]
[promesa.core :as p]
[clojure.string :as str]))
@@ -10,8 +9,10 @@
;; State
;; ---------------------------------------------------------------------------
(defonce !conn (atom nil))
(defonce !todos (atom nil)) ;; the SyncedAtom
(defonce conn (pb/connect {:db "atomsync-todomvc"
:server "http://localhost:8090/sync"
:interval 15000}))
(defonce !todos (pb/synced-atom conn "todo"))
(defonce !filter (atom :all)) ;; :all | :active | :completed
(defonce !editing (atom nil)) ;; id of todo being edited, or nil
@@ -22,7 +23,7 @@
(defn- todos-list
"Return todos as sorted vec of [id doc] pairs."
[]
(->> @@!todos
(->> @!todos
(sort-by (fn [[_ doc]] (:created doc)))
vec))
@@ -50,33 +51,33 @@
(let [text (str/trim text)]
(when (seq text)
(let [id (str "todo:" (random-uuid))]
(swap! @!todos assoc id
(swap! !todos assoc id
{:text text
:completed false
:created (.now js/Date)})))))
(defn- toggle-todo! [id]
(swap! @!todos update-in [id :completed] not))
(swap! !todos update-in [id :completed] not))
(defn- toggle-all! []
(let [target (not (all-completed?))]
(swap! @!todos
(swap! !todos
(fn [m]
(reduce-kv (fn [acc k v] (assoc acc k (assoc v :completed target)))
{} m)))))
(defn- destroy-todo! [id]
(swap! @!todos dissoc id))
(swap! !todos dissoc id))
(defn- edit-todo! [id new-text]
(let [text (str/trim new-text)]
(if (seq text)
(swap! @!todos assoc-in [id :text] text)
(swap! !todos assoc-in [id :text] text)
(destroy-todo! id))
(reset! !editing nil)))
(defn- clear-completed! []
(swap! @!todos
(swap! !todos
(fn [m]
(into {} (remove (fn [[_ v]] (:completed v))) m))))
@@ -119,7 +120,7 @@
"Clear completed"])]))
(defn- render-sync-status []
(let [pending (when @!todos (pb/pending-count @!todos))
(let [pending (pb/pending-count !todos)
online? (.-onLine js/navigator)]
[:div.sync-bar
[:span {:class (str "sync-dot " (if online? "online" "offline"))}]
@@ -232,22 +233,16 @@
;; ---------------------------------------------------------------------------
(defn ^:export init []
(p/let [store (idb/open "atomsync-todomvc")]
(let [todos (pb/synced-atom store "todo"
{:server "http://localhost:8090/sync"
:interval 15000})]
(reset! !conn store)
(reset! !todos todos)
;; Render + bind immediately (empty or stale is fine)
(render!)
(bind-events!)
;; Re-render on any data change (fires when IDB loads + server syncs)
(add-watch todos :render (fn [_ _ _ _] (render!)))
(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")))))
;; Render + bind immediately (empty or stale is fine)
(render!)
(bind-events!)
;; Re-render on any data change (fires when IDB loads + server syncs)
(add-watch !todos :render (fn [_ _ _ _] (render!)))
(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 [todos !todos]
(js/console.log "🔶 Atomsync TodoMVC loaded —" (count @todos) "todos")))
(init)