Skip to content

Instantly share code, notes, and snippets.

@lgessler
Last active September 17, 2021 15:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lgessler/19f24dc89eb0a2a1a6a77d96dc1325b2 to your computer and use it in GitHub Desktop.
Save lgessler/19f24dc89eb0a2a1a6a77d96dc1325b2 to your computer and use it in GitHub Desktop.
(defn- filtered-refers
"ns-refers, but without clojure.core vars"
[ns]
(into {} (filter (fn [[_ v]] (not= "clojure.core" (namespace (symbol v))))
(ns-refers ns))))
(defn- fully-qualify-symbols
"Add fully qualified symbols where needed so that we can store the body in an XTDB transaction function.
"
[body]
(let [ns-vars (clojure.core/merge (ns-interns *ns*) (filtered-refers *ns*))]
(walk/postwalk
(fn [x]
(cond
(and (symbol? x) (some? (namespace x)))
(symbol (ns-resolve *ns* x))
(and (symbol? x) (contains? ns-vars x))
(symbol (get ns-vars x))
:else
x))
body)))
;; macro for transactions to avoid race conditions: https://clojurians-log.clojureverse.org/crux/2020-03-24
(defmacro deftx [name bindings & body]
"Defines a function used for mutations that uses a Crux transaction function under the hood.
Body must return a valid Crux transaction vector (or return false, error, etc.)
`install-tx-fns` must be called on the node before the deftx function can work.
NOTE: XTDB tx fns require all symbols to be fully qualified. This macro will attempt to resolve
them for you, with the following restriction: none of your symbols should shadow either symbols
that are interned in your current namespace, or ones that are :refer'd into your current namespace.
(If you do shadow them, e.g. in a `let` expression, this macro will fully qualify them and likely
produce an invalid expression which the compiler will complain about.)"
(let [kwd-name (keyword (str *ns*) (str name))
symbol-name (symbol (str name))
fq-bindings (fully-qualify-symbols bindings)
fq-body (fully-qualify-symbols body)]
`(do
(def
~(vary-meta
symbol-name
assoc
:crux-tx-fn
`(fn [node#]
(xt/submit-tx node# [[:xtdb.api/put {:xt/id ~kwd-name
:xt/fn (quote (fn ~fq-bindings
~@fq-body))}]])))
(fn ~symbol-name [node# & ~'args]
(let [tx-map# (xt/submit-tx node# [(into [:xtdb.api/fn ~kwd-name] ~'args)])]
(xt/await-tx node# tx-map#)
(xt/tx-committed? node# tx-map#))))
;; Also define a version that just returns the transaction
(def
~(symbol (str symbol-name "**"))
(fn ~symbol-name [node# & ~'args]
[(into [:xtdb.api/fn ~kwd-name] ~'args)]))))
(defn install-deftx-fns
"Given a node and a seq of namespace symbols, scan all public vars
and use any :crux-tx-fn in their metadata to install the tx-fn on
the node"
([node]
;; If no namespaces are supplied, take all "my.xtdb.ns" nses that aren't tests
(install-deftx-fns
node
(->> (all-ns)
(filter #(clojure.string/starts-with? (str %) "my.xtdb.ns"))
(filter #(not (clojure.string/ends-with? (str %) "-test"))))))
([node namespaces]
(doseq [ns-symbol namespaces]
(when-let [ns (the-ns ns-symbol)]
(doseq [[vname v] (ns-publics ns)]
(when-let [tx-install-fn (some-> v meta :crux-tx-fn)]
;; evict any already-existing entities with the tx-fn's id
;; TODO: is there any cost to doing this over and over? If so, consider
;; enabling this only in dev and using a put-if-nil strategy for prod
(xt/await-tx node (xt/submit-tx node [[:xtdb.api/evict (keyword (str ns) (str vname))]]))
(tx-install-fn node)))))))
;; Wherever you start your node, call install-deftx-fns on it: note you don't need to tell it anything special
(mount/defstate xtdb-node
:start
(let [node ...]
(install-deftx-fns node)))
;; Now, in your other namespace: use deftx to define a transaction function that
;; can do whatever it wants, usually returning a transaction vector
(deftx my-function [xtdb-node arg1 arg2]
(let [res1 (f arg1 arg2)]
[[:xtdb.api/put res1]]))
;; Finally, in your consumer code, just use my-function as you would any other side-effecting fn:
(my-function xtdb-node :foo :bar)
;; Or, if you want to compose its contents into a larger transaction, use the ** suffixed version:
(let [new-tx (into existing-tx (my-function** xtdb-node :foo :bar))]
...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment