Rename state <--> sync namespaces
This commit is contained in:
parent
e14b3d2a29
commit
9591e9fd93
6 changed files with 318 additions and 312 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}))
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)}))
|
||||
|
|
Loading…
Add table
Reference in a new issue