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.
76 lines
2.7 KiB
Clojure
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)
|