diff --git a/reagent-data-views/project.clj b/reagent-data-views/project.clj index 262e131..dc3b2e2 100644 --- a/reagent-data-views/project.clj +++ b/reagent-data-views/project.clj @@ -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 diff --git a/reagent-data-views/src/reagent_data_views/client/component.cljs b/reagent-data-views/src/reagent_data_views/client/component.cljs index b308a24..e0ec552 100644 --- a/reagent-data-views/src/reagent_data_views/client/component.cljs +++ b/reagent-data-views/src/reagent_data_views/client/component.cljs @@ -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!]])) diff --git a/reagent-data-views/src/reagent_data_views/client/core.cljs b/reagent-data-views/src/reagent_data_views/client/core.cljs index 4ebffa3..ceab133 100644 --- a/reagent-data-views/src/reagent_data_views/client/core.cljs +++ b/reagent-data-views/src/reagent_data_views/client/core.cljs @@ -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))) diff --git a/reagent-data-views/src/reagent_data_views/client/utils.cljs b/reagent-data-views/src/reagent_data_views/client/utils.cljs index 0a2cade..d4c17a0 100644 --- a/reagent-data-views/src/reagent_data_views/client/utils.cljs +++ b/reagent-data-views/src/reagent_data_views/client/utils.cljs @@ -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)) \ No newline at end of file + (swap! (r/state-atom owner) f)) \ No newline at end of file diff --git a/reagent-data-views/src/reagent_data_views/server/core.clj b/reagent-data-views/src/reagent_data_views/server/core.clj index bd1a8fd..827947f 100644 --- a/reagent-data-views/src/reagent_data_views/server/core.clj +++ b/reagent-data-views/src/reagent_data_views/server/core.clj @@ -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))) \ No newline at end of file +(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))) diff --git a/reagent-data-views/src/reagent_data_views/utils.cljc b/reagent-data-views/src/reagent_data_views/utils.cljc new file mode 100644 index 0000000..56e6734 --- /dev/null +++ b/reagent-data-views/src/reagent_data_views/utils.cljc @@ -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/")))