The idea is to have a structure of subscribers on the different parts (different paths) of the db via a hierarchy of hashmaps which list sets of subscribers in its nodes. I call this structure a subscription-tree
.
That is probably not a new thing, I guess reagent has something like that. The part which might be new is to have a subscription-tree
on each node of the compute graph. It means that the action of listening changes on a part of a data is a generalized and homogenous feature in Vrac.
The selection of the right subscribers is fast when done based on a diff. An since each compute node is going to return a diff, everything should work well and efficiently.
Precision: the diff approach is only related to structural changes. Most operations which will be in the compute node (like first-name + last-name -> full-name
) won’t really need it, and will still be written as a classical function implementation which does not output diffs. Those functions will be wrapped to fit in the compute graph.
Thanks to the subscription-tree
, the template below will use zero compute nodes and will feed directly on the client db through this structure:
;; Subscription-tree on the client db:
{:children {:user {:children {:first-name {:subscribers #{1}}
:last-name {:subscribers #{2}}}}}}
;; Subscribing template:
(let [user (:user global)
{:keys [first-name last-name]} user]
[:div first-name " " last-name])
I am going to start implementing a (slow) reference version of the compute graph, as a proof-of-concept of the algorithms and diff-flow.
Here is a bit of the impl:
(def empty-subscription-tree {})
(defn subscribe-on-path [tree path subscriber]
(if (seq path)
(let [[path-element & path-rest] path]
(update-in tree [:children path-element] subscribe-on-path path-rest subscriber))
(update tree :subscribers (fnil conj #{}) subscriber)))
(defn- update-coll-then-dissoc-empty [coll key f & args]
(let [new-val (apply f (get coll key) args)]
(if (seq new-val) ; new-val is assumed to be a collection
(assoc coll key new-val)
(dissoc coll key))))
(defn unsubscribe-from-path [tree path subscriber]
(if (seq path)
(let [[path-element & path-rest] path]
(update-coll-then-dissoc-empty tree :children
update-coll-then-dissoc-empty path-element
unsubscribe-from-path path-rest subscriber))
(update-coll-then-dissoc-empty tree :subscribers disj subscriber)))
;; Testing
#_ (-> empty-subscription-tree
(subscribe-on-path [:user :first-name] 1)
(subscribe-on-path [:user :last-name] 2))
; => {:children {:user {:children {:first-name {:subscribers #{1}}
; :last-name {:subscribers #{2}}}}}}
#_ (-> *1
(unsubscribe-from-path [:user :first-name] 1)
(unsubscribe-from-path [:user :last-name] 2))
; => {}
— End of today’s brainstorming for me.