cgrand / enlivez

8 stars 0 forks source link

Day #1 #2

Closed cgrand closed 5 years ago

cgrand commented 5 years ago

Working on the working prototype. The goal is to get the lib in the hand of devs asap even if the implementation is utterly naive and inefficient.

Some realizations of the day: component identity, query maps in arguments and subscriptions.

Component identity

A component is fully identified by the arguments that were passed to it (if they change then the component is replaced, not mutated) and its mount point (relative to its parent). However one must not panick at these words: when a component represents a todo-list item, its argument is the list item entity id which is stable. Changing the item title would not replace the component, just refresh it (or some subcomponents).

Query maps as arguments

When using query maps as a kind of destructuring (for example {[title status] ::item/attrs}) in arguments there's the risk the resulting query may return 0 to many rows where we expect only one.

Too many rows: this can be prevented by analysis of the query and attributes cardinality.

For zero rows returned (eg because the entity as a missing field and no default has been provided), this has to be dealt with at runtime. Great, but how? Through a "default default value" (nil being a candidate)? Warning? Exception?

Subscriptions

So in our model, the database is not available as a first class value (except in transaction functions). One has to subscribe to a (rel-returning) query. If the query returns N columns then the subscribed handler receives a relation with N+1 columns: the last one is a boolean, true for addition, false for retractation.

I was uneasy with using d/listen! to implement it because you have to take Javascript "thread model" in account to be sure that there's no lost transaction between the initial query (used to bootstrap the live-query) and the first delta. (There are 2 atoms: one for the listeners, one for the db...) While it can be worked through, I opted for a simpler and more general solution that works on the JVM too. This solution is to have only one listener (created at db init), and all subscriptions are reified in the db itself! Thus a live query kicks off the moment its subscription is transacted!

;; Datasource is only accessible through subscription

(def ^:private conn
  (doto (d/create-conn {::child {:db/valueType :db.type/ref
                                 :db/isComponent true
                                 :db/cardinality :db.cardinality/many}})
    (d/listen! ::meta-subscriber
      (fn [{:keys [tx-data db-after]}]
        (doseq [[eid q f] (d/q '[:find ?eid ?q ?f :where [?eid ::live-query ?q] [?eid ::handler ?f]] db-after)]
          (f (d/q q db-after)))))))

(defn subscription
  "Returns transaction.
   Upon successful transaction, the "
  "Subscription is immediate: upon subscription f receives a (positive only) delta representing the current state."
  [q f]
  [{::live-query q
    ::handler
    (let [prev-rows (atom #{})]
      (fn [rows]
        (let [prev-rows @prev-rows
              added (reduce disj rows prev-rows)
              retracted (reduce disj prev-rows rows)
              delta (-> #{}
               (into (map #(conj % true)) added)
               (into (map #(conj % false)) retracted))]
          (when (not= #{} delta)
            (f delta)))
        (reset! prev-rows rows)))}])