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:
parent
6563a3ecc4
commit
86b7ec0ca2
|
@ -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
|
||||
|
|
|
@ -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!]]))
|
||||
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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))
|
|
@ -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)))
|
||||
|
|
9
reagent-data-views/src/reagent_data_views/utils.cljc
Normal file
9
reagent-data-views/src/reagent_data_views/utils.cljc
Normal 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/")))
|
Loading…
Reference in a new issue