Files
atomsync/src/pocketbook/example.cljs
Florian Schroedl 55cddf751b feat: implement Pocketbook — a Clojure-native synced atom
Offline-first key-value store with atom interface (swap!, deref,
add-watch) that syncs to a SQLite-backed server over Transit.

Server (CLJ):
- SQLite storage with Nippy serialization preserving all Clojure types
- GET /sync?group=G&since=T pull endpoint with prefix-based groups
- POST /sync push endpoint with per-document version checking
- Conflict detection (stale write rejection)
- Token-based auth with per-user group access
- CORS support, soft deletes, purge compaction

Client (CLJS):
- IndexedDB wrapper with Transit serialization
- SyncedAtom implementing IAtom (IDeref, ISwap, IReset, IWatchable)
- Write-through to IndexedDB on every swap!
- Background sync loop (pull + push) with configurable interval
- Online/offline detection with reconnect sync
- Conflict resolution (accept server value)
- ready? channel for initial load
- Custom cache atom support (Reagent ratom compatible)

25 tests, 77 assertions across db, transit, server, and auth.
2026-04-04 16:33:14 +02:00

76 lines
2.7 KiB
Clojure

(ns pocketbook.example
"Example: a simple todo app using Pocketbook."
(:require [pocketbook.core :as pb]
[cljs.core.async :refer [go <!]]))
(defn- render-todos! [todos-atom]
(let [todos @todos-atom
container (js/document.getElementById "app")]
(set! (.-innerHTML container)
(str "<h1>Pocketbook Todos (" (count todos) ")</h1>"
"<div id='add-form'>"
"<input id='new-todo' type='text' placeholder='New todo...' />"
"<button id='add-btn'>Add</button>"
"</div>"
"<ul>"
(apply str
(for [[id doc] (sort-by key todos)]
(str "<li>"
"<label>"
"<input type='checkbox' data-id='" id "' "
(when (:done doc) "checked") " /> "
"<span" (when (:done doc) " style='text-decoration:line-through'") ">"
(:text doc)
"</span>"
"</label>"
" <button class='del-btn' data-id='" id "'>✕</button>"
"</li>")))
"</ul>"
"<p style='color:#888;font-size:12px'>"
"Pending sync: " (pb/pending-count todos-atom)
"</p>"))))
(defn- setup-handlers! [todos-atom]
;; We re-setup after each render
(when-let [btn (js/document.getElementById "add-btn")]
(.addEventListener btn "click"
(fn [_]
(let [input (js/document.getElementById "new-todo")
text (.-value input)]
(when (seq text)
(let [id (str "todo:" (random-uuid))]
(swap! todos-atom assoc id {:text text :done false})
(set! (.-value input) "")))))))
;; Checkbox toggles
(doseq [cb (array-seq (.querySelectorAll js/document "input[type=checkbox]"))]
(.addEventListener cb "change"
(fn [e]
(let [id (.-id (.-dataset (.-target e)))]
(swap! todos-atom update-in [id :done] not)))))
;; Delete buttons
(doseq [btn (array-seq (.querySelectorAll js/document ".del-btn"))]
(.addEventListener btn "click"
(fn [e]
(let [id (.-id (.-dataset (.-target e)))]
(swap! todos-atom dissoc id))))))
(defn ^:export init []
(go
(let [conn (<! (pb/open "pocketbook-example"))
todos (pb/synced-atom conn "todo"
{:server "http://localhost:8090/sync"})]
;; Wait for initial load
(<! (pb/ready? todos))
;; Render
(render-todos! todos)
(setup-handlers! todos)
;; Re-render on changes
(add-watch todos :render
(fn [_ _ _ _]
(render-todos! todos)
(setup-handlers! todos)))
(js/console.log "Pocketbook example loaded!" (count @todos) "todos"))))
;; Auto-init
(init)