lots of updates: new views and reagent. remove direct browserchannel dep

new "architecture" will rely on secondary "adapter" libraries that
plug in to reagent-data-views and provide the actual underlying
client/server messaging implementation (e.g. browserchannel or sente)
This commit is contained in:
Gered 2016-05-19 16:24:26 -04:00
parent 6563a3ecc4
commit 86b7ec0ca2
6 changed files with 93 additions and 195 deletions

View file

@ -4,7 +4,7 @@
:license {:name "MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies []
:dependencies [[org.clojure/tools.logging "0.3.1"]]
:profiles {:provided
{:dependencies

View file

@ -2,7 +2,7 @@
(:require
[clojure.set :refer [difference]]
[reagent.core :as r]
[reagent.impl.util :refer [reagent-component?]]
[reagent.impl.component :refer [reagent-component?]]
[reagent-data-views.client.core :as views]
[reagent-data-views.client.utils :refer [update-component-state!]]))

View file

@ -1,19 +1,18 @@
(ns reagent-data-views.client.core
(:require
[reagent.core :as r]
[clj-browserchannel-messaging.client :as browserchannel]))
;; IMPORTANT NOTE:
;; We are using Reagent's built-in RCursor instead of the one provided by reagent-cursor
;; because as of reagent 0.5.0 there is added performance improvements to protect
;; against extra rerenders when deref'ing cursors.
;; This is very important for us here as we store all view subscription data in a single
;; Reagent atom and use cursors keyed by view-sig to access the data. With reagent 0.5.0
;; cursors, Component A deref'ing view-sig X will not rerender when Component B
;; deref'ing view-sig Y receives updated data.
[reagent-data-views.utils :refer [relevant-event?]]))
(defonce view-data (r/atom {}))
(defonce send-fn (atom nil))
(defn send-data!
[data]
(if-not @send-fn
(throw (js/Error. "send-fn not set"))
(@send-fn data)))
(defn ->view-sig-cursor
"Creates and returns a Reagent cursor that can be used to access the data
for the view corresponding with the view-sig.
@ -29,48 +28,23 @@
[view-sig]
(r/cursor [view-sig :data] view-data))
(defn- inc-view-sig-refcount! [view-sig]
(let [path [view-sig :refcount]]
(swap! view-data update-in path #(if % (inc %) 1))
(get-in @view-data path)))
(defn- dec-view-sig-refcount! [view-sig]
(let [path [view-sig :refcount]]
(swap! view-data update-in path #(if % (dec %) 0))
(get-in @view-data path)))
(defn- add-initial-view-data! [view-sig data]
(defn- handle-view-refresh [view-sig data]
(let [cursor (->view-sig-cursor view-sig)]
(reset! cursor data)))
(defn- remove-view-data! [view-sig]
(swap! view-data dissoc view-sig))
(defn subscribed?
"Returns true if we are currently subscribed to the specified view."
[view-sig]
(boolean (get view-data view-sig)))
(defn- apply-delete-deltas [existing-data delete-deltas]
(reduce
(fn [result row-to-delete]
(remove #(= % row-to-delete) result))
existing-data
delete-deltas))
(defn- apply-insert-deltas [existing-data insert-deltas]
(concat existing-data insert-deltas))
(defn- apply-deltas! [view-sig deltas]
(let [cursor (->view-sig-cursor view-sig)]
(doseq [{:keys [refresh-set insert-deltas delete-deltas]} deltas]
(if refresh-set (reset! cursor refresh-set))
(if (seq delete-deltas) (swap! cursor apply-delete-deltas delete-deltas))
(if (seq insert-deltas) (swap! cursor apply-insert-deltas insert-deltas)))))
(defn- handle-view-data-init [{:keys [body]}]
(doseq [[view-sig data] body]
(add-initial-view-data! view-sig data)))
(defn- handle-view-deltas [{:keys [body]}]
(doseq [delta-batch body]
(doseq [[view-sig deltas] delta-batch]
(apply-deltas! view-sig deltas))))
(defn- update-for-unsubscription!
[view-sig]
(swap! view-data
(fn [vd]
(let [{:keys [refcount data]} (get vd view-sig)]
(if (= 1 refcount)
(dissoc vd view-sig)
(update-in vd [view-sig :refcount] dec))))))
(defn unsubscribe!
"Unsubscribes from all of the specified view(s). No further updates from the
@ -78,10 +52,22 @@
the server is cleared."
[view-sigs]
(doseq [view-sig view-sigs]
(let [refcount (dec-view-sig-refcount! view-sig)]
(when (<= refcount 0)
(remove-view-data! view-sig)
(browserchannel/send :views.unsubscribe [view-sig])))))
(let [vd (update-for-unsubscription! view-sig)]
(if-not (get vd view-sig)
(send-data! [:views/unsubscribe view-sig])))))
(defn- update-for-subscription!
[view-sig]
(swap! view-data
(fn [vd]
(let [{:keys [refcount data]} (get vd view-sig)]
; for the first subscription, add an initial entry for this view
; with empty data. the server will send us initial data as
; a standard view refresh when the subscription is processed
(if-not refcount
(assoc vd view-sig {:refcount 1
:data nil})
(update-in vd [view-sig :refcount] inc))))))
(defn subscribe!
"Subscribes to the specified view(s). Updates to the data on the server will
@ -89,10 +75,11 @@
render it in any component(s)."
[view-sigs]
(doseq [view-sig view-sigs]
(let [refcount (inc-view-sig-refcount! view-sig)]
(when (= refcount 1)
(add-initial-view-data! view-sig nil)
(browserchannel/send :views.subscribe [view-sig])))))
(let [vd (update-for-subscription! view-sig)]
; on the first subscription we need to tell the server we are subscribing
; to this view
(if (= 1 (get-in vd [view-sig :refcount]))
(send-data! [:views/subscribe view-sig])))))
(defn update-subscriptions!
"Unsubscribes from old-view-sigs and then subscribes to new-view-sigs. This
@ -102,9 +89,12 @@
(unsubscribe! old-view-sigs)
(subscribe! new-view-sigs))
(defn init!
"Sets up message handling needed to process incoming view subscription deltas.
Should be called once on page load after BrowserChannel has been initialized."
[]
(browserchannel/message-handler :views.init handle-view-data-init)
(browserchannel/message-handler :views.deltas handle-view-deltas))
(defn on-receive!
[data]
(when (relevant-event? data)
(let [[event & args] data]
(condp = event
:views/refresh (handle-view-refresh (first args) (second args))
(js/console.log "unrecognized event" event "-- full received data:" data))
; indicating that we handled the received event
true)))

View file

@ -1,16 +1,11 @@
(ns reagent-data-views.client.utils
(:require
[reagent.core :as r]
[reagent.impl.component :as rcomp]
[reagent.impl.util :refer [reagent-component?]]))
[reagent.impl.component :refer [reagent-component?]]))
; TODO: relies on internal Reagent functionality. state-atom is not officially
; part of the public Reagent API yet, but will probably be part of it in
; the future. this function may need to be updated at that time.
; https://github.com/reagent-project/reagent/issues/80#issuecomment-67302125
(defn update-component-state!
"Updates the Reagent component's internal state atom by swap!-ing in the value
returned by the function f (which receives the current state atom's value)."
[owner f]
(assert (reagent-component? owner))
(swap! (rcomp/state-atom owner) f))
(swap! (r/state-atom owner) f))

