initial commit using test application code as the base & comments added
This commit is contained in:
commit
7c2350f136
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.DS_Store
|
||||||
|
/target
|
||||||
|
/classes
|
||||||
|
/checkouts
|
||||||
|
pom.xml
|
||||||
|
pom.xml.asc
|
||||||
|
*.jar
|
||||||
|
*.class
|
||||||
|
/.lein-*
|
||||||
|
/.nrepl-port
|
||||||
|
.settings/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Gered King
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
13
README.md
Normal file
13
README.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# reagent-data-views
|
||||||
|
|
||||||
|
A Clojure library designed to ... well, that part is up to you.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
FIXME
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2014 Gered King
|
||||||
|
|
||||||
|
Distributed under the the MIT License. See LICENSE for more details.
|
15
project.clj
Normal file
15
project.clj
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
(defproject reagent-data-views "0.1.0-SNAPSHOT"
|
||||||
|
:description "Support for Reagent components that get pushed realtime database updates from the server."
|
||||||
|
:url "https://github.com/gered/reagent-data-views"
|
||||||
|
:license {:name "MIT License"
|
||||||
|
:url "http://opensource.org/licenses/MIT"}
|
||||||
|
|
||||||
|
:dependencies [[org.clojure/clojure "1.6.0"]
|
||||||
|
[org.clojure/clojurescript "0.0-2371" :scope "provided"]
|
||||||
|
[org.clojure/core.async "0.1.346.0-17112a-alpha"]
|
||||||
|
[reagent "0.5.0-alpha" :scope "provided"]
|
||||||
|
[clj-browserchannel-messaging "0.0.2"]
|
||||||
|
[views "0.4.9"]]
|
||||||
|
|
||||||
|
:source-paths ["src/clj"]
|
||||||
|
:resource-paths ["src/cljs"])
|
130
src/clj/reagent_data_views/server/core.clj
Normal file
130
src/clj/reagent_data_views/server/core.clj
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
(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]]))
|
||||||
|
|
||||||
|
(defonce
|
||||||
|
^{:doc "The Views configuration. Used with functions like vexec!, etc."}
|
||||||
|
views-config (atom nil))
|
||||||
|
|
||||||
|
(defn- send-deltas [client-id topic body]
|
||||||
|
(browserchannel/send client-id topic body))
|
||||||
|
|
||||||
|
(defn- no-filters? [templates]
|
||||||
|
(not (some
|
||||||
|
(fn [[_ {:keys [filter-fn]}]]
|
||||||
|
(not (nil? filter-fn)))
|
||||||
|
templates)))
|
||||||
|
|
||||||
|
(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
|
||||||
|
:browserchannel-session-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 :browserchannel-session-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 :persistence persistence)
|
||||||
|
(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 [browserchannel-session-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 @views-config) browserchannel-session-id)))
|
||||||
|
(handler browserchannel-session-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 [persistence (:persistence @views-config)
|
||||||
|
namespace (or (:namespace @views-config) :default-ns)]
|
||||||
|
(subscriptions persistence namespace view-sigs)))
|
24
src/cljs/reagent_data_views/client/component.clj
Normal file
24
src/cljs/reagent_data_views/client/component.clj
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
(ns reagent-data-views.client.component)
|
||||||
|
|
||||||
|
(defmacro def-views-component
|
||||||
|
[component-name args view-sigs & body]
|
||||||
|
`(defn ~component-name ~args
|
||||||
|
(let [gen-view-sigs# (fn ~args ~view-sigs)
|
||||||
|
view-sigs-atom# (atom nil)]
|
||||||
|
(reagent.core/create-class
|
||||||
|
{:component-will-mount
|
||||||
|
(fn [this#]
|
||||||
|
(reset! view-sigs-atom# (apply gen-view-sigs# (rest (reagent.core/argv this#))))
|
||||||
|
(reagent-data-views.client.core/subscribe! (deref view-sigs-atom#)))
|
||||||
|
|
||||||
|
:component-will-unmount
|
||||||
|
(fn [this#]
|
||||||
|
(reagent-data-views.client.core/unsubscribe! (deref view-sigs-atom#)))
|
||||||
|
|
||||||
|
:component-did-update
|
||||||
|
(fn [this# old-argv#]
|
||||||
|
(reagent-data-views.client.core/update-view-component-sigs this# gen-view-sigs# view-sigs-atom#))
|
||||||
|
|
||||||
|
:component-function
|
||||||
|
(fn ~args
|
||||||
|
~@body)}))))
|
132
src/cljs/reagent_data_views/client/core.cljs
Normal file
132
src/cljs/reagent_data_views/client/core.cljs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
(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.
|
||||||
|
|
||||||
|
(defonce view-data (r/atom {}))
|
||||||
|
|
||||||
|
; return items in b that don't exist in a
|
||||||
|
(defn- diff [a b]
|
||||||
|
(vec
|
||||||
|
(reduce
|
||||||
|
(fn [item-a item-b]
|
||||||
|
(remove #(= % item-b) item-a))
|
||||||
|
a b)))
|
||||||
|
|
||||||
|
(declare update-subscriptions!)
|
||||||
|
|
||||||
|
(defn update-view-component-sigs
|
||||||
|
"Not intended to be used outside of the def-view-component macro's
|
||||||
|
internal functionality."
|
||||||
|
[owner view-sig-gen-fn view-sigs-atom]
|
||||||
|
(let [new-args (rest (r/argv owner))
|
||||||
|
old-sigs @view-sigs-atom
|
||||||
|
new-sigs (apply view-sig-gen-fn new-args)]
|
||||||
|
(when (not= old-sigs new-sigs)
|
||||||
|
(let [sigs-to-sub (diff new-sigs old-sigs)
|
||||||
|
sigs-to-unsub (diff old-sigs new-sigs)]
|
||||||
|
(update-subscriptions! sigs-to-sub sigs-to-unsub)
|
||||||
|
(if (not= old-sigs new-sigs)
|
||||||
|
(reset! view-sigs-atom new-sigs))))))
|
||||||
|
|
||||||
|
(defn get-view-sig-cursor
|
||||||
|
"Returns a Reagent cursor that can be used to access the data for this view.
|
||||||
|
NOTE: This is intended to be used in a read-only manner. Using this cursor
|
||||||
|
to change the data will *not* propagate to the server or any other
|
||||||
|
clients currently subscribed to this view."
|
||||||
|
[view-sig]
|
||||||
|
(r/cursor [view-sig] view-data))
|
||||||
|
|
||||||
|
(defn- get-views-by-name [view-name]
|
||||||
|
(filter
|
||||||
|
(fn [[view-sig _]]
|
||||||
|
(= view-name (first view-sig)))
|
||||||
|
@view-data))
|
||||||
|
|
||||||
|
(defn get-view-cursor
|
||||||
|
"Returns a Reagent cursor that can be used to access the data for the view-sig
|
||||||
|
with the specified name. If there is currently multiple subscriptions to views
|
||||||
|
with the same name (but different arguments), this will throw an error.
|
||||||
|
NOTE: This is intended to be used in a read-only manner. Using this cursor
|
||||||
|
to change the data will *not* propagate to the server or any other
|
||||||
|
clients currently subscribed to this view."
|
||||||
|
[view-name]
|
||||||
|
(let [view-sig (get-views-by-name view-name)]
|
||||||
|
(if (> (count view-sig) 1)
|
||||||
|
(throw (str "More then one view signature by the name \"" view-name "\" found."))
|
||||||
|
(get-view-sig-cursor (ffirst view-sig)))))
|
||||||
|
|
||||||
|
(defn- add-initial-view-data! [view-sig data]
|
||||||
|
(let [cursor (get-view-sig-cursor view-sig)]
|
||||||
|
(reset! cursor data)))
|
||||||
|
|
||||||
|
(defn- remove-view-data! [view-sig]
|
||||||
|
(swap! view-data dissoc 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 (get-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 unsubscribe!
|
||||||
|
"Unsubscribes from all of the specified view(s). No further updates from the
|
||||||
|
server will be received for these views and the latest data received from
|
||||||
|
the server is cleared."
|
||||||
|
[view-sigs]
|
||||||
|
(doseq [view-sig view-sigs]
|
||||||
|
(remove-view-data! view-sig)
|
||||||
|
(browserchannel/send :views.unsubscribe [view-sig])))
|
||||||
|
|
||||||
|
(defn subscribe!
|
||||||
|
"Subscribes to the specified view(s). Updates to the data on the server will
|
||||||
|
be automatically pushed out. Use get-data-cursor to read this data and
|
||||||
|
render it in any component(s)."
|
||||||
|
[view-sigs]
|
||||||
|
(doseq [view-sig view-sigs]
|
||||||
|
(add-initial-view-data! view-sig nil)
|
||||||
|
(browserchannel/send :views.subscribe [view-sig])))
|
||||||
|
|
||||||
|
(defn update-subscriptions!
|
||||||
|
"Unsubscribes from old-view-sigs and then subscribes to new-view-sigs. This
|
||||||
|
function should be used when one or more arguments to views that are currently
|
||||||
|
subscribed to have changed."
|
||||||
|
[new-view-sigs old-view-sigs]
|
||||||
|
(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))
|
Loading…
Reference in a new issue