Rename state <--> sync namespaces

This commit is contained in:
Nicholas Kariniemi 2014-10-17 16:51:49 +03:00
parent e14b3d2a29
commit 9591e9fd93
6 changed files with 318 additions and 312 deletions

View file

@ -63,7 +63,7 @@
(let [to-client (chan)
from-client (chan)]
(ws/add-connected-client! ws-channel to-client from-client)
(state/sync-new-client! to-client from-client new-states states)))
(sync/sync-new-client! to-client from-client new-states states)))
(handler request))))
(defn handle-root [handler index]
@ -91,11 +91,11 @@
(defn start [{:keys [port db-name states] :as system}]
(let [{:keys [db conn]} (db/connect db-name)
new-states (chan)
_ (reset! states (sync/new-state (db/get-current-state db)))
_ (reset! states (state/new-state (db/get-current-state db)))
stop-server (httpkit/run-server (make-handler system new-states) {:port port})]
(add-watch states :db (fn [_ _ old new]
(when-not (= old new)
(let [new-state (sync/get-current-state new)]
(let [new-state (state/get-current-state new)]
(a/put! new-states new-state)
(db/update-db! db new-state)))))
(println "Started server on localhost:" port)

View file

@ -24,10 +24,10 @@
events (chan)
view-state (view/render-app state/empty-state render-states new-states)
ws (websocket/connect pending-msg >remote events)
agent-states (state/sync-client! >remote events new-states states)]
agent-states (sync/sync-client! >remote events new-states states)]
(add-watch states :render (fn [_ _ old new]
(when-not (= old new)
(a/put! render-states (sync/get-current-state new)))))
(a/put! render-states (state/get-current-state new)))))
(assoc system
:ws ws
:channels {:new-states new-states

View file

@ -1,87 +1,32 @@
(ns grub.state
(:require [grub.diff :as diff]
[grub.message :as message]
[grub.sync :as sync]
[hasch.core :as hasch]
#+clj [clojure.core.async :as a :refer [<! >! chan go]]
#+cljs [cljs.core.async :as a :refer [<! >! chan]])
#+cljs (:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go]]))
[grub.util :as util]
[hasch.core :as hasch]))
(def empty-state sync/empty-state)
(def num-history-states 20)
(defn update-states [states diff]
(let [state (sync/get-current-state states)
new-state (diff/patch-state state diff)]
(sync/add-history-state states new-state)))
(def empty-state {:grubs {} :recipes {}})
(def empty-states [{:grubs {} :recipes {}}])
(defn diff-msg [shadow state]
(let [diff (diff/diff-states shadow state)
hash (hasch/uuid shadow)]
(message/diff-msg diff hash)))
(defn new-state [state]
[{:hash (hasch/uuid state)
:state state}])
(defmulti handle-event (fn [event] (:type event)))
(defn get-current-state [states]
(:state (last states)))
(defmethod handle-event :diff [{:keys [hash diff states shadow client?]}]
(let [history-shadow (sync/get-history-state @states hash)]
(if history-shadow
(let [new-states (swap! states update-states diff)
new-state (sync/get-current-state new-states)
new-shadow (diff/patch-state history-shadow diff)]
{:out-event (when-not (sync/empty-diff? diff)
(diff-msg new-shadow new-state))
:new-states new-states
:new-shadow new-shadow})
(if client?
{:out-event message/full-sync-request
:new-shadow shadow}
(let [state (sync/get-current-state states)]
{:out-event (message/full-sync state)
:new-shadow state})))))
(defn get-history-state [states hash]
(:state (first (filter #(= (:hash %) hash) states))))
(defmethod handle-event :full-sync-request [{:keys [states]}]
(let [state (sync/get-current-state @states)]
{:new-shadow state
:out-event (message/full-sync state)}))
(defn add-history-state [states new-state]
(let [last-hash (:hash (last states))
new-hash (hasch/uuid new-state)]
(if (= last-hash new-hash)
states
(let [new-states (conj states {:hash new-hash :state new-state})]
(if (>= (count states) num-history-states)
(into [] (rest new-states))
new-states)))))
(defmethod handle-event :full-sync [{:keys [full-state states]}]
(reset! states (sync/new-state full-state))
{:new-shadow full-state})
(defmethod handle-event :default [msg]
#+cljs (logs "Unhandled message:" msg)
#+clj (println "Unhandled message:" msg)
{})
(defn make-agent
([client? >remote events new-states states]
(make-agent client? >remote events new-states states sync/empty-state))
([client? >remote events new-states states initial-shadow]
(go (loop [shadow initial-shadow]
(let [[v c] (a/alts! [new-states events] :priority true)]
(cond (nil? v) nil ;; drop out of loop
(= c new-states)
(do (when-not (= shadow v)
(swap! states sync/add-history-state v)
(>! >remote (diff-msg shadow v)))
(recur shadow))
(= c events)
(let [event (assoc v
:states states
:client? client?
:shadow shadow)
{:keys [new-shadow out-event]} (handle-event event)]
(when out-event (a/put! >remote out-event))
(recur (if new-shadow new-shadow shadow)))))))))
(def make-server-agent (partial make-agent false))
(def make-client-agent (partial make-agent true))
#+clj
(defn sync-new-client! [>remote events new-states states]
(make-server-agent >remote events new-states states))
#+cljs
(defn sync-client! [>remote events new-states states]
(make-client-agent >remote events new-states states)
(a/put! >remote message/full-sync-request))
(defn empty-diff? [diff]
(= diff {:recipes {:deleted #{}, :updated nil}, :grubs {:deleted #{}, :updated nil}}))

View file

@ -1,32 +1,93 @@
(ns grub.sync
(:require [grub.diff :as diff]
[grub.util :as util]
[hasch.core :as hasch]))
[grub.message :as message]
[grub.state :as state]
[hasch.core :as hasch]
#+clj [clojure.core.async :as a :refer [<! >! chan go]]
#+cljs [cljs.core.async :as a :refer [<! >! chan]])
#+cljs (:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go]]))
(def num-history-states 20)
(def empty-state state/empty-state)
(def empty-state {:grubs {} :recipes {}})
(def empty-states [{:grubs {} :recipes {}}])
(defn update-states [states diff]
(let [state (state/get-current-state states)
new-state (diff/patch-state state diff)]
(state/add-history-state states new-state)))
(defn new-state [state]
[{:hash (hasch/uuid state)
:state state}])
(defn diff-msg [shadow state]
(let [diff (diff/diff-states shadow state)
hash (hasch/uuid shadow)]
(message/diff-msg diff hash)))
(defn get-current-state [states]
(:state (last states)))
(defmulti handle-event (fn [event] (:type event)))
(defn get-history-state [states hash]
(:state (first (filter #(= (:hash %) hash) states))))
(defmethod handle-event :diff [{:keys [hash diff states shadow client?]}]
(let [history-shadow (state/get-history-state @states hash)]
(if history-shadow
(let [new-states (swap! states update-states diff)
new-state (state/get-current-state new-states)
new-shadow (diff/patch-state history-shadow diff)]
{:out-event (when-not (state/empty-diff? diff)
(diff-msg new-shadow new-state))
:new-states new-states
:new-shadow new-shadow})
(if client?
{:out-event message/full-sync-request
:new-shadow shadow}
(let [state (state/get-current-state states)]
{:out-event (message/full-sync state)
:new-shadow state})))))
(defn add-history-state [states new-state]
(let [last-hash (:hash (last states))
new-hash (hasch/uuid new-state)]
(if (= last-hash new-hash)
states
(let [new-states (conj states {:hash new-hash :state new-state})]
(if (>= (count states) num-history-states)
(into [] (rest new-states))
new-states)))))
(defmethod handle-event :full-sync-request [{:keys [states]}]
(let [state (state/get-current-state @states)]
{:new-shadow state
:out-event (message/full-sync state)}))
(defn empty-diff? [diff]
(= diff {:recipes {:deleted #{}, :updated nil}, :grubs {:deleted #{}, :updated nil}}))
(defmethod handle-event :full-sync [{:keys [full-state states]}]
(reset! states (state/new-state full-state))
{:new-shadow full-state})
(defmethod handle-event :default [msg]
#+cljs (logs "Unhandled message:" msg)
#+clj (println "Unhandled message:" msg)
{})
(defn make-agent
([client? >remote events new-states states]
(make-agent client? >remote events new-states states state/empty-state))
([client? >remote events new-states states initial-shadow]
(go (loop [shadow initial-shadow]
(let [[v c] (a/alts! [new-states events] :priority true)]
(cond (nil? v) nil ;; drop out of loop
(= c new-states)
(do (when-not (= shadow v)
(swap! states state/add-history-state v)
(>! >remote (diff-msg shadow v)))
(recur shadow))
(= c events)
(let [event (assoc v
:states states
:client? client?
:shadow shadow)
{:keys [new-shadow out-event]} (handle-event event)]
(when out-event (a/put! >remote out-event))
(recur (if new-shadow new-shadow shadow)))))))))
(def make-server-agent (partial make-agent false))
(def make-client-agent (partial make-agent true))
#+clj
(defn sync-new-client! [>remote events new-states states]
(make-server-agent >remote events new-states states))
#+cljs
(defn sync-client! [>remote events new-states states]
(let [new-states* (chan)]
(go (loop []
(let [v (<! new-states)]
(<! (a/timeout 1000))
(jk>! new-states* v)
(recur))))
(make-client-agent >remote events new-states* states)
(a/put! >remote message/full-sync-request)))

View file

@ -1,179 +1,45 @@
(ns grub.test.unit.state
(:require [grub.state :as state]
[grub.sync :as sync]
(:require [grub.state :as s]
[midje.sweet :refer :all]
[hasch.core :as hasch]))
(defn hashed-states [& states]
(->> states
(map (fn [s] {:hash (hasch/uuid s)
:state s}))
(into [])))
(fact "Sets correct initial state"
(let [grubs [{:id "1" :text "2 bananas" :completed false}
{:id "2" :text "3 onions" :completed false}]
recipes []
expected-state {:grubs {"1" {:id "1" :text "2 bananas" :completed false}
"2" {:id "2" :text "3 onions" :completed false}}
:recipes {}}
expected-hash (hasch/uuid expected-state)]
(s/initial-state grubs recipes) => [{:state expected-state :hash expected-hash}]))
(fact "Server applies diff and returns empty diff when no server changes"
(let [states (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? false}
{:keys [new-states new-shadow out-event]} (state/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 "Get current state returns last state"
(let [states [{:hash "asdf" :state {:a :b}}
{:hash "fdsa" :state {:c :d}}]]
(s/get-current-state states) => {:c :d}))
(fact "Client applies diff, clears history, updates shadow, returns empty diff when no client changes"
(let [states (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]} (state/handle-event event)]
new-states => (hashed-states
{: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 "Get history state returns state with given hash"
(let [states [{:hash "hash1" :state {:a :b}}
{:hash "hash2" :state {:c :d}}
{:hash "hash3" :state {:e :f}}]]
(s/get-history-state states "hash1") => {:a :b}
(s/get-history-state states "hash2") => {:c :d}
(s/get-history-state states "hash3") => {:e :f}))
(fact "Server applies diff and returns changes when server has changed"
(let [states (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 sync/empty-state
:client? false}
{:keys [new-states new-shadow out-event]} (state/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}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed true}
"2" {:text "3 onions" :completed false}}
: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 "Add history state appends state to the end"
(let [states [{:hash "hash1" :state {:a :b}}
{:hash "hash2" :state {:c :d}}]]
(:state (last (s/add-history-state states {:e :f}))) => {:e :f}))
(fact "Server forces full sync if client is out of sync"
(let [states (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 sync/empty-state
:client? false}
{:keys [new-states new-shadow out-event]} (state/handle-event event)]
new-states => nil
out-event => {:type :full-sync
:state {:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}}))
(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}}))
new-states (s/add-history-state states {:i 21})]
(count new-states) => 20
(:state (last new-states)) => {:i 21}
(:state (first new-states)) => {:i 1}))
(fact "Server sends full sync if client requests it"
(let [states (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]} (state/handle-event event)]
new-states => nil
out-event => {:type :full-sync
:state {:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}}))
(fact "New state - server passes diff to client, does not update shadow"
(let [states (hashed-states
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
client-state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
event {:type :new-state
:state (:state (last states))
:client? false
:states states
:shadow client-state}
{:keys [new-states new-shadow out-event]} (state/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}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
new-shadow => nil
out-event => {:type :diff
:diff {:grubs {:deleted #{}
:updated {"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}}
:recipes {:deleted #{}, :updated nil}}
:hash (hasch/uuid client-state)}))
(fact "New state - client passes diff to server, does not update shadow"
(let [states (hashed-states
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
shadow {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
event {:type :new-state
:state (:state (last states))
:client? true
:states states
:shadow shadow}
{:keys [new-states new-shadow out-event]} (state/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}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
new-shadow => nil
out-event => {:type :diff
:diff {:grubs {:deleted #{}
:updated {"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}}
:recipes {:deleted #{}, :updated nil}}
:hash (hasch/uuid shadow)}))
(fact "Add history state does not add consecutive duplicate states"
(let [hash (hasch/uuid {:c :d})
states [{:hash "hash1" :state {:a :b}}
{:hash hash :state {:c :d}}]]
(s/add-history-state states {:c :d}) => states))

View file

@ -1,45 +1,179 @@
(ns grub.test.unit.sync
(:require [grub.sync :as s]
(:require [grub.state :as state]
[grub.sync :as sync]
[midje.sweet :refer :all]
[hasch.core :as hasch]))
(fact "Sets correct initial state"
(let [grubs [{:id "1" :text "2 bananas" :completed false}
{:id "2" :text "3 onions" :completed false}]
recipes []
expected-state {:grubs {"1" {:id "1" :text "2 bananas" :completed false}
"2" {:id "2" :text "3 onions" :completed false}}
:recipes {}}
expected-hash (hasch/uuid expected-state)]
(s/initial-state grubs recipes) => [{:state expected-state :hash expected-hash}]))
(defn hashed-states [& states]
(->> states
(map (fn [s] {:hash (hasch/uuid s)
:state s}))
(into [])))
(fact "Get current state returns last state"
(let [states [{:hash "asdf" :state {:a :b}}
{:hash "fdsa" :state {:c :d}}]]
(s/get-current-state states) => {:c :d}))
(fact "Server applies diff and returns empty diff when no server changes"
(let [states (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? false}
{: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 "Get history state returns state with given hash"
(let [states [{:hash "hash1" :state {:a :b}}
{:hash "hash2" :state {:c :d}}
{:hash "hash3" :state {:e :f}}]]
(s/get-history-state states "hash1") => {:a :b}
(s/get-history-state states "hash2") => {:c :d}
(s/get-history-state states "hash3") => {:e :f}))
(fact "Client applies diff, clears history, updates shadow, returns empty diff when no client changes"
(let [states (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 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 "Add history state appends state to the end"
(let [states [{:hash "hash1" :state {:a :b}}
{:hash "hash2" :state {:c :d}}]]
(:state (last (s/add-history-state states {:e :f}))) => {:e :f}))
(fact "Server applies diff and returns changes when server has changed"
(let [states (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}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed true}
"2" {:text "3 onions" :completed false}}
: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 "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}}))
new-states (s/add-history-state states {:i 21})]
(count new-states) => 20
(:state (last new-states)) => {:i 21}
(:state (first new-states)) => {:i 1}))
(fact "Server forces full sync if client is out of sync"
(let [states (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
:state {:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}}))
(fact "Add history state does not add consecutive duplicate states"
(let [hash (hasch/uuid {:c :d})
states [{:hash "hash1" :state {:a :b}}
{:hash hash :state {:c :d}}]]
(s/add-history-state states {:c :d}) => states))
(fact "Server sends full sync if client requests it"
(let [states (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
:state {:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}}))
(fact "New state - server passes diff to client, does not update shadow"
(let [states (hashed-states
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
client-state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
event {:type :new-state
:state (:state (last states))
:client? false
:states states
:shadow client-state}
{: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}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
new-shadow => nil
out-event => {:type :diff
:diff {:grubs {:deleted #{}
:updated {"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}}
:recipes {:deleted #{}, :updated nil}}
:hash (hasch/uuid client-state)}))
(fact "New state - client passes diff to server, does not update shadow"
(let [states (hashed-states
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
shadow {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
event {:type :new-state
:state (:state (last states))
:client? true
:states states
:shadow shadow}
{: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}}
:recipes {}}
{:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}
:recipes {}})
new-shadow => nil
out-event => {:type :diff
:diff {:grubs {:deleted #{}
:updated {"2" {:text "3 onions" :completed false}
"3" {:text "milk" :completed false}}}
:recipes {:deleted #{}, :updated nil}}
:hash (hasch/uuid shadow)}))