diff --git a/.gitignore b/.gitignore index c363d45..0587e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ node_modules .DS_Store #src/**/*.js -- for tsc modules only, see https://github.com/hyperfiddle/photon/commit/0aa9b26c35d961151891c1e42ec13fcf52e38fa2 /src-dev/fiddles.cljc +# for xtdb-starter demo: +data +# --- \ No newline at end of file diff --git a/deps.edn b/deps.edn index 0cd1bcc..08fb914 100644 --- a/deps.edn +++ b/deps.edn @@ -68,6 +68,13 @@ {:extra-deps {com.datomic/peer {:mvn/version "1.0.7075"}}} + :xtdb-starter ; process must be started with `XTDB_ENABLE_BYTEUTILS_SHA1=true clj -A:xtdb-starter:...` + {:extra-deps + {com.xtdb/xtdb-core {:mvn/version "1.23.0"} + com.xtdb/xtdb-rocksdb {:mvn/version "1.23.0"}} + :jvm-opts [;; the following option is required for JDK 16 and 17 (https://github.com/xtdb/xtdb/issues/1462) + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"]} + :uix-demo {:extra-deps {com.pitch/uix.core {:mvn/version "1.0.1"} com.pitch/uix.dom {:mvn/version "1.0.1"}}} diff --git a/src/xtdb_starter/Readme.md b/src/xtdb_starter/Readme.md new file mode 100644 index 0000000..2ba288e --- /dev/null +++ b/src/xtdb_starter/Readme.md @@ -0,0 +1,18 @@ +This demo requires an environment variable to be set. + +Run a REPL: +```shell +XTDB_ENABLE_BYTEUTILS_SHA1=true clj -A:dev:xtdb-starter +``` +At the REPL: +```clojure +(dev/-main) +(dev/load-fiddle! 'xtdb-starter) +``` + +Build and run for prod: + +```shell +clojure -X:build:prod:xtdb-starter build-client :hyperfiddle/domain xtdb-starter +XTDB_ENABLE_BYTEUTILS_SHA1=true clj -M:prod:xtdb-starter -m prod +``` diff --git a/src/xtdb_starter/fiddles.cljc b/src/xtdb_starter/fiddles.cljc new file mode 100644 index 0000000..f2bb433 --- /dev/null +++ b/src/xtdb_starter/fiddles.cljc @@ -0,0 +1,41 @@ +(ns xtdb-starter.fiddles + (:require #?(:clj [clojure.java.io :as io]) + #?(:clj [xtdb.api :as xt]) + [hyperfiddle.electric-dom2 :as dom] + [hyperfiddle.electric :as e] + [xtdb-starter.todo-list :refer [Todo-list]])) + +#?(:clj (defonce !xtdb-node (atom nil))) + +#?(:clj + (defn start-xtdb! [] ; from XTDB’s getting started: xtdb-in-a-box + (assert (= "true" (System/getenv "XTDB_ENABLE_BYTEUTILS_SHA1"))) ; App must start with this env var set to "true" + (letfn [(kv-store [dir] {:kv-store {:xtdb/module 'xtdb.rocksdb/->kv-store + :db-dir (io/file dir) + :sync? true}})] + (or @!xtdb-node + (reset! !xtdb-node + (xt/start-node + {:xtdb/tx-log (kv-store "data/dev/tx-log") + :xtdb/document-store (kv-store "data/dev/doc-store") + :xtdb/index-store (kv-store "data/dev/index-store")})))))) + +(e/defn XTDB-Starter [] + (e/server + (if-let [!xtdb (try (e/offload #(start-xtdb!)) + (catch hyperfiddle.electric.Pending _ + nil))] + (Todo-list. !xtdb) + (e/client + (dom/p (dom/text "XTDB is starting ...")))))) + +(e/def fiddles {`XTDB-Starter XTDB-Starter}) + +(e/defn FiddleMain [ring-request] + (e/client + (binding [dom/node js/document.body] + (XTDB-Starter.)))) + +(comment + (.close @!xtdb-node) + (reset! !xtdb-node nil)) diff --git a/src/xtdb_starter/todo_list.cljc b/src/xtdb_starter/todo_list.cljc new file mode 100644 index 0000000..0fd714b --- /dev/null +++ b/src/xtdb_starter/todo_list.cljc @@ -0,0 +1,83 @@ +(ns xtdb-starter.todo-list + (:require + #?(:clj [xtdb-starter.xtdb-contrib :as db]) + [hyperfiddle.electric :as e] + [hyperfiddle.electric-dom2 :as dom] + [hyperfiddle.electric-ui4 :as ui] + [xtdb.api #?(:clj :as :cljs :as-alias) xt])) + +(e/def !xtdb) +(e/def db) ; injected database ref; Electric defs are always dynamic + +(e/defn TodoItem [id] + (e/server + (let [e (xt/entity db id) + status (:task/status e)] + (e/client + (dom/div + (ui/checkbox + (case status :active false, :done true) + (e/fn [v] + (e/server + (e/discard + (e/offload + #(xt/submit-tx !xtdb [[:xtdb.api/put + {:xt/id id + :task/description (:task/description e) ; repeat + :task/status (if v :done :active)}]]))))) + (dom/props {:id id})) + (dom/label (dom/props {:for id}) (dom/text (e/server (:task/description e))))))))) + +(e/defn InputSubmit [F] + ; Custom input control using lower dom interface for Enter handling + (e/client + (dom/input (dom/props {:placeholder "Buy milk"}) + (dom/on "keydown" (e/fn [e] + (when (= "Enter" (.-key e)) + (when-some [v (contrib.str/empty->nil (-> e .-target .-value))] + (new F v) + (set! (.-value dom/node) "")))))))) + +(e/defn TodoCreate [] + (e/client + (InputSubmit. (e/fn [v] + (e/server + (e/discard + (e/offload + #(xt/submit-tx !xtdb [[:xtdb.api/put + {:xt/id (random-uuid) + :task/description v + :task/status :active}]])))))))) + +#?(:clj + (defn todo-records [db] + (->> (xt/q db '{:find [(pull ?e [:xt/id :task/description])] + :where [[?e :task/status]]}) + (map first) + (sort-by :task/description) + vec))) + +#?(:clj + (defn todo-count [db] + (count (xt/q db '{:find [?e] :in [$ ?status] + :where [[?e :task/status ?status]]} + :active)))) + +(e/defn Todo-list [!xtdb] + (e/server + (binding [xtdb-starter.todo-list/!xtdb !xtdb + db (new (db/latest-db> !xtdb))] + (e/client + (dom/link (dom/props {:rel :stylesheet :href "/todo-list.css"})) + (dom/h1 (dom/text "minimal todo list")) + (dom/p (dom/text "it's multiplayer, try two tabs")) + (dom/div (dom/props {:class "todo-list"}) + (TodoCreate.) + (dom/div {:class "todo-items"} + (e/server + (e/for-by :xt/id [{:keys [xt/id]} (e/offload #(todo-records db))] + (TodoItem. id)))) + (dom/p (dom/props {:class "counter"}) + (dom/span (dom/props {:class "count"}) + (dom/text (e/server (e/offload #(todo-count db))))) + (dom/text " items left"))))))) diff --git a/src/xtdb_starter/xtdb_contrib.clj b/src/xtdb_starter/xtdb_contrib.clj new file mode 100644 index 0000000..218759a --- /dev/null +++ b/src/xtdb_starter/xtdb_contrib.clj @@ -0,0 +1,16 @@ +(ns xtdb-starter.xtdb-contrib + (:require [missionary.core :as m] + [xtdb.api :as xt])) + +(defn latest-db> + "return flow of latest XTDB tx, but only works for XTDB in-process mode. see + https://clojurians.slack.com/archives/CG3AM2F7V/p1677432108277939?thread_ts=1677430221.688989&cid=CG3AM2F7V" + [!xtdb] + (->> (m/observe (fn [!] + (let [listener (xt/listen !xtdb {::xt/event-type ::xt/indexed-tx :with-tx-ops? true} !)] + #(.close listener)))) + (m/reductions {} (xt/latest-completed-tx !xtdb)) ; initial value is the latest known tx, possibly nil + (m/relieve {}) + (m/latest (fn [{:keys [:xtdb.api/tx-time] :as ?tx}] + (if tx-time (xt/db !xtdb {::xt/tx-time tx-time}) + (xt/db !xtdb))))))