Use timestamp tags instead of hashes
- Hashing was slow and we weren't using the hash properties much anyway
This commit is contained in:
parent
19ca650078
commit
8fe22b9a52
13 changed files with 385 additions and 269 deletions
|
@ -18,7 +18,8 @@
|
||||||
(defn start
|
(defn start
|
||||||
"Starts the current development system."
|
"Starts the current development system."
|
||||||
[]
|
[]
|
||||||
(alter-var-root #'system system/start system/dev-system))
|
(alter-var-root #'system (constantly system/dev-system))
|
||||||
|
(alter-var-root #'system system/start))
|
||||||
|
|
||||||
(defn stop
|
(defn stop
|
||||||
"Shuts down and destroys the current development system."
|
"Shuts down and destroys the current development system."
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
[clj-webdriver "0.6.1" :exclusions [org.clojure/core.cache]]
|
[clj-webdriver "0.6.1" :exclusions [org.clojure/core.cache]]
|
||||||
[sablono "0.2.17"]
|
[sablono "0.2.17"]
|
||||||
[cljs-uuid "0.0.4"]
|
[cljs-uuid "0.0.4"]
|
||||||
[net.polyc0l0r/hasch "0.2.3"]
|
|
||||||
[com.cognitect/transit-clj "0.8.259"]
|
[com.cognitect/transit-clj "0.8.259"]
|
||||||
[com.cognitect/transit-cljs "0.8.188"]]
|
[com.cognitect/transit-cljs "0.8.188"]]
|
||||||
:profiles {:uberjar {:aot :all}
|
:profiles {:uberjar {:aot :all}
|
||||||
|
|
|
@ -92,11 +92,11 @@
|
||||||
(let [{:keys [db conn]} (db/connect db-name)
|
(let [{:keys [db conn]} (db/connect db-name)
|
||||||
new-states (chan)
|
new-states (chan)
|
||||||
db-state (db/get-current-state db)
|
db-state (db/get-current-state db)
|
||||||
_ (reset! states (state/new-state (if db-state db-state (state/empty-state))))
|
_ (reset! states (state/new-states (if db-state db-state state/empty-state)))
|
||||||
stop-server (httpkit/run-server (make-handler system new-states) {:port port})]
|
stop-server (httpkit/run-server (make-handler system new-states) {:port port})]
|
||||||
(add-watch states :db (fn [_ _ old new]
|
(add-watch states :db (fn [_ _ old new]
|
||||||
(when-not (= old new)
|
(when-not (= old new)
|
||||||
(let [new-state (state/get-current-state new)]
|
(let [new-state (state/get-latest new)]
|
||||||
(a/put! new-states new-state)
|
(a/put! new-states new-state)
|
||||||
(db/update-db! db new-state)))))
|
(db/update-db! db new-state)))))
|
||||||
(println "Started server on localhost:" port)
|
(println "Started server on localhost:" port)
|
||||||
|
@ -106,6 +106,7 @@
|
||||||
:stop-server stop-server
|
:stop-server stop-server
|
||||||
:states states)))
|
:states states)))
|
||||||
|
|
||||||
|
|
||||||
(defn stop [{:keys [db-conn stop-server states] :as system}]
|
(defn stop [{:keys [db-conn stop-server states] :as system}]
|
||||||
(remove-watch states :db)
|
(remove-watch states :db)
|
||||||
(stop-server)
|
(stop-server)
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
agent-states (sync/sync-client! >remote events new-states states)]
|
agent-states (sync/sync-client! >remote events new-states states)]
|
||||||
(add-watch states :render (fn [_ _ old new]
|
(add-watch states :render (fn [_ _ old new]
|
||||||
(when-not (= old new)
|
(when-not (= old new)
|
||||||
(a/put! render-states (state/get-current-state new)))))
|
(a/put! render-states (state/get-latest new)))))
|
||||||
(assoc system
|
(assoc system
|
||||||
:ws ws
|
:ws ws
|
||||||
:channels {:new-states new-states
|
:channels {:new-states new-states
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
(ns grub.diff
|
(ns grub.diff
|
||||||
(:require [clojure.data :as data]
|
(:require [clojure.data :as data]
|
||||||
[clojure.set :as set]))
|
[clojure.set :as set]
|
||||||
|
[grub.tag :as tag]))
|
||||||
|
|
||||||
(defn deleted [a b]
|
(defn deleted [a b]
|
||||||
(set/difference (into #{} (keys a)) (into #{} (keys b))))
|
(set/difference (into #{} (keys a)) (into #{} (keys b))))
|
||||||
|
@ -9,22 +10,39 @@
|
||||||
(second (data/diff a b)))
|
(second (data/diff a b)))
|
||||||
|
|
||||||
(defn diff-maps [a b]
|
(defn diff-maps [a b]
|
||||||
{:deleted (deleted a b)
|
(when (and (map? a) (map? b))
|
||||||
:updated (updated a b)})
|
{:- (deleted a b)
|
||||||
|
:+ (updated a b)}))
|
||||||
|
|
||||||
(defn diff-states [prev next]
|
(defn diff-keys [prev next]
|
||||||
(->> prev
|
(->> prev
|
||||||
(keys)
|
(keys)
|
||||||
(map (fn [k] [k (diff-maps (k prev) (k next))]))
|
(map (fn [k] [k (diff-maps (k prev) (k next))]))
|
||||||
|
(filter #(not (nil? (second %))))
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
|
(defn diff-states [prev next]
|
||||||
|
(let [key-diffs (diff-keys prev next)]
|
||||||
|
(if (and (:tag prev) (:tag next))
|
||||||
|
(assoc key-diffs
|
||||||
|
:shadow-tag (:tag prev)
|
||||||
|
:tag (:tag next))
|
||||||
|
key-diffs)))
|
||||||
|
|
||||||
(defn patch-map [state diff]
|
(defn patch-map [state diff]
|
||||||
(-> state
|
(-> state
|
||||||
(#(apply dissoc % (into [] (:deleted diff))))
|
(#(apply dissoc % (into [] (:- diff))))
|
||||||
(#(merge-with merge % (:updated diff)))))
|
(#(merge-with merge % (:+ diff)))))
|
||||||
|
|
||||||
(defn patch-state [state diff]
|
(defn patch-state
|
||||||
(->> state
|
([state diff] (patch-state state diff false))
|
||||||
(keys)
|
([state diff use-diff-tag?]
|
||||||
(map (fn [k] [k (patch-map (k state) (k diff))]))
|
(let [patched (->> state
|
||||||
(into {})))
|
(keys)
|
||||||
|
(map (fn [k] [k (patch-map (k state) (k diff))]))
|
||||||
|
(into {}))]
|
||||||
|
(if use-diff-tag?
|
||||||
|
(assoc patched :tag (:tag diff))
|
||||||
|
(if (= state patched)
|
||||||
|
state
|
||||||
|
(assoc patched :tag (tag/new-tag)))))))
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
(ns grub.message)
|
|
||||||
|
|
||||||
(def full-sync-request {:type :full-sync-request})
|
|
||||||
|
|
||||||
(defn full-sync [state]
|
|
||||||
{:type :full-sync
|
|
||||||
:full-state state})
|
|
||||||
|
|
||||||
(defn diff-msg [diff hash]
|
|
||||||
{:type :diff
|
|
||||||
:diff diff
|
|
||||||
:hash hash})
|
|
|
@ -1,32 +1,31 @@
|
||||||
(ns grub.state
|
(ns grub.state
|
||||||
(:require [grub.diff :as diff]
|
(:require [grub.diff :as diff]
|
||||||
[grub.util :as util]
|
[grub.util :as util]
|
||||||
[hasch.core :as hasch]))
|
[grub.tag :as tag]))
|
||||||
|
|
||||||
(def num-history-states 20)
|
(def num-history-states 20)
|
||||||
|
|
||||||
(def empty-state {:grubs {} :recipes {}})
|
(def empty-state {:tag (tag/oldest-tag) :grubs {} :recipes {}})
|
||||||
(def empty-states [{:grubs {} :recipes {}}])
|
|
||||||
|
|
||||||
(defn new-state [state]
|
(defn new-states [state]
|
||||||
[{:hash (hasch/uuid state)
|
[(assoc state :tag (tag/new-tag))])
|
||||||
:state state}])
|
|
||||||
|
|
||||||
(defn get-current-state [states]
|
(defn get-latest [states]
|
||||||
(:state (last states)))
|
(last states))
|
||||||
|
|
||||||
(defn get-history-state [states hash]
|
(defn get-tagged [states tag]
|
||||||
(:state (first (filter #(= (:hash %) hash) states))))
|
(->> states
|
||||||
|
(filter #(= (:tag %) tag))
|
||||||
|
(first)))
|
||||||
|
|
||||||
(defn add-history-state [states new-state]
|
(defn add [states new-state]
|
||||||
(let [last-hash (:hash (last states))
|
(let [last-state (last states)]
|
||||||
new-hash (hasch/uuid new-state)]
|
(if (= last-state new-state)
|
||||||
(if (= last-hash new-hash)
|
|
||||||
states
|
states
|
||||||
(let [new-states (conj states {:hash new-hash :state new-state})]
|
(let [new-states (conj states (assoc new-state :tag (tag/new-tag)))]
|
||||||
(if (>= (count states) num-history-states)
|
(if (>= (count states) num-history-states)
|
||||||
(into [] (rest new-states))
|
(into [] (rest new-states))
|
||||||
new-states)))))
|
new-states)))))
|
||||||
|
|
||||||
(defn empty-diff? [diff]
|
(defn state= [a b]
|
||||||
(= diff {:recipes {:deleted #{}, :updated nil}, :grubs {:deleted #{}, :updated nil}}))
|
(= (dissoc a :tag) (dissoc b :tag)))
|
||||||
|
|
|
@ -1,51 +1,55 @@
|
||||||
(ns grub.sync
|
(ns grub.sync
|
||||||
(:require [grub.diff :as diff]
|
(:require [grub.diff :as diff]
|
||||||
[grub.message :as message]
|
|
||||||
[grub.state :as state]
|
[grub.state :as state]
|
||||||
[hasch.core :as hasch]
|
|
||||||
#+clj [clojure.core.async :as a :refer [<! >! chan go]]
|
#+clj [clojure.core.async :as a :refer [<! >! chan go]]
|
||||||
#+cljs [cljs.core.async :as a :refer [<! >! chan]])
|
#+cljs [cljs.core.async :as a :refer [<! >! chan]])
|
||||||
#+cljs (:require-macros [grub.macros :refer [log logs]]
|
#+cljs (:require-macros [grub.macros :refer [log logs]]
|
||||||
[cljs.core.async.macros :refer [go]]))
|
[cljs.core.async.macros :refer [go]]))
|
||||||
|
|
||||||
|
(def full-sync-request {:type :full-sync-request})
|
||||||
|
|
||||||
|
(defn full-sync [state]
|
||||||
|
{:type :full-sync
|
||||||
|
:full-state state})
|
||||||
|
|
||||||
(def empty-state state/empty-state)
|
(def empty-state state/empty-state)
|
||||||
|
|
||||||
(defn update-states [states diff]
|
(defn update-states [states diff]
|
||||||
(let [state (state/get-current-state states)
|
(let [state (state/get-latest states)
|
||||||
new-state (diff/patch-state state diff)]
|
new-state (diff/patch-state state diff)]
|
||||||
(state/add-history-state states new-state)))
|
(state/add states new-state)))
|
||||||
|
|
||||||
(defn diff-msg [shadow state]
|
(defn diff-msg [shadow state]
|
||||||
(let [diff (diff/diff-states shadow state)
|
(let [diff (diff/diff-states shadow state)]
|
||||||
hash (hasch/uuid shadow)]
|
{:type :diff
|
||||||
(message/diff-msg diff hash)))
|
:diff diff}))
|
||||||
|
|
||||||
(defmulti handle-event (fn [event] (:type event)))
|
(defmulti handle-event (fn [event] (:type event)))
|
||||||
|
|
||||||
(defmethod handle-event :diff [{:keys [hash diff states shadow client?]}]
|
(defmethod handle-event :diff [{:keys [diff states shadow client?]}]
|
||||||
(let [history-shadow (state/get-history-state @states hash)]
|
(let [history-shadow (state/get-tagged @states (:shadow-tag diff))]
|
||||||
(if history-shadow
|
(if history-shadow
|
||||||
(let [new-states (swap! states update-states diff)
|
(let [new-states (swap! states update-states diff)
|
||||||
new-state (state/get-current-state new-states)
|
new-state (state/get-latest new-states)
|
||||||
new-shadow (diff/patch-state history-shadow diff)]
|
new-shadow (diff/patch-state history-shadow diff true)]
|
||||||
{:out-event (when-not (state/empty-diff? diff)
|
{:out-event (when-not (state/state= history-shadow new-state)
|
||||||
(diff-msg new-shadow new-state))
|
(diff-msg new-shadow new-state))
|
||||||
:new-states new-states
|
:new-states new-states
|
||||||
:new-shadow new-shadow})
|
:new-shadow new-shadow})
|
||||||
(if client?
|
(if client?
|
||||||
{:out-event message/full-sync-request
|
{:out-event full-sync-request
|
||||||
:new-shadow shadow}
|
:new-shadow shadow}
|
||||||
(let [state (state/get-current-state @states)]
|
(let [state (state/get-latest @states)]
|
||||||
{:out-event (message/full-sync state)
|
{:out-event (full-sync state)
|
||||||
:new-shadow state})))))
|
:new-shadow state})))))
|
||||||
|
|
||||||
(defmethod handle-event :full-sync-request [{:keys [states]}]
|
(defmethod handle-event :full-sync-request [{:keys [states]}]
|
||||||
(let [state (state/get-current-state @states)]
|
(let [state (state/get-latest @states)]
|
||||||
{:new-shadow state
|
{:new-shadow state
|
||||||
:out-event (message/full-sync state)}))
|
:out-event (full-sync state)}))
|
||||||
|
|
||||||
(defmethod handle-event :full-sync [{:keys [full-state states]}]
|
(defmethod handle-event :full-sync [{:keys [full-state states]}]
|
||||||
(reset! states (state/new-state full-state))
|
(reset! states (state/new-states full-state))
|
||||||
{:new-shadow full-state})
|
{:new-shadow full-state})
|
||||||
|
|
||||||
(defmethod handle-event :default [msg]
|
(defmethod handle-event :default [msg]
|
||||||
|
@ -61,8 +65,8 @@
|
||||||
(let [[v c] (a/alts! [new-states events] :priority true)]
|
(let [[v c] (a/alts! [new-states events] :priority true)]
|
||||||
(cond (nil? v) nil ;; drop out of loop
|
(cond (nil? v) nil ;; drop out of loop
|
||||||
(= c new-states)
|
(= c new-states)
|
||||||
(do (when-not (= shadow v)
|
(do (when-not (state/state= shadow v)
|
||||||
(swap! states state/add-history-state v)
|
(swap! states state/add v)
|
||||||
(>! >remote (diff-msg shadow v)))
|
(>! >remote (diff-msg shadow v)))
|
||||||
(recur shadow))
|
(recur shadow))
|
||||||
(= c events)
|
(= c events)
|
||||||
|
@ -90,4 +94,4 @@
|
||||||
(>! new-states* v)
|
(>! new-states* v)
|
||||||
(recur))))
|
(recur))))
|
||||||
(make-client-agent >remote events new-states* states)
|
(make-client-agent >remote events new-states* states)
|
||||||
(a/put! >remote message/full-sync-request)))
|
(a/put! >remote full-sync-request)))
|
||||||
|
|
9
src/cljx/grub/tag.cljx
Normal file
9
src/cljx/grub/tag.cljx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
(ns grub.tag)
|
||||||
|
|
||||||
|
(defn new-tag []
|
||||||
|
#+clj (java.util.Date.)
|
||||||
|
#+cljs (js/Date.))
|
||||||
|
|
||||||
|
(defn oldest-tag []
|
||||||
|
#+clj (java.util.Date. 0)
|
||||||
|
#+cljs (js/Date. 0))
|
|
@ -2,18 +2,8 @@
|
||||||
(:require [grub.sync :as sync]
|
(:require [grub.sync :as sync]
|
||||||
[clojure.test :refer :all]
|
[clojure.test :refer :all]
|
||||||
[midje.sweet :refer :all]
|
[midje.sweet :refer :all]
|
||||||
[hasch.core :as hasch]
|
|
||||||
[clojure.core.async :as a :refer [<!! >!! chan go]]))
|
[clojure.core.async :as a :refer [<!! >!! chan go]]))
|
||||||
|
|
||||||
(defn hashed-states [& states]
|
|
||||||
(->> states
|
|
||||||
(map (fn [s] {:hash (hasch/uuid s)
|
|
||||||
:state s}))
|
|
||||||
(into [])))
|
|
||||||
|
|
||||||
(defn states-atom [& states]
|
|
||||||
(atom (apply hashed-states states)))
|
|
||||||
|
|
||||||
(defn <!!? [c]
|
(defn <!!? [c]
|
||||||
(let [[v p] (a/alts!! [c (a/timeout 100)])]
|
(let [[v p] (a/alts!! [c (a/timeout 100)])]
|
||||||
v))
|
v))
|
||||||
|
|
|
@ -1,73 +1,76 @@
|
||||||
(ns grub.test.unit.diff
|
(ns grub.test.unit.diff
|
||||||
(:require [grub.diff :as diff]
|
(:require [grub.diff :as diff]
|
||||||
[clojure.test :refer :all]))
|
[midje.sweet :refer :all]))
|
||||||
|
|
||||||
|
|
||||||
(def empty-diff {:grubs {:deleted #{} :updated nil}
|
(def empty-diff {:grubs {:- #{} :+ nil}
|
||||||
:recipes {:deleted #{} :updated nil}})
|
:recipes {:- #{} :+ nil}})
|
||||||
|
|
||||||
(deftest diff-empty-states
|
(fact "Diff of empty states is empty diff"
|
||||||
(let [empty-state {:grubs {} :recipes {}}]
|
(let [empty-state {:grubs {} :recipes {}}]
|
||||||
(is (= empty-diff
|
(diff/diff-states empty-state empty-state) => empty-diff))
|
||||||
(diff/diff-states empty-state empty-state)))))
|
|
||||||
|
|
||||||
(deftest diff-equal-states
|
(fact "Diff of equal states is empty diff"
|
||||||
(is (= empty-diff
|
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
||||||
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
{:grubs {"id" {:text "asdf" :completed false}} :recipes {}})
|
||||||
{:grubs {"id" {:text "asdf" :completed false}} :recipes {}}))))
|
=> empty-diff)
|
||||||
|
|
||||||
(deftest diff-added-grub
|
(fact "Diff of one added grub has one updated grub"
|
||||||
(is (= {:grubs {:deleted #{}
|
(diff/diff-states {:grubs {} :recipes {}}
|
||||||
:updated {"id" {:completed false, :text "asdf"}}}
|
{:grubs {"id" {:text "asdf" :completed false}} :recipes {}})
|
||||||
:recipes {:deleted #{} :updated nil}}
|
=> {:grubs {:- #{}
|
||||||
(diff/diff-states {:grubs {} :recipes {}}
|
:+ {"id" {:completed false, :text "asdf"}}}
|
||||||
{:grubs {"id" {:text "asdf" :completed false}} :recipes {}}))))
|
:recipes {:- #{} :+ nil}})
|
||||||
|
|
||||||
(deftest diff-deleted-grub
|
(fact "Diff of one removed grub has one deleted grub"
|
||||||
(is (= {:grubs {:deleted #{"id"}
|
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
||||||
:updated nil}
|
{:grubs {} :recipes {}})
|
||||||
:recipes {:deleted #{} :updated nil}}
|
=>
|
||||||
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
{:grubs {:- #{"id"}
|
||||||
{:grubs {} :recipes {}}))))
|
:+ nil}
|
||||||
|
:recipes {:- #{} :+ nil}})
|
||||||
|
|
||||||
(deftest diff-edited-grub
|
(fact "Diff of one changed grub has updated grub"
|
||||||
(is (= {:grubs {:deleted #{}
|
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
||||||
:updated {"id" {:text "asdf2"}}}
|
{:grubs {"id" {:text "asdf2" :completed false}} :recipes {}})
|
||||||
:recipes {:deleted #{} :updated nil}}
|
=>
|
||||||
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
{:grubs {:- #{}
|
||||||
{:grubs {"id" {:text "asdf2" :completed false}} :recipes {}}))))
|
:+ {"id" {:text "asdf2"}}}
|
||||||
|
:recipes {:- #{} :+ nil}})
|
||||||
|
|
||||||
(deftest diff-completed-grub
|
(fact "Diff of one completed grub has updated grub"
|
||||||
(is (= {:grubs {:deleted #{}
|
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
||||||
:updated {"id" {:completed true}}}
|
{:grubs {"id" {:text "asdf" :completed true}} :recipes {}})
|
||||||
:recipes {:deleted #{} :updated nil}}
|
=> {:grubs {:- #{}
|
||||||
(diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}}
|
:+ {"id" {:completed true}}}
|
||||||
{:grubs {"id" {:text "asdf" :completed true}} :recipes {}}))))
|
:recipes {:- #{} :+ nil}})
|
||||||
|
|
||||||
(deftest diff-added-recipe
|
(fact "Diff of one added recipe has updated recipe"
|
||||||
(is (= {:grubs {:deleted #{}
|
(diff/diff-states {:grubs {} :recipes {}}
|
||||||
:updated nil}
|
{:grubs {} :recipes {"id" {:name "Blue Cheese Soup"
|
||||||
:recipes {:deleted #{} :updated {"id" {:name "Blue Cheese Soup"
|
:grubs "Some grubs"}}})
|
||||||
:grubs "Some grubs"}}}}
|
=>
|
||||||
(diff/diff-states {:grubs {} :recipes {}}
|
{:grubs {:- #{}
|
||||||
{:grubs {} :recipes {"id" {:name "Blue Cheese Soup"
|
:+ nil}
|
||||||
:grubs "Some grubs"}}}))))
|
:recipes {:- #{} :+ {"id" {:name "Blue Cheese Soup"
|
||||||
|
:grubs "Some grubs"}}}})
|
||||||
|
|
||||||
(deftest diff-edited-recipe
|
(fact "Diff of one changed recipe has one updated recipe"
|
||||||
(is (= {:grubs {:deleted #{}
|
(diff/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup"
|
||||||
:updated nil}
|
:grubs "Some grubs"}}}
|
||||||
:recipes {:deleted #{} :updated {"id" {:name "Bleu Cheese Soup" }}}}
|
{:grubs {} :recipes {"id" {:name "Bleu Cheese Soup"
|
||||||
(diff/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup"
|
:grubs "Some grubs"}}})
|
||||||
:grubs "Some grubs"}}}
|
=> {:grubs {:- #{}
|
||||||
{:grubs {} :recipes {"id" {:name "Bleu Cheese Soup"
|
:+ nil}
|
||||||
:grubs "Some grubs"}}}))))
|
:recipes {:- #{} :+ {"id" {:name "Bleu Cheese Soup" }}}})
|
||||||
|
|
||||||
(deftest diff-deleted-recipe
|
(fact "Diff of one removed recipe has one deleted recipe"
|
||||||
(is (= {:grubs {:deleted #{} :updated nil}
|
(diff/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup"
|
||||||
:recipes {:deleted #{"id"} :updated nil}}
|
:grubs "Some grubs"}}}
|
||||||
(diff/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup"
|
{:grubs {} :recipes {}})
|
||||||
:grubs "Some grubs"}}}
|
=>
|
||||||
{:grubs {} :recipes {}}))))
|
{:grubs {:- #{} :+ nil}
|
||||||
|
:recipes {:- #{"id"} :+ nil}})
|
||||||
|
|
||||||
(def before-state
|
(def before-state
|
||||||
{:grubs
|
{:grubs
|
||||||
|
@ -107,8 +110,8 @@
|
||||||
|
|
||||||
(def expected-diff
|
(def expected-diff
|
||||||
{:recipes
|
{:recipes
|
||||||
{:deleted #{"recipe-deleted"}
|
{:- #{"recipe-deleted"}
|
||||||
:updated
|
:+
|
||||||
{"recipe-added"
|
{"recipe-added"
|
||||||
{:name "Burgers"
|
{:name "Burgers"
|
||||||
:grubs
|
:grubs
|
||||||
|
@ -117,17 +120,47 @@
|
||||||
{:grubs
|
{: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
|
:grubs
|
||||||
{:deleted #{"grub-deleted"}
|
{:- #{"grub-deleted"}
|
||||||
:updated
|
:+
|
||||||
{"grub-completed" {:completed true}
|
{"grub-completed" {:completed true}
|
||||||
"grub-updated" {:text "Ketchup"}
|
"grub-updated" {:text "Ketchup"}
|
||||||
"grub-added"
|
"grub-added"
|
||||||
{:completed false :text "Toothpaste"}}}})
|
{:completed false :text "Toothpaste"}}}})
|
||||||
|
|
||||||
(deftest diff-many-changes
|
(fact "Diff of many changes has all changes"
|
||||||
(is (= expected-diff (diff/diff-states before-state after-state))))
|
(diff/diff-states before-state after-state) => expected-diff)
|
||||||
|
|
||||||
(deftest patch-returns-original-state
|
(fact "Diff and patch of many changes returns original state with new tag"
|
||||||
(is
|
(let [diff (diff/diff-states before-state after-state)
|
||||||
(let [diff (diff/diff-states before-state after-state)]
|
result (diff/patch-state before-state diff)]
|
||||||
(= after-state (diff/patch-state before-state diff)))))
|
(dissoc result :tag) => after-state
|
||||||
|
(:tag result) => #(not (nil? %))))
|
||||||
|
|
||||||
|
(fact "Diff of states with tags includes tags in diff"
|
||||||
|
(diff/diff-states {:tag "1"} {:tag "2"}) => {:tag "2" :shadow-tag "1"})
|
||||||
|
|
||||||
|
(fact "Patch of state creates new tag by default"
|
||||||
|
(let [result (diff/patch-state {:grubs {:a {:b1 :b2}} :tag 0} {:grubs {:+ {:a {:b1 :b3}}} :- #{}})]
|
||||||
|
result => (contains {:grubs {:a {:b1 :b3}}})
|
||||||
|
(:tag result) => #(not (nil? %))
|
||||||
|
(:tag result) => #(not= % 0)))
|
||||||
|
|
||||||
|
(fact "Patch of state sets new tag to patch tag if specified"
|
||||||
|
(diff/patch-state {:grubs {:a {:b1 :b2}} :tag 0}
|
||||||
|
{:grubs {:+ {:a {:b1 :b3}}} :- #{} :tag 4}
|
||||||
|
true)
|
||||||
|
=>
|
||||||
|
{:grubs {:a {:b1 :b3}} :tag 4})
|
||||||
|
|
||||||
|
(fact "Empty patch of state sets new tag to patch tag if specified"
|
||||||
|
(diff/patch-state {:grubs {:a {:b1 :b2}} :tag 0}
|
||||||
|
{:shadow-tag 0 :tag 4 :grubs {:+ nil :- #{}}}
|
||||||
|
true)
|
||||||
|
=>
|
||||||
|
{:grubs {:a {:b1 :b2}} :tag 4})
|
||||||
|
|
||||||
|
(fact "Patch of empty diff returns original state"
|
||||||
|
(diff/patch-state {:grubs {:a {:b1 :b2}} :tag 0}
|
||||||
|
{:grubs {:+ nil :- #{}} :tag 4})
|
||||||
|
=>
|
||||||
|
{:grubs {:a {:b1 :b2}} :tag 0})
|
||||||
|
|
|
@ -1,35 +1,36 @@
|
||||||
(ns grub.test.unit.state
|
(ns grub.test.unit.state
|
||||||
(:require [grub.state :as s]
|
(:require [grub.state :as s]
|
||||||
[midje.sweet :refer :all]
|
[midje.sweet :refer :all]))
|
||||||
[hasch.core :as hasch]))
|
|
||||||
|
|
||||||
(fact "Get current state returns last state"
|
(fact "Get current state returns last state"
|
||||||
(let [states [{:hash "asdf" :state {:a :b}}
|
(let [states [{:tag "1" :a :b}
|
||||||
{:hash "fdsa" :state {:c :d}}]]
|
{:tag "2" :c :d}]]
|
||||||
(s/get-current-state states) => {:c :d}))
|
(s/get-latest states) => {:tag "2" :c :d}))
|
||||||
|
|
||||||
(fact "Get history state returns state with given hash"
|
(fact "Get history state returns state with given hash"
|
||||||
(let [states [{:hash "hash1" :state {:a :b}}
|
(let [states [{:tag "1" :a :b}
|
||||||
{:hash "hash2" :state {:c :d}}
|
{:tag "2" :c :d}
|
||||||
{:hash "hash3" :state {:e :f}}]]
|
{:tag "3" :e :f}]]
|
||||||
(s/get-history-state states "hash1") => {:a :b}
|
(s/get-tagged states "1") => {:tag "1" :a :b}
|
||||||
(s/get-history-state states "hash2") => {:c :d}
|
(s/get-tagged states "2") => {:tag "2" :c :d}
|
||||||
(s/get-history-state states "hash3") => {:e :f}))
|
(s/get-tagged states "3") => {:tag "3" :e :f}))
|
||||||
|
|
||||||
(fact "Add history state appends state to the end"
|
(fact "Add history state appends state to the end"
|
||||||
(let [states [{:hash "hash1" :state {:a :b}}
|
(let [states [{:tag "1" :a :b}
|
||||||
{:hash "hash2" :state {:c :d}}]]
|
{:tag "2" :c :d}]]
|
||||||
(:state (last (s/add-history-state states {:e :f}))) => {:e :f}))
|
(-> (s/add states {:e :f})
|
||||||
|
(last)
|
||||||
|
(dissoc :tag))
|
||||||
|
=> {:e :f}))
|
||||||
|
|
||||||
(fact "Add history state appends state to the end and drops first state if full"
|
(fact "Add history state appends state to the end and drops first state if full"
|
||||||
(let [states (into [] (for [i (range 20)] {:hash (str "hash" i) :state {:i i}}))
|
(let [states (into [] (for [i (range 20)] {:tag (str i) :i i}))
|
||||||
new-states (s/add-history-state states {:i 21})]
|
new-states (s/add states {:i 21})]
|
||||||
(count new-states) => 20
|
(count new-states) => 20
|
||||||
(:state (last new-states)) => {:i 21}
|
(dissoc (last new-states) :tag) => {:i 21}
|
||||||
(:state (first new-states)) => {:i 1}))
|
(first new-states) => {:tag "1" :i 1}))
|
||||||
|
|
||||||
(fact "Add history state does not add consecutive duplicate states"
|
(fact "Add history state does not add consecutive duplicate states"
|
||||||
(let [hash (hasch/uuid {:c :d})
|
(let [states [{:tag "1" :a :b}
|
||||||
states [{:hash "hash1" :state {:a :b}}
|
{:tag "2" :c :d}]]
|
||||||
{:hash hash :state {:c :d}}]]
|
(s/add states {:tag "2" :c :d}) => states))
|
||||||
(s/add-history-state states {:c :d}) => states))
|
|
||||||
|
|
|
@ -1,112 +1,185 @@
|
||||||
(ns grub.test.unit.sync
|
(ns grub.test.unit.sync
|
||||||
(:require [grub.state :as state]
|
(:require [grub.state :as state]
|
||||||
[grub.sync :as sync]
|
[grub.sync :as sync]
|
||||||
[midje.sweet :refer :all]
|
[midje.sweet :refer :all]))
|
||||||
[hasch.core :as hasch]))
|
|
||||||
|
|
||||||
(defn hashed-states [& states]
|
(facts "Server diff"
|
||||||
(->> states
|
(fact "Server applies diff, returns empty diff with client tag, new server tag when no server changes"
|
||||||
(map (fn [s] {:hash (hasch/uuid s)
|
(let [{:keys [new-states out-event new-shadow]}
|
||||||
:state s}))
|
(sync/handle-event
|
||||||
(into [])))
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? false})]
|
||||||
|
new-states => (just (just {:tag 0
|
||||||
|
:grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
|
:recipes {}})
|
||||||
|
(just {:tag #(not (nil? %))
|
||||||
|
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
|
:recipes {}}))
|
||||||
|
out-event => (just {:type :diff
|
||||||
|
:diff (just {:shadow-tag 4
|
||||||
|
:tag #(not (nil? %))
|
||||||
|
:grubs {:- #{} :+ nil}
|
||||||
|
:recipes {:- #{}, :+ nil}})})
|
||||||
|
new-shadow => {:tag 4
|
||||||
|
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
|
:recipes {}}))
|
||||||
|
|
||||||
(fact "Server applies diff and returns empty diff when no server changes"
|
(fact "Server applies diff and returns changes when server has changed"
|
||||||
(let [states (atom (hashed-states
|
(let [{:keys [new-states new-shadow out-event]}
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}))
|
(sync/handle-event
|
||||||
event {:type :diff
|
{:type :diff
|
||||||
:diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
:diff {:shadow-tag 0 :tag 4
|
||||||
:hash (:hash (first @states))
|
:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
:states states
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:shadow (:state (last @states))
|
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
:client? false}
|
"2" {:text "3 onions" :completed false}}
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
:recipes {}}])
|
||||||
new-states => (hashed-states
|
:shadow state/empty-state
|
||||||
{:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
:client? false})]
|
||||||
{:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}})
|
new-states =>
|
||||||
new-shadow {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}
|
(just {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
out-event => {:type :diff
|
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
:diff {:grubs {:deleted #{}, :updated nil}
|
|
||||||
:recipes {:deleted #{}, :updated nil}}
|
|
||||||
:hash (:hash (last new-states))}))
|
|
||||||
|
|
||||||
(fact "Client applies diff and returns empty diff when no client changes"
|
|
||||||
(let [states (atom (hashed-states
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}))
|
|
||||||
event {:type :diff
|
|
||||||
:diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
|
||||||
:hash (:hash (first @states))
|
|
||||||
:states states
|
|
||||||
:shadow (:state (last @states))
|
|
||||||
:client? true}
|
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
|
||||||
new-states => (hashed-states
|
|
||||||
{:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
|
||||||
{:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}})
|
|
||||||
new-shadow => {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}
|
|
||||||
out-event => {:type :diff
|
|
||||||
:diff {:grubs {:deleted #{}, :updated nil}
|
|
||||||
:recipes {:deleted #{}, :updated nil}}
|
|
||||||
:hash (:hash (last new-states))}))
|
|
||||||
|
|
||||||
(fact "Server applies diff and returns changes when server has changed"
|
|
||||||
(let [states (atom (hashed-states
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}
|
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}))
|
|
||||||
event {:type :diff
|
|
||||||
:diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
|
||||||
:hash (:hash (first @states))
|
|
||||||
:states states
|
|
||||||
:shadow state/empty-state
|
|
||||||
:client? false}
|
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
|
||||||
new-states => (hashed-states
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}
|
|
||||||
"2" {:text "3 onions" :completed false}}
|
"2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}
|
:recipes {}}
|
||||||
{:grubs {"1" {:text "2 apples" :completed true}
|
(just {:tag #(not (nil? %)) :grubs {"1" {:text "2 apples" :completed true}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}))
|
||||||
|
out-event =>
|
||||||
|
(just {:type :diff
|
||||||
|
:diff (just {:shadow-tag 4
|
||||||
|
:tag #(not (nil? %))
|
||||||
|
:grubs {:- #{} :+ {"2" {:completed false, :text "3 onions"}}}
|
||||||
|
:recipes {:- #{}, :+ nil}})})
|
||||||
|
new-shadow => {:tag 4
|
||||||
|
:grubs {"1" {:text "2 apples" :completed true}}
|
||||||
|
:recipes {}}))
|
||||||
|
|
||||||
|
(fact "Server forces full sync if client is out of sync"
|
||||||
|
(let [states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}])
|
||||||
|
event {:type :diff
|
||||||
|
:diff {:shadow-tag 3 :tag 12
|
||||||
|
:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
||||||
|
:states states
|
||||||
|
:shadow state/empty-state
|
||||||
|
:client? false}
|
||||||
|
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
||||||
|
new-states => nil
|
||||||
|
out-event => {:type :full-sync
|
||||||
|
:full-state {:tag 15
|
||||||
|
:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}}))
|
||||||
|
|
||||||
|
(fact "Server state is unchanged on receiving empty diff"
|
||||||
|
(-> (sync/handle-event
|
||||||
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? false})
|
||||||
|
:new-states)
|
||||||
|
=> [{:tag 0
|
||||||
|
:grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
|
:recipes {}}])
|
||||||
|
|
||||||
|
(fact "Server returns no response on empty diff"
|
||||||
|
(-> (sync/handle-event
|
||||||
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? false})
|
||||||
|
:out-event)
|
||||||
|
=> nil)
|
||||||
|
|
||||||
|
(fact "Server updates client shadow on empty diff"
|
||||||
|
(-> (sync/handle-event
|
||||||
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? false})
|
||||||
|
:new-shadow)
|
||||||
|
=> {:tag 4 :grubs {"1" {:completed false, :text "2 apples"}} :recipes {}}))
|
||||||
|
|
||||||
|
(facts "Client diffs"
|
||||||
|
(fact "Client applies diff, returns empty diff with server tag, new client tag when no client changes"
|
||||||
|
(let [event {:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
|
:states (atom
|
||||||
|
[{:tag 0 :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
|
:recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
|
:recipes {}}
|
||||||
|
:client? true}
|
||||||
|
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
||||||
|
new-states =>
|
||||||
|
(just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
||||||
|
(just {:tag #(not (nil? %))
|
||||||
|
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
|
:recipes {}}))
|
||||||
|
new-shadow {:tag 4 :grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}
|
||||||
|
out-event => (just {:type :diff
|
||||||
|
:diff (just {:shadow-tag 4
|
||||||
|
:tag #(not (nil? %))
|
||||||
|
:grubs {:- #{} :+ nil}
|
||||||
|
:recipes {:- #{}, :+ nil}})})))
|
||||||
|
|
||||||
|
(fact "Client state is unchanged on receiving empty diff"
|
||||||
|
(-> (sync/handle-event
|
||||||
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? true})
|
||||||
|
:new-states)
|
||||||
|
=> [{:tag 0
|
||||||
|
:grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
|
:recipes {}}])
|
||||||
|
|
||||||
|
(fact "Client returns no response on empty diff"
|
||||||
|
(-> (sync/handle-event
|
||||||
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? true})
|
||||||
|
:out-event)
|
||||||
|
=> nil)
|
||||||
|
|
||||||
|
(fact "Client updates server shadow on empty diff"
|
||||||
|
(-> (sync/handle-event
|
||||||
|
{:type :diff
|
||||||
|
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||||
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
:client? true})
|
||||||
|
:new-shadow)
|
||||||
|
=> {:tag 4 :grubs {"1" {:completed false, :text "2 apples"}} :recipes {}}))
|
||||||
|
|
||||||
|
(facts "Full sync"
|
||||||
|
(fact "Server sends full sync if client requests it"
|
||||||
|
(let [result (sync/handle-event
|
||||||
|
{:type :full-sync-request
|
||||||
|
:states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}])})]
|
||||||
|
(:new-states result) => nil
|
||||||
|
(:new-shadow result) =>
|
||||||
|
(just {:tag #(not (nil? %))
|
||||||
|
:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
(:out-event result) =>
|
||||||
|
{:type :full-sync
|
||||||
|
:full-state {:tag 15
|
||||||
|
:grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
"2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}})
|
:recipes {}}})))
|
||||||
out-event => {:type :diff
|
|
||||||
:diff {:grubs {:deleted #{}
|
|
||||||
:updated {"2" {:completed false, :text "3 onions"}}}
|
|
||||||
:recipes {:deleted #{}, :updated nil}}
|
|
||||||
:hash (hasch/uuid {:grubs {"1" {:text "2 apples" :completed true}}
|
|
||||||
:recipes {}})}))
|
|
||||||
|
|
||||||
(fact "Server forces full sync if client is out of sync"
|
|
||||||
(let [states (atom (hashed-states
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}
|
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}))
|
|
||||||
event {:type :diff
|
|
||||||
:diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
|
||||||
:hash (:hash {:grubs {"0" {:text "milk" :completed false}}
|
|
||||||
:recipes {}})
|
|
||||||
:states states
|
|
||||||
:shadow state/empty-state
|
|
||||||
:client? false}
|
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
|
||||||
new-states => nil
|
|
||||||
out-event => {:type :full-sync
|
|
||||||
:full-state {:grubs {"1" {:text "2 apples" :completed false}
|
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}}))
|
|
||||||
|
|
||||||
(fact "Server sends full sync if client requests it"
|
|
||||||
(let [states (atom (hashed-states
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
{:grubs {"1" {:text "2 apples" :completed false}
|
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}))
|
|
||||||
event {:type :full-sync-request
|
|
||||||
:states states}
|
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
|
||||||
new-states => nil
|
|
||||||
out-event => {:type :full-sync
|
|
||||||
:full-state {:grubs {"1" {:text "2 apples" :completed false}
|
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}}))
|
|
||||||
|
|
Loading…
Reference in a new issue