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.
This commit is contained in:
75
src/pocketbook/example.cljs
Normal file
75
src/pocketbook/example.cljs
Normal file
@@ -0,0 +1,75 @@
|
||||
(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)
|
||||
Reference in New Issue
Block a user