Sync state using diffs instead of events

This commit is contained in:
Nicholas Kariniemi 2014-08-10 00:26:35 +03:00
parent f5ac6cbf26
commit becfb42627
17 changed files with 216 additions and 330 deletions

View file

@ -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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]]))

View file

@ -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

View file

@ -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}}))]

View file

@ -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

View file

@ -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 [_]

View file

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

View file

@ -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
View file

@ -0,0 +1,7 @@
(ns grub.util)
(defn map-by-key [key coll]
(->> coll
(map (fn [a] [(get a key) a]))
(into {})))

View file

@ -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")

View file

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