initial commit using test application code as the base & comments added

This commit is contained in:
Gered 2014-12-24 19:19:43 -05:00
commit 7c2350f136
7 changed files with 352 additions and 0 deletions

17
.gitignore vendored Normal file
View 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
View 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
View 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
View 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"])

View 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)))

View 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)}))))

View 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))