View file

@ -1,130 +1,34 @@
(ns reagent-data-views.server.core
(:require
[clojure.core.async :refer [put!]]
[clj-browserchannel-messaging.server :as browserchannel]
[views.core :as vc]
[views.persistence.core :refer [subscriptions]]
[views.persistence.memory :refer [new-memory-persistence]]
[views.router :as vr]
[views.subscribed-views :refer [subscribed-views persistence]]))
[clojure.string :as string]
[clojure.tools.logging :as log]
[views.core :as views]
[reagent-data-views.utils :refer [relevant-event?]]))
(defonce
^{:doc "The Views configuration. Used with functions like vexec!, etc."}
views-config (atom nil))
(defn on-close!
[client-id]
(log/trace client-id "on-close, unsubscribing from all views")
(views/unsubscribe-all! client-id))
(defn- send-deltas [client-id topic body]
(browserchannel/send client-id topic body))
(defn handle-subscriptions!
[client-id view-sig]
(log/trace client-id "subscribing to" view-sig)
(let [{:keys [namespace view-id parameters]} view-sig]
(views/subscribe! namespace view-id parameters client-id)))
(defn- no-filters? [templates]
(not (some
(fn [[_ {:keys [filter-fn]}]]
(not (nil? filter-fn)))
templates)))
(defn handle-unsubscriptions!
[client-id view-sig]
(log/trace client-id "unsubscribing from" view-sig)
(let [{:keys [namespace view-id parameters]} view-sig]
(views/unsubscribe! namespace view-id parameters client-id)))
(defn init!
"Initializes configuration for the Views system suitable for use with most applications
that are only using a single database. You should call this function once during
application startup after your database connection has been configured. If your
application requires a more complex Views configuration, you can manually configure
it yourself and pass in the configuration map directly as the only argument.
Either way, this function needs to be called as in both cases it initializes
views.router to enable automatic routing of view subscription/unsubscription
browserchannel messages.
* db
Database connection.
* persistence
Optional. An instance of views.persistence.core/IPersistence. If not specified, a
default views.persistence.memory/ViewsMemoryPersistence instance is used which keeps
all view subscriptions and data in memory (not suitable for large applications).
* send-fn
Optional. A function that takes care of sending view data deltas to clients (e.g. a
function that sends data to a client via BrowserChannel). The function should accept
3 arguments: client-id, topic and body. If not specified, a default function that
sends deltas to the subscribed BrowserChannel client is used.
* subscriber-key
Optional. A function applied against incoming BrowserChannel subscription messages
that is used to get the client-id from the message. If not used, the default is
:client-id.
* templates
Map of views used by this application. Keys are the view names and the values are maps
containing information about each view. The template map values can contain the
following keys:
:fn
Required. A var of a function (e.g. #'func or (var func)) that returns a HoneySQL
SELECT query map. This query when run should return the data that this view
represents. The function can receive any number of arguments which will be passed in
as-is from the view-sig (everything after the name).
:post-fn
Optional. A function that can be used to filter the data returned by the view.
Receives one argument (the data).
:filter-fn
Optional. A function that is run before a view subscription is processed. If false
is returned, the view subscription is denied. This function receives 2 arguments:
the raw BrowserChannel subscription request message and the view-sig of the view
being subscribed to.
Additionally, the template map you pass in can contain metadata (on the map itself)
that has a :filter-fn key which works the same as individual view filter-fn's
described above, except that this one will be global and run before subscriptions
to any view."
([config]
(vr/init! config browserchannel/incoming-messages-pub)
(reset! views-config config))
([db templates & {:keys [persistence send-fn subscriber-key]}]
(let [persistence (or persistence (new-memory-persistence))
send-fn (or send-fn send-deltas)
subscriber-key (or subscriber-key :client-id)]
(init! (-> {:db db
:subscriber-key-fn subscriber-key
:templates templates
:persistence persistence
:send-fn send-fn
:unsafe? (no-filters? templates)}
(vc/config)
; these just keep some useful things around that are handy to have
; for 'simpler' use-cases. vc/config returns a map without these
; present (even though we passed them in).
; this is not necessarily desirable behaviour for certain advanced
; configurations!
(assoc :namespace :default-ns)
(assoc :subscriber-key-fn subscriber-key))))))
(def
^{:doc "Middleware for use with clj-browserchannel-messaging that performs important housekeeping operations."}
views-middleware
{:on-close (fn [handler]
(fn [client-id request reason]
; views.router is notified of session disconnects when messages of this type
; are received on the channel passed to views.router/init!. we simply
; inject a disconnect message on this channel when the browserchannel session
; is closed and all is good
(put! browserchannel/incoming-messages
(-> {:topic :client-channel
:body :disconnect}
(assoc (:subscriber-key-fn @views-config) client-id)))
(handler client-id request reason)))})
(defn get-subscribed-views
"Returns information about the views that are currently subscribed to by clients."
[]
(let [bsv (:base-subscribed-views @views-config)
namespace (or (:namespace @views-config) :default-ns)]
(subscribed-views bsv namespace)))
(defn get-subscriptions
"Returns a set of subscriber-keys representing clients subscribed to the views
identified by the list of view signatures specified."
[view-sigs]
(let [bsv (:base-subscribed-views @views-config)
persistence (persistence bsv)
namespace (or (:namespace @views-config) :default-ns)]
(subscriptions persistence namespace view-sigs)))
(defn on-receive!
[client-id data]
(when (relevant-event? data)
(let [[event view-sig] data]
(condp = event
:views/subscribe (handle-subscriptions! client-id view-sig)
:views/unsubscribe (handle-unsubscriptions! client-id view-sig)
(log/warn client-id "unrecognized event" event "-- full received data:" data))
; indicating that we handled the received event
true)))

View file

@ -0,0 +1,9 @@
(ns reagent-data-views.utils
(:require
[clojure.string :as string]))
(defn relevant-event?
[data]
(and (vector? data)
(keyword? (first data))
(string/starts-with? (name (first data)) "views/")))