Move core, sync, and transit from platform-specific .clj/.cljs to shared .cljc files with reader conditionals. This enables testing the full sync logic on the JVM and using SyncedAtom from Clojure clients. Key changes: - PStore protocol (store.cljc) decouples core from storage backend - IDB store (store/idb.cljs) and memory store (store/memory.cljc) - SyncedAtom implements CLJ IDeref/IAtom/IRef + CLJS equivalents - Sync client uses java.net.http on CLJ, fetch on CLJS - SSE remains CLJS-only; JVM clients use polling - API change: store passed explicitly instead of pb/open - 7 new JVM tests: local ops, persistence, watches, two-client sync - 28 tests total, 87 assertions, all passing
119 lines
4.3 KiB
Clojure
119 lines
4.3 KiB
Clojure
(ns pocketbook.store.idb
|
|
"IndexedDB store implementing the PStore protocol."
|
|
(:require [pocketbook.store :as store]
|
|
[pocketbook.transit :as transit]
|
|
[cljs.core.async :as async :refer [chan put!]]))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; IDB operations
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defn- tx
|
|
"Start an IDB transaction. mode is :readonly or :readwrite."
|
|
[db store-name mode]
|
|
(let [mode-str (case mode :readonly "readonly" :readwrite "readwrite")]
|
|
(.transaction db #js [store-name] mode-str)))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; IDBStore
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(deftype IDBStore [db]
|
|
store/PStore
|
|
(put-doc! [_ doc]
|
|
(let [ch (chan 1)
|
|
txn (tx db "docs" :readwrite)
|
|
store (.objectStore txn "docs")
|
|
obj #js {:id (:id doc)
|
|
:value (transit/encode (:value doc))
|
|
:version (:version doc 0)
|
|
:updated (:updated doc 0)
|
|
:deleted (boolean (:deleted doc false))
|
|
:synced (boolean (:synced doc false))}
|
|
req (.put store obj)]
|
|
(set! (.-onsuccess req) (fn [_] (put! ch true) (async/close! ch)))
|
|
(set! (.-onerror req) (fn [e] (js/console.error "IDB put error:" e) (async/close! ch)))
|
|
ch))
|
|
|
|
(docs-by-prefix [_ prefix]
|
|
(let [ch (chan 1)
|
|
txn (tx db "docs" :readonly)
|
|
store (.objectStore txn "docs")
|
|
range (.bound js/IDBKeyRange prefix (str prefix "\uffff"))
|
|
req (.openCursor store range)
|
|
docs (atom [])]
|
|
(set! (.-onsuccess req)
|
|
(fn [e]
|
|
(let [cursor (.-result (.-target e))]
|
|
(if cursor
|
|
(let [val (.-value cursor)]
|
|
(swap! docs conj
|
|
{:id (.-id val)
|
|
:value (transit/decode (.-value val))
|
|
:version (.-version val)
|
|
:updated (.-updated val)
|
|
:deleted (.-deleted val)
|
|
:synced (.-synced val)})
|
|
(.continue cursor))
|
|
(do
|
|
(put! ch @docs)
|
|
(async/close! ch))))))
|
|
(set! (.-onerror req)
|
|
(fn [e] (js/console.error "IDB cursor error:" e) (async/close! ch)))
|
|
ch))
|
|
|
|
(get-meta [_ key]
|
|
(let [ch (chan 1)
|
|
txn (tx db "meta" :readonly)
|
|
store (.objectStore txn "meta")
|
|
req (.get store key)]
|
|
(set! (.-onsuccess req)
|
|
(fn [e]
|
|
(let [result (.-result (.-target e))]
|
|
(if result
|
|
(do (put! ch (.-value result))
|
|
(async/close! ch))
|
|
(async/close! ch)))))
|
|
(set! (.-onerror req) (fn [_] (async/close! ch)))
|
|
ch))
|
|
|
|
(set-meta! [_ key value]
|
|
(let [ch (chan 1)
|
|
txn (tx db "meta" :readwrite)
|
|
store (.objectStore txn "meta")
|
|
req (.put store #js {:key key :value value})]
|
|
(set! (.-onsuccess req) (fn [_] (put! ch true) (async/close! ch)))
|
|
(set! (.-onerror req) (fn [_] (async/close! ch)))
|
|
ch))
|
|
|
|
(close-store! [_]
|
|
(when db (.close db))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Open
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defn open
|
|
"Open an IndexedDB store. Returns a channel yielding the IDBStore."
|
|
[db-name]
|
|
(let [ch (chan 1)
|
|
req (.open js/indexedDB db-name 1)]
|
|
(set! (.-onupgradeneeded req)
|
|
(fn [e]
|
|
(let [db (.-result (.-target e))]
|
|
(when-not (.contains (.-objectStoreNames db) "docs")
|
|
(let [store (.createObjectStore db "docs" #js {:keyPath "id"})]
|
|
(.createIndex store "synced" "synced" #js {:unique false})
|
|
(.createIndex store "updated" "updated" #js {:unique false})))
|
|
(when-not (.contains (.-objectStoreNames db) "meta")
|
|
(.createObjectStore db "meta" #js {:keyPath "key"})))))
|
|
(set! (.-onsuccess req)
|
|
(fn [e]
|
|
(put! ch (IDBStore. (.-result (.-target e))))
|
|
(async/close! ch)))
|
|
(set! (.-onerror req)
|
|
(fn [e]
|
|
(js/console.error "IDB open error:" e)
|
|
(async/close! ch)))
|
|
ch))
|