Sync state using diffs instead of events
This commit is contained in:
parent
f5ac6cbf26
commit
becfb42627
17 changed files with 216 additions and 330 deletions
14
project.clj
14
project.clj
|
@ -21,18 +21,26 @@
|
|||
:profiles {:uberjar {:aot :all}}
|
||||
:min-lein-version "2.1.2"
|
||||
:plugins [[lein-cljsbuild "1.0.3"]
|
||||
[lein-ring "0.8.6"]]
|
||||
:cljsbuild {:builds {:dev {:source-paths ["src/cljs"]
|
||||
[lein-ring "0.8.6"]
|
||||
[com.keminglabs/cljx "0.4.0"]]
|
||||
:cljsbuild {:builds {:dev {:source-paths ["src/cljs" "target/generated/cljs"]
|
||||
:compiler {:output-dir "public/js/out"
|
||||
:output-to "public/js/grub.js"
|
||||
:optimizations :none
|
||||
:source-map true}}
|
||||
:prod {:source-paths ["src/cljs"]
|
||||
:prod {:source-paths ["src/cljs" "target/generated/cljs"]
|
||||
:compiler {:output-to "public/js/grub.min.js"
|
||||
:optimizations :advanced
|
||||
:pretty-print false
|
||||
:preamble ["react/react.min.js"]
|
||||
:externs ["react/externs/react.js"]}}}}
|
||||
:cljx {:builds [{:source-paths ["src/cljx"]
|
||||
:output-path "target/classes"
|
||||
:rules :clj}
|
||||
{:source-paths ["src/cljx"]
|
||||
:output-path "target/generated/cljs"
|
||||
:rules :cljs}]}
|
||||
:hooks [cljx.hooks]
|
||||
:source-paths ["src/clj" "src/test"]
|
||||
:test-paths ["spec/clj"]
|
||||
:ring {:handler grub.core/app}
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
(:require [grub.websocket :as ws]
|
||||
[grub.db :as db]
|
||||
[grub.test.integration.core :as integration-test]
|
||||
[ring.middleware.reload :as reload]
|
||||
[grub.state :as state]
|
||||
[ring.middleware.file :as file]
|
||||
[ring.util.response :as resp]
|
||||
[compojure.core :refer [defroutes GET POST]]
|
||||
[compojure.handler :as handler]
|
||||
[compojure.route :as route]
|
||||
[org.httpkit.server :as httpkit]
|
||||
[clojure.core.async :as a :refer [<! >! chan go]]
|
||||
[hiccup
|
||||
[page :refer [html5]]
|
||||
[page :refer [include-js include-css]]]
|
||||
|
@ -41,40 +42,45 @@
|
|||
|
||||
(def index-page (atom dev-index-page))
|
||||
|
||||
(defn websocket-handler [request]
|
||||
(when (:websocket? request)
|
||||
(httpkit/with-channel request ws-channel
|
||||
(let [to-client (chan)
|
||||
from-client (chan)]
|
||||
(ws/add-client! ws-channel to-client from-client)
|
||||
(state/add-client! to-client from-client)))))
|
||||
|
||||
(defroutes routes
|
||||
(GET "/" [] ws/websocket-handler)
|
||||
(GET "/" [] websocket-handler)
|
||||
(GET "/" [] @index-page)
|
||||
(GET "*/src/cljs/grub/:file" [file] (resp/file-response file {:root "src/cljs/grub"}))
|
||||
(GET "/js/public/js/:file" [file] (resp/redirect (str "/js/" file)))
|
||||
(route/files "/")
|
||||
(route/not-found "<p>Page not found.</p>"))
|
||||
|
||||
(def app
|
||||
(let [dev? true]
|
||||
(if dev?
|
||||
(reload/wrap-reload (handler/site #'routes) {:dirs ["src/clj"]})
|
||||
(handler/site routes))))
|
||||
|
||||
(def default-port 3000)
|
||||
|
||||
(defn start-server [port]
|
||||
(httpkit/run-server app {:port port}))
|
||||
(httpkit/run-server (handler/site routes) {:port port}))
|
||||
|
||||
(defn run-integration-test []
|
||||
(let [stop-server (start-server integration-test/server-port)]
|
||||
(println "Starting integration test server on localhost:" integration-test/server-port)
|
||||
(integration-test/run)
|
||||
(stop-server)))
|
||||
|
||||
(defn start-production-server [{:keys [port mongo-url]}]
|
||||
(reset! index-page prod-index-page)
|
||||
(let [db-chan (db/connect-production-database mongo-url)]
|
||||
(ws/pass-received-events-to-clients-and-db db-chan)
|
||||
(let [to-db (chan)]
|
||||
(db/connect-production-database to-db mongo-url)
|
||||
(state/init to-db (db/get-current-grubs) (db/get-current-recipes))
|
||||
(println "Starting production server on localhost:" port)
|
||||
(start-server port)))
|
||||
|
||||
(defn start-development-server [{:keys [port]}]
|
||||
(let [db-chan (db/connect-development-database)]
|
||||
(ws/pass-received-events-to-clients-and-db db-chan)
|
||||
(let [to-db (chan)]
|
||||
(db/connect-development-database to-db)
|
||||
(state/init to-db (db/get-current-grubs) (db/get-current-recipes))
|
||||
(println "Starting development server on localhost:" port)
|
||||
(start-server port)))
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
(ns grub.db
|
||||
(:require [monger.core :as m]
|
||||
(:require [grub.util :as util]
|
||||
[monger.core :as m]
|
||||
[monger.collection :as mc]
|
||||
[monger.operators :as mo]
|
||||
[clojure.core.async :as a :refer [<! >! chan go]]))
|
||||
|
@ -8,6 +9,8 @@
|
|||
(def db (atom nil))
|
||||
(def grub-collection "grubs")
|
||||
(def recipe-collection "recipes")
|
||||
(def production-db "grub")
|
||||
(def development-db "grub-dev")
|
||||
|
||||
(defn clear-grubs []
|
||||
(mc/drop @db grub-collection))
|
||||
|
@ -19,80 +22,33 @@
|
|||
(clear-grubs)
|
||||
(clear-recipes))
|
||||
|
||||
(defmulti handle-event :event :default :unknown-event)
|
||||
|
||||
(defn insert-grub [event]
|
||||
(let [grub (-> event
|
||||
(select-keys [:id :grub :completed])
|
||||
(clojure.set/rename-keys {:id :_id}))]
|
||||
(mc/insert @db grub-collection grub)))
|
||||
|
||||
(defmethod handle-event :add-grub [event]
|
||||
(insert-grub event))
|
||||
|
||||
(defmethod handle-event :add-grub-list [event]
|
||||
(doseq [grub-event (:grubs event)]
|
||||
(insert-grub grub-event)))
|
||||
|
||||
(defmethod handle-event :complete-grub [event]
|
||||
(mc/update @db grub-collection
|
||||
{:_id (:id event)}
|
||||
{mo/$set {:completed true}}))
|
||||
|
||||
(defmethod handle-event :uncomplete-grub [event]
|
||||
(mc/update @db grub-collection
|
||||
{:_id (:id event)}
|
||||
{mo/$set {:completed false}}))
|
||||
|
||||
(defmethod handle-event :update-grub [event]
|
||||
(let [orig (mc/find-one-as-map @db grub-collection {:_id (:id event)})
|
||||
new (dissoc event :event-type :id)]
|
||||
(mc/update-by-id @db grub-collection (:id event) (merge orig new))))
|
||||
|
||||
(defmethod handle-event :clear-all-grubs [event]
|
||||
(clear-grubs))
|
||||
|
||||
(defmethod handle-event :remove-grub [event]
|
||||
(mc/remove-by-id @db grub-collection (:id event)))
|
||||
|
||||
(defmethod handle-event :add-recipe [event]
|
||||
(let [recipe (-> event
|
||||
(select-keys [:id :name :grubs])
|
||||
(clojure.set/rename-keys {:id :_id}))]
|
||||
(mc/insert @db recipe-collection recipe)))
|
||||
|
||||
(defmethod handle-event :update-recipe [event]
|
||||
(mc/update @db recipe-collection
|
||||
{:_id (:id event)}
|
||||
{mo/$set {:name (:name event) :grubs (:grubs event)}}))
|
||||
|
||||
(defmethod handle-event :remove-recipe [event]
|
||||
(mc/remove-by-id @db recipe-collection (:id event)))
|
||||
|
||||
(defmethod handle-event :unknown-event [event]
|
||||
(println "Cannot handle unknown event:" event))
|
||||
(defn update-db! [{:keys [grubs recipes]}]
|
||||
(let [deleted-grubs (:deleted grubs)
|
||||
updated-grubs (->> (:updated grubs)
|
||||
(seq)
|
||||
(map (fn [[k v]] (assoc v :_id v))))
|
||||
deleted-recipes (:deleted recipes)
|
||||
updated-recipes (->> (:updated recipes)
|
||||
(seq)
|
||||
(map (fn [[k v]] (assoc v :_id v))))]
|
||||
(doseq [g deleted-grubs]
|
||||
(mc/remove-by-id @db grub-collection g))
|
||||
(doseq [g updated-grubs]
|
||||
(mc/update-by-id @db grub-collection (:_id g) g {:upsert true}))
|
||||
(doseq [r deleted-recipes]
|
||||
(mc/remove-by-id @db recipe-collection r))
|
||||
(doseq [r updated-recipes]
|
||||
(mc/update-by-id @db recipe-collection (:_id r) r {:upsert true}))))
|
||||
|
||||
(defn get-current-grubs []
|
||||
(->> (mc/find-maps @db grub-collection)
|
||||
(sort-by :_id)
|
||||
(map #(select-keys % [:_id :grub :completed]))
|
||||
(map #(clojure.set/rename-keys % {:_id :id}))
|
||||
(vec)))
|
||||
(map #(clojure.set/rename-keys % {:_id :id}))))
|
||||
|
||||
(defn get-current-recipes []
|
||||
(->> (mc/find-maps @db recipe-collection)
|
||||
(sort-by :_id)
|
||||
(map #(select-keys % [:_id :name :grubs]))
|
||||
(map #(clojure.set/rename-keys % {:_id :id}))
|
||||
(vec)))
|
||||
|
||||
(def production-db "grub")
|
||||
(def development-db "grub-dev")
|
||||
|
||||
(defn handle-incoming-events [in]
|
||||
(a/go-loop [] (let [event (<! in)]
|
||||
(handle-event event)
|
||||
(recur))))
|
||||
(map #(clojure.set/rename-keys % {:_id :id}))))
|
||||
|
||||
(defn connect! [db-name mongo-url]
|
||||
(if mongo-url
|
||||
|
@ -101,16 +57,18 @@
|
|||
(do (println "Connected to mongo at localhost:" db-name)
|
||||
(m/connect))))
|
||||
|
||||
(defn connect-and-handle-events [db-name & [mongo-url]]
|
||||
(let [in (chan)]
|
||||
(handle-incoming-events in)
|
||||
(let [_conn (connect! db-name mongo-url)]
|
||||
(reset! conn _conn)
|
||||
(reset! db (m/get-db _conn db-name)))
|
||||
in))
|
||||
(defn connect-and-handle-events [to-db db-name & [mongo-url]]
|
||||
(a/go-loop []
|
||||
(if-let [diff (<! to-db)]
|
||||
(do (update-db! diff)
|
||||
(recur))
|
||||
(println "Database disconnected")))
|
||||
(let [_conn (connect! db-name mongo-url)]
|
||||
(reset! conn _conn)
|
||||
(reset! db (m/get-db _conn db-name))))
|
||||
|
||||
(defn connect-production-database [mongo-url]
|
||||
(connect-and-handle-events production-db mongo-url))
|
||||
(defn connect-production-database [to-db mongo-url]
|
||||
(connect-and-handle-events to-db production-db mongo-url))
|
||||
|
||||
(defn connect-development-database []
|
||||
(connect-and-handle-events development-db))
|
||||
(defn connect-development-database [to-db]
|
||||
(connect-and-handle-events to-db development-db))
|
||||
|
|
40
src/clj/grub/state.clj
Normal file
40
src/clj/grub/state.clj
Normal file
|
@ -0,0 +1,40 @@
|
|||
(ns grub.state
|
||||
(:require [grub.sync :as sync]
|
||||
[grub.util :as util]
|
||||
[clojure.core.async :as a :refer [<! >! chan go]]))
|
||||
|
||||
(def empty-state
|
||||
{:grubs {}
|
||||
:recipes {}})
|
||||
|
||||
(def state (atom empty-state))
|
||||
(def to-db (atom nil))
|
||||
(def to-all (chan))
|
||||
(def from-all (a/mult to-all))
|
||||
|
||||
(defn get-initial-state [grubs recipes]
|
||||
{:grubs (util/map-by-key :id grubs)
|
||||
:recipes (util/map-by-key :id recipes)})
|
||||
|
||||
(defn add-client! [to from]
|
||||
(let [client-id (java.util.UUID/randomUUID)]
|
||||
(println "New client id:" client-id)
|
||||
(a/go-loop []
|
||||
(when-let [diff (<! from)]
|
||||
(swap! state #(sync/patch-state % diff))
|
||||
(>! @to-db diff)
|
||||
(>! to-all {:diff diff :source-id client-id})
|
||||
(recur)))
|
||||
(let [all-diffs (chan)]
|
||||
(a/tap from-all all-diffs)
|
||||
(a/go-loop [] (if-let [{:keys [diff source-id] :as event} (<! all-diffs)]
|
||||
(do
|
||||
(when-not (= source-id client-id)
|
||||
(>! to diff))
|
||||
(recur))
|
||||
(a/untap from-all all-diffs))))
|
||||
(a/put! to (sync/diff-states empty-state @state))))
|
||||
|
||||
(defn init [_to-db grubs recipes]
|
||||
(reset! state (get-initial-state grubs recipes))
|
||||
(reset! to-db _to-db))
|
|
@ -3,83 +3,18 @@
|
|||
[org.httpkit.server :as httpkit]
|
||||
[clojure.core.async :as a :refer [<! >! chan go]]))
|
||||
|
||||
(def incoming-events (chan))
|
||||
|
||||
(def connected-clients (atom {}))
|
||||
|
||||
(def ws-channel-id-count (atom 0))
|
||||
|
||||
(defn get-unique-ws-id []
|
||||
(swap! ws-channel-id-count inc))
|
||||
|
||||
(defn add-connected-client! [ws-channel]
|
||||
(let [ws-channel-id (get-unique-ws-id)
|
||||
client-chan (chan)]
|
||||
(swap! connected-clients #(assoc % ws-channel-id client-chan))
|
||||
[ws-channel-id client-chan]))
|
||||
|
||||
(defn remove-connected-client! [status ws-channel ws-channel-id client-chan]
|
||||
(println "Client disconnected:"
|
||||
(.toString ws-channel)
|
||||
(str "(" ws-channel-id ")")
|
||||
"with status" status)
|
||||
(swap! connected-clients #(dissoc % ws-channel-id))
|
||||
(println (count @connected-clients) "client(s) still connected")
|
||||
(a/close! client-chan))
|
||||
|
||||
(defn send-current-grubs-and-recipes-to-client [client-chan]
|
||||
(let [add-grubs-event {:event :add-grub-list
|
||||
:grubs (db/get-current-grubs)}
|
||||
add-recipes-event {:event :add-recipe-list
|
||||
:recipes (db/get-current-recipes)}]
|
||||
(go (>! client-chan add-grubs-event)
|
||||
(>! client-chan add-recipes-event))))
|
||||
|
||||
(defn on-receive [raw-event ws-channel-id client-chan]
|
||||
(let [parsed-event (read-string raw-event)
|
||||
event (assoc parsed-event :ws-channel ws-channel-id)]
|
||||
(println "Received event" event)
|
||||
(if (= (:event event) :send-all-items)
|
||||
(send-current-grubs-and-recipes-to-client client-chan)
|
||||
(go (>! incoming-events event)))))
|
||||
|
||||
(defn forward-other-events-to-client [c ws-channel]
|
||||
(a/go-loop []
|
||||
(when-let [event (<! c)]
|
||||
(println "Send to client '" (str event) "'")
|
||||
(httpkit/send! ws-channel (str event))
|
||||
(recur))))
|
||||
|
||||
(defn set-up-new-connection [ws-channel]
|
||||
(let [[ws-channel-id client-chan] (add-connected-client! ws-channel)]
|
||||
(println "Client connected:" (.toString ws-channel) (str "(" ws-channel-id ")"))
|
||||
(println (count @connected-clients) "client(s) connected")
|
||||
(httpkit/on-close ws-channel #(remove-connected-client! % ws-channel ws-channel-id client-chan))
|
||||
(httpkit/on-receive ws-channel #(on-receive % ws-channel-id client-chan))
|
||||
(forward-other-events-to-client client-chan ws-channel)))
|
||||
|
||||
(defn websocket-handler [request]
|
||||
(when (:websocket? request)
|
||||
(httpkit/with-channel request channel (set-up-new-connection channel))))
|
||||
|
||||
(defn get-other-client-channels [my-ws-channel-id]
|
||||
(-> @connected-clients
|
||||
(dissoc my-ws-channel-id)
|
||||
(vals)))
|
||||
|
||||
(defn push-event-to-others [orig-event]
|
||||
(let [my-ws-channel-id (:ws-channel orig-event)
|
||||
event (dissoc orig-event :ws-channel)]
|
||||
(go (doseq [c (get-other-client-channels my-ws-channel-id)]
|
||||
(>! c event)))))
|
||||
|
||||
(defn pass-received-events-to-clients-and-db [db-chan]
|
||||
(let [in' (a/mult incoming-events)
|
||||
to-others (chan)
|
||||
to-database (chan)]
|
||||
(a/tap in' to-others)
|
||||
(a/tap in' to-database)
|
||||
(a/go-loop [] (let [event (<! to-others)]
|
||||
(push-event-to-others event)
|
||||
(recur)))
|
||||
(a/pipe to-database (a/map> #(dissoc % :ws-channel) db-chan))))
|
||||
(defn add-client! [ws-channel to from]
|
||||
(println "Client connected:" (.toString ws-channel))
|
||||
(httpkit/on-close ws-channel
|
||||
(fn [status]
|
||||
(println "Client disconnected:" (.toString ws-channel)
|
||||
"with status" status)
|
||||
(a/close! to)
|
||||
(a/close! from)))
|
||||
(httpkit/on-receive ws-channel #(a/put! from (read-string %)))
|
||||
(a/go-loop []
|
||||
(if-let [event (<! to)]
|
||||
(do
|
||||
(httpkit/send! ws-channel (str event))
|
||||
(recur))
|
||||
(httpkit/close ws-channel))))
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
(ns grub.core
|
||||
(:require [grub.state :as state]
|
||||
[grub.websocket :as ws]
|
||||
[grub.view.app :as view]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go]]))
|
||||
(:require-macros [grub.macros :refer [log logs]]))
|
||||
|
||||
(defn wire-channels-together []
|
||||
(defn init-app []
|
||||
(view/render-app state/app-state)
|
||||
(let [to-remote (chan)
|
||||
to-state (chan)
|
||||
from-remote (ws/get-remote-chan to-remote)
|
||||
from-state (state/update-state-and-render to-state)]
|
||||
from-state (state/update-state-on-event! to-state)]
|
||||
(a/pipe from-remote to-state)
|
||||
(a/pipe from-state to-remote)))
|
||||
|
||||
(wire-channels-together)
|
||||
(init-app)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(ns grub.state
|
||||
(:require [grub.view.app :as view]
|
||||
(:require [grub.sync :as sync]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
@ -7,70 +7,15 @@
|
|||
(def app-state (atom {:grubs {}
|
||||
:recipes {}}))
|
||||
|
||||
(defmulti handle-event (fn [event state] (:event event))
|
||||
:default :unknown-event)
|
||||
|
||||
(defmethod handle-event :unknown-event [event state]
|
||||
state)
|
||||
|
||||
(defn new-grub [id grub completed]
|
||||
{:id id :grub grub :completed completed})
|
||||
|
||||
(defmethod handle-event :add-grub [event state]
|
||||
(let [grub (new-grub (:id event) (:grub event) (:completed event))]
|
||||
(assoc-in state [:grubs (:id grub)] grub)))
|
||||
|
||||
(defn map-by-key [key coll]
|
||||
(->> coll
|
||||
(map (fn [a] [(get a key) a]))
|
||||
(into {})))
|
||||
|
||||
(defmethod handle-event :add-grub-list [event state]
|
||||
(->> event
|
||||
:grubs
|
||||
(map-by-key :id)
|
||||
(merge (:grubs state))
|
||||
(assoc state :grubs)))
|
||||
|
||||
(defmethod handle-event :update-grub [event state]
|
||||
(let [new-grub-info (dissoc event :event-type)
|
||||
orig-grub (get-in state [:grubs (:id event)])]
|
||||
(assoc-in state [:grubs (:id event)] (merge orig-grub new-grub-info))))
|
||||
|
||||
(defmethod handle-event :clear-all-grubs [event state]
|
||||
(assoc state :grubs {}))
|
||||
|
||||
(defmethod handle-event :remove-grub [event state]
|
||||
(assoc state :grubs (dissoc (:grubs state) (:id event))))
|
||||
|
||||
(defn new-recipe [id name grubs]
|
||||
{:id id :name name :grubs grubs})
|
||||
|
||||
(defmethod handle-event :add-recipe [event state]
|
||||
(let [recipe (new-recipe (:id event) (:name event) (:grubs event))]
|
||||
(assoc-in state [:recipes (:id recipe)] recipe)))
|
||||
|
||||
(defmethod handle-event :add-recipe-list [event state]
|
||||
(->> event
|
||||
:recipes
|
||||
(map-by-key :id)
|
||||
(merge (:recipes state))
|
||||
(assoc state :recipes)))
|
||||
|
||||
(defmethod handle-event :update-recipe [event state]
|
||||
(-> state
|
||||
(assoc-in [:recipes (:id event) :name] (:name event))
|
||||
(assoc-in [:recipes (:id event) :grubs] (:grubs event))))
|
||||
|
||||
(defmethod handle-event :remove-recipe [event state]
|
||||
(assoc state :recipes (dissoc (:recipes state) (:id event))))
|
||||
|
||||
(defn update-state-and-render [remote]
|
||||
(view/render-app app-state)
|
||||
(defn update-state-on-event! [in]
|
||||
(let [out (chan)]
|
||||
(add-watch app-state :app-state
|
||||
(fn [key ref old new]
|
||||
(when-not (= old new)
|
||||
(let [diff (sync/diff-states old new)]
|
||||
(a/put! out diff)))))
|
||||
(go-loop []
|
||||
(let [event (<! remote)
|
||||
new-state (handle-event event @app-state)]
|
||||
(reset! app-state new-state)
|
||||
(when-let [diff (<! in)]
|
||||
(swap! app-state #(sync/patch-state % diff))
|
||||
(recur)))
|
||||
out))
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
(ns grub.view.app
|
||||
(:require [om.core :as om :include-macros true]
|
||||
[sablono.core :refer-macros [html]]
|
||||
[cljs.core.async :as a :refer [<! put! chan]]
|
||||
[grub.view.dom :as dom]
|
||||
(:require [grub.view.dom :as dom]
|
||||
[grub.view.grub-list :as grub-list]
|
||||
[grub.view.recipe-list :as recipe-list])
|
||||
[grub.view.recipe-list :as recipe-list]
|
||||
[om.core :as om :include-macros true]
|
||||
[sablono.core :refer-macros [html]]
|
||||
[cljs.core.async :as a :refer [<! put! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
(ns grub.view.grub
|
||||
(:require [om.core :as om :include-macros true]
|
||||
(:require [grub.view.dom :as dom]
|
||||
[om.core :as om :include-macros true]
|
||||
[sablono.core :as html :refer-macros [html]]
|
||||
[cljs.core.async :as a :refer [<! put! chan]]
|
||||
[grub.view.dom :as dom]
|
||||
[cljs-uuid.core :as uuid])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
@ -46,6 +46,10 @@
|
|||
:grub-text text
|
||||
:unmounted false})
|
||||
|
||||
om/IWillReceiveProps
|
||||
(will-receive-props [this {:keys [text]}]
|
||||
(om/set-state! owner :grub-text text))
|
||||
|
||||
om/IRenderState
|
||||
(render-state [_ {:keys [edit-state] :as state}]
|
||||
(html
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
(ns grub.view.grub-list
|
||||
(:require [om.core :as om :include-macros true]
|
||||
(:require [grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view]
|
||||
[om.core :as om :include-macros true]
|
||||
[sablono.core :as html :refer-macros [html]]
|
||||
[cljs.core.async :as a :refer [<! chan]]
|
||||
[grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view])
|
||||
[cljs.core.async :as a :refer [<! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
(defn get-grub-ingredient [grub]
|
||||
(when-not (nil? (:grub grub))
|
||||
(let [text (clojure.string/lower-case (:grub grub))
|
||||
(when-not (nil? (:text grub))
|
||||
(let [text (clojure.string/lower-case (:text grub))
|
||||
match (re-find #"[a-z]{3}.*$" text)]
|
||||
match)))
|
||||
|
||||
(defn sort-grubs [grubs]
|
||||
(sort-by (juxt :completed get-grub-ingredient :grub) (vals grubs)))
|
||||
(->> grubs
|
||||
(vals)
|
||||
(sort-by (juxt :completed get-grub-ingredient :text))))
|
||||
|
||||
(defn add-grub [owner grubs new-grub-text]
|
||||
(when (not (empty? new-grub-text))
|
||||
|
@ -47,7 +49,7 @@
|
|||
{:id "add-grub-btn"
|
||||
:type "button"
|
||||
:on-click #(add-grub owner grubs new-grub-text)}
|
||||
[:span.glyphicon.glyphicon-plus]]]
|
||||
[:span.glyphicon.glyphicon-plus#add-grub-btn]]]
|
||||
[:ul#grub-list.list-group
|
||||
(for [grub (sort-grubs grubs)]
|
||||
(om/build grub-view/view grub {:key :id :opts {:remove-ch remove-grub-ch}}))]
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
(ns grub.view.recipe
|
||||
(:require [om.core :as om :include-macros true]
|
||||
(:require [grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view]
|
||||
[grub.util :as util]
|
||||
[om.core :as om :include-macros true]
|
||||
[sablono.core :as html :refer-macros [html]]
|
||||
[cljs.core.async :as a :refer [<! put! chan]]
|
||||
[cljs-uuid.core :as uuid]
|
||||
[grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view])
|
||||
[cljs-uuid.core :as uuid])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
|
@ -19,14 +20,9 @@
|
|||
(map grub-view/new-grub)
|
||||
(into [])))
|
||||
|
||||
(defn map-by-key [key coll]
|
||||
(->> coll
|
||||
(map (fn [a] [(get a key) a]))
|
||||
(into {})))
|
||||
|
||||
(defn add-grubs [add-grubs-ch grubs-str]
|
||||
(let [grubs (parse-grubs-from-str grubs-str)
|
||||
grubs-map (map-by-key :id grubs)]
|
||||
grubs-map (util/map-by-key :id grubs)]
|
||||
(put! add-grubs-ch grubs-map)))
|
||||
|
||||
(def transitions
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
(ns grub.view.recipe-list
|
||||
(:require [om.core :as om :include-macros true]
|
||||
(:require [grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view]
|
||||
[grub.view.recipe :as recipe]
|
||||
[om.core :as om :include-macros true]
|
||||
[sablono.core :as html :refer-macros [html]]
|
||||
[cljs.core.async :as a :refer [<! chan]]
|
||||
[cljs-uuid.core :as uuid]
|
||||
[grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view]
|
||||
[grub.view.recipe :as recipe])
|
||||
[cljs-uuid.core :as uuid])
|
||||
(:require-macros [grub.macros :refer [log logs]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
|
@ -68,7 +68,7 @@
|
|||
{:type "button"
|
||||
:ref :save-btn
|
||||
:on-click #(transition-state owner :save)}
|
||||
[:span.glyphicon.glyphicon-ok]]]]))
|
||||
[:span.glyphicon.glyphicon-ok#save-recipe-btn]]]]))
|
||||
|
||||
om/IWillMount
|
||||
(will-mount [_]
|
||||
|
|
|
@ -40,6 +40,5 @@
|
|||
(.listen handler @websocket* goog.net.WebSocket.EventType.CLOSED #(log "Closed:" %) false)
|
||||
(.listen handler @websocket* goog.net.WebSocket.EventType.ERROR #(log "Error:" %) false)
|
||||
(send-outgoing-events to-remote)
|
||||
(go (>! to-remote {:event :send-all-items}))
|
||||
(.open @websocket* server-url)
|
||||
remote-events))
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
(ns grub.sync
|
||||
(:require [clojure.data :as data]
|
||||
[clojure.pprint :as pprint :refer [pprint]]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defn deleted [a b]
|
||||
|
@ -11,7 +10,7 @@
|
|||
|
||||
(defn diff-maps [a b]
|
||||
{:deleted (deleted a b)
|
||||
:updated (changed a b)})
|
||||
:updated (updated a b)})
|
||||
|
||||
(defn diff-states [prev next]
|
||||
(->> prev
|
||||
|
@ -29,4 +28,3 @@
|
|||
(keys)
|
||||
(map (fn [k] [k (patch-map (k state) (k diff))]))
|
||||
(into {})))
|
||||
|
7
src/cljx/grub/util.cljx
Normal file
7
src/cljx/grub/util.cljx
Normal file
|
@ -0,0 +1,7 @@
|
|||
(ns grub.util)
|
||||
|
||||
(defn map-by-key [key coll]
|
||||
(->> coll
|
||||
(map (fn [a] [(get a key) a]))
|
||||
(into {})))
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
(ns grub.test.integration.core
|
||||
(:require [grub.db :as db]
|
||||
[grub.websocket :as ws]
|
||||
[grub.state :as state]
|
||||
[clojure.core.async :as a :refer [<! >! chan go]]
|
||||
[clj-webdriver.taxi :as taxi]
|
||||
[clj-webdriver.core :as webdriver]
|
||||
[clojure.test :as test]))
|
||||
|
@ -20,7 +22,7 @@
|
|||
|
||||
(defn add-grub [driver grub-text]
|
||||
(taxi/input-text driver "#add-grub-input" grub-text)
|
||||
(taxi/click driver {:text "Add"}))
|
||||
(taxi/click driver "#add-grub-btn"))
|
||||
|
||||
(defn test-grubs-saved-to-server [url driver]
|
||||
(taxi/to driver url)
|
||||
|
@ -31,7 +33,7 @@
|
|||
(taxi/refresh driver)
|
||||
(Thread/sleep 200)
|
||||
(doseq [grub grubs]
|
||||
(test/is (taxi/find-element driver {:text grub})
|
||||
(test/is (taxi/find-element driver {:value grub})
|
||||
"Previously added grubs should be loaded on refresh")))
|
||||
(db/clear-grubs))
|
||||
|
||||
|
@ -42,7 +44,7 @@
|
|||
(doseq [grub grubs]
|
||||
(add-grub driver1 grub))
|
||||
(doseq [grub grubs]
|
||||
(test/is (taxi/find-element driver2 {:text grub})
|
||||
(test/is (taxi/find-element driver2 {:value grub})
|
||||
"Added grubs should appear in other browser"))))
|
||||
|
||||
(defn get-rand-recipe []
|
||||
|
@ -53,7 +55,7 @@
|
|||
(taxi/click driver "#new-recipe-name")
|
||||
(taxi/input-text driver "#new-recipe-name" name)
|
||||
(taxi/input-text driver "#new-recipe-grubs" grubs)
|
||||
(taxi/click driver {:text "Done"}))
|
||||
(taxi/click driver "#save-recipe-btn"))
|
||||
|
||||
(defn test-added-recipes-sync [url driver1 driver2]
|
||||
(taxi/to driver1 url)
|
||||
|
@ -71,9 +73,9 @@
|
|||
(test-added-recipes-sync site-url driver1 driver2))
|
||||
|
||||
(defn start-db-and-websocket-server! []
|
||||
(let [db-chan (db/connect-and-handle-events "grub-integration-test")]
|
||||
(db/clear-all)
|
||||
(ws/pass-received-events-to-clients-and-db db-chan)))
|
||||
(let [to-db (chan)]
|
||||
(db/connect-and-handle-events to-db "grub-integration-test")
|
||||
(state/init to-db (db/get-current-grubs) (db/get-current-recipes))))
|
||||
|
||||
(defn run []
|
||||
(println "Starting integration test")
|
||||
|
|
|
@ -4,73 +4,58 @@
|
|||
|
||||
(def server-state
|
||||
{:grubs
|
||||
{"grub-same" {:id "grub-same",
|
||||
:completed false,
|
||||
:grub "3 garlic cloves"}
|
||||
"grub-completed" {:id "grub-completed",
|
||||
:completed false,
|
||||
:grub "2 tomatoes"}
|
||||
"grub-updated" {:id "grub-updated",
|
||||
:completed false,
|
||||
:grub "BBQ sauce"}
|
||||
"grub-deleted" {:id "grub-deleted"
|
||||
:completed true
|
||||
:grub "diapers"}}
|
||||
{"grub-same" {:completed false
|
||||
:text "3 garlic cloves"}
|
||||
"grub-completed" {:completed false
|
||||
:text "2 tomatoes"}
|
||||
"grub-updated" {:completed false
|
||||
:text "BBQ sauce"}
|
||||
"grub-deleted" {:completed true
|
||||
:text "diapers"}}
|
||||
:recipes
|
||||
{"recipe-same" {:id "recipe-same"
|
||||
:grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread"
|
||||
{"recipe-same" {:grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread"
|
||||
:name "Blue Cheese Soup"}
|
||||
"recipe-updated" {:id "recipe-updated"
|
||||
:grubs "450 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n350 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n3 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"
|
||||
"recipe-updated" {:grubs "450 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n350 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n3 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"
|
||||
:name "Beef Stew"}
|
||||
"recipe-deleted" {:id "recipe-deleted"
|
||||
:grubs "8 slices rye bread\n400 g chicken breast\nBBQ sauce\nketchup\nmustard\nbutter\n1 package rocket\n4 tomatoes\n2 red onions\n1 bottle Coca Cola"
|
||||
"recipe-deleted" {:grubs "8 slices rye bread\n400 g chicken breast\nBBQ sauce\nketchup\nmustard\nbutter\n1 package rocket\n4 tomatoes\n2 red onions\n1 bottle Coca Cola"
|
||||
:name "Chickenburgers"}}})
|
||||
|
||||
(def client-state
|
||||
{:grubs
|
||||
{"grub-same" {:id "grub-same",
|
||||
:completed false,
|
||||
:grub "3 garlic cloves"}
|
||||
"grub-completed" {:id "grub-completed",
|
||||
:completed true,
|
||||
:grub "2 tomatoes"}
|
||||
"grub-updated" {:id "grub-updated",
|
||||
:completed false,
|
||||
:grub "Ketchup"}
|
||||
"grub-added" {:id "grub-added"
|
||||
:completed false
|
||||
:grub "Toothpaste"}}
|
||||
{"grub-same" {:completed false,
|
||||
:text "3 garlic cloves"}
|
||||
"grub-completed" {:completed true,
|
||||
:text "2 tomatoes"}
|
||||
"grub-updated" {:completed false,
|
||||
:text "Ketchup"}
|
||||
"grub-added" {:completed false
|
||||
:text "Toothpaste"}}
|
||||
:recipes
|
||||
{"recipe-same" {:id "recipe-same"
|
||||
:grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread"
|
||||
{"recipe-same" {:grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread"
|
||||
:name "Blue Cheese Soup"}
|
||||
"recipe-updated" {:id "recipe-updated"
|
||||
:grubs "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"
|
||||
"recipe-updated" {:grubs "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"
|
||||
:name "Beef Stew"}
|
||||
"recipe-added" {:id "recipe-added"
|
||||
:grubs "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"
|
||||
"recipe-added" {:grubs "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"
|
||||
:name "Burgers"}}})
|
||||
|
||||
(def expected-diff
|
||||
{:recipes
|
||||
{:deleted #{"recipe-deleted"},
|
||||
{:deleted #{"recipe-deleted"}
|
||||
:updated
|
||||
{"recipe-added"
|
||||
{:name "Burgers",
|
||||
:id "recipe-added",
|
||||
{:name "Burgers"
|
||||
:grubs
|
||||
"400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"},
|
||||
"400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"}
|
||||
"recipe-updated"
|
||||
{:grubs
|
||||
"300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"}}},
|
||||
"300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"}}}
|
||||
:grubs
|
||||
{:deleted #{"grub-deleted"},
|
||||
{:deleted #{"grub-deleted"}
|
||||
:updated
|
||||
{"grub-completed" {:completed true},
|
||||
"grub-updated" {:grub "Ketchup"},
|
||||
{"grub-completed" {:completed true}
|
||||
"grub-updated" {:text "Ketchup"}
|
||||
"grub-added"
|
||||
{:completed false, :grub "Toothpaste", :id "grub-added"}}}})
|
||||
{:completed false :text "Toothpaste"}}}})
|
||||
|
||||
(deftest diffing
|
||||
(is (= expected-diff (sync/diff-states server-state client-state))))
|
||||
|
|
Loading…
Reference in a new issue