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