Use version numbers instead of timestamps
Also: - Don't acknowledge changes sent from the server to the client. - Only send one message at a time to the server. All of these changes bring this more in line with the differential sync paper.
This commit is contained in:
parent
79e64596ae
commit
5ee40a6471
10 changed files with 224 additions and 223 deletions
|
@ -56,14 +56,16 @@
|
|||
:stop-server nil
|
||||
:states (atom nil)})
|
||||
|
||||
(defn handle-websocket [handler states new-states]
|
||||
(defn handle-websocket [handler states new-states-pub]
|
||||
(fn [{:keys [websocket?] :as request}]
|
||||
(if websocket?
|
||||
(httpkit/with-channel request ws-channel
|
||||
(let [to-client (chan)
|
||||
from-client (chan)]
|
||||
from-client (chan)
|
||||
new-states (chan (a/sliding-buffer 1))]
|
||||
(a/sub new-states-pub :new-state new-states)
|
||||
(ws/add-connected-client! ws-channel to-client from-client)
|
||||
(sync/sync-new-client! to-client from-client new-states states)))
|
||||
(sync/make-server-agent to-client from-client new-states states)))
|
||||
(handler request))))
|
||||
|
||||
(defn handle-root [handler index]
|
||||
|
@ -80,20 +82,21 @@
|
|||
:body ""}
|
||||
(handler req))))
|
||||
|
||||
(defn make-handler [{:keys [index states]} new-states]
|
||||
(defn make-handler [{:keys [index states]} new-states-pub]
|
||||
(-> (fn [req] "Not found")
|
||||
(file/wrap-file "public")
|
||||
(content-type/wrap-content-type)
|
||||
(handle-root index)
|
||||
(handle-websocket states new-states)
|
||||
(handle-websocket states new-states-pub)
|
||||
(wrap-bounce-favicon)))
|
||||
|
||||
(defn start [{:keys [port db-name states] :as system}]
|
||||
(let [{:keys [db conn]} (db/connect db-name)
|
||||
new-states (chan)
|
||||
new-states-pub (a/pub new-states (fn [_] :new-state))
|
||||
db-state (db/get-current-state db)
|
||||
_ (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-pub) {:port port})]
|
||||
(add-watch states :db (fn [_ _ old new]
|
||||
(when-not (= old new)
|
||||
(let [new-state (state/get-latest new)]
|
||||
|
|
|
@ -18,8 +18,9 @@
|
|||
|
||||
(defn read-msg [msg]
|
||||
(let [in (ByteArrayInputStream. (.getBytes msg))
|
||||
reader (t/reader in :json)]
|
||||
(t/read reader)))
|
||||
reader (t/reader in :json)
|
||||
received (t/read reader)]
|
||||
received))
|
||||
|
||||
(defn add-connected-client! [ws-channel to from]
|
||||
(println "Client connected:" (.toString ws-channel))
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
(send-pending-msg websocket pending-msg))
|
||||
|
||||
(defn read-msg [msg]
|
||||
(t/read reader (.-message msg)))
|
||||
(let [received (t/read reader (.-message msg))]
|
||||
received))
|
||||
|
||||
(defn connect [pending-msg in out]
|
||||
(let [ws (goog.net.WebSocket.)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
(ns grub.diff
|
||||
(:require [clojure.data :as data]
|
||||
[clojure.set :as set]
|
||||
[grub.tag :as tag]))
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defn deleted [a b]
|
||||
(set/difference (into #{} (keys a)) (into #{} (keys b))))
|
||||
|
@ -22,27 +21,19 @@
|
|||
(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)))
|
||||
(->> prev
|
||||
(keys)
|
||||
(map (fn [k] [k (diff-maps (k prev) (k next))]))
|
||||
(filter #(not (nil? (second %))))
|
||||
(into {})))
|
||||
|
||||
(defn patch-map [state diff]
|
||||
(-> state
|
||||
(#(apply dissoc % (into [] (:- diff))))
|
||||
(#(merge-with merge % (:+ diff)))))
|
||||
|
||||
(defn patch-state
|
||||
([state diff] (patch-state state diff false))
|
||||
([state diff use-diff-tag?]
|
||||
(let [patched (->> state
|
||||
(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)))))))
|
||||
(defn patch-state [state diff]
|
||||
(->> state
|
||||
(keys)
|
||||
(map (fn [k] [k (patch-map (k state) (k diff))]))
|
||||
(into {})))
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
(ns grub.state
|
||||
(:require [grub.diff :as diff]
|
||||
[grub.util :as util]
|
||||
[grub.tag :as tag]))
|
||||
[grub.util :as util]))
|
||||
|
||||
(def num-history-states 20)
|
||||
|
||||
(def empty-state {:tag (tag/oldest-tag) :grubs {} :recipes {}})
|
||||
(def empty-state {:tag 0 :grubs {} :recipes {}})
|
||||
|
||||
(defn new-states [state]
|
||||
[(assoc state :tag (tag/new-tag))])
|
||||
[(assoc state :tag 0)])
|
||||
|
||||
(defn get-latest [states]
|
||||
(last states))
|
||||
|
@ -22,7 +21,7 @@
|
|||
(let [last-state (last states)]
|
||||
(if (= last-state new-state)
|
||||
states
|
||||
(let [new-states (conj states (assoc new-state :tag (tag/new-tag)))]
|
||||
(let [new-states (conj states (assoc new-state :tag (inc (:tag last-state))))]
|
||||
(if (>= (count states) num-history-states)
|
||||
(into [] (rest new-states))
|
||||
new-states)))))
|
||||
|
|
|
@ -22,24 +22,27 @@
|
|||
(defn diff-msg [shadow state]
|
||||
(let [diff (diff/diff-states shadow state)]
|
||||
{:type :diff
|
||||
:diff diff}))
|
||||
:diff diff
|
||||
:tag (:tag state)
|
||||
:shadow-tag (:tag shadow)}))
|
||||
|
||||
(defmulti handle-event (fn [event]
|
||||
(:type event)))
|
||||
(defmulti handle-event (fn [event] (:type event)))
|
||||
|
||||
(defn apply-diff [states diff shadow]
|
||||
(defn apply-diff [states diff shadow new-shadow-tag client?]
|
||||
(let [new-states (swap! states update-states diff)
|
||||
new-state (state/get-latest new-states)
|
||||
new-shadow (diff/patch-state shadow diff true)]
|
||||
{:out-event (when-not (state/state= shadow new-state)
|
||||
(diff-msg new-shadow new-state))
|
||||
:new-states new-states
|
||||
:new-shadow new-shadow}))
|
||||
new-shadow (assoc (diff/patch-state shadow diff)
|
||||
:tag new-shadow-tag)]
|
||||
{:new-shadow new-shadow
|
||||
;; Workaround to send an "ACK" diff when there are no changes
|
||||
:out-event (when (and (not client?)
|
||||
(state/state= new-state new-shadow))
|
||||
(diff-msg new-shadow new-state))}))
|
||||
|
||||
(defmethod handle-event :diff [{:keys [diff states shadow client?]}]
|
||||
(let [history-shadow (state/get-tagged @states (:shadow-tag diff))]
|
||||
(defmethod handle-event :diff [{:keys [diff states shadow shadow-tag tag client?]}]
|
||||
(let [history-shadow (state/get-tagged @states shadow-tag)]
|
||||
(if history-shadow
|
||||
(apply-diff states diff history-shadow)
|
||||
(apply-diff states diff history-shadow tag client?)
|
||||
(if client?
|
||||
{:out-event full-sync-request
|
||||
:new-shadow shadow}
|
||||
|
@ -56,45 +59,79 @@
|
|||
(reset! states (state/new-states full-state))
|
||||
{:new-shadow full-state})
|
||||
|
||||
(defmethod handle-event :new-state [{:keys [shadow states new-state client?]}]
|
||||
(let [new-states (swap! states state/add new-state)
|
||||
latest-state (state/get-latest new-states)]
|
||||
{:out-event (when-not (state/state= shadow latest-state)
|
||||
(diff-msg shadow latest-state))
|
||||
:new-shadow (when (and (not client?)
|
||||
(not (state/state= shadow latest-state)))
|
||||
(assoc latest-state :tag (inc (:tag shadow))))}))
|
||||
|
||||
(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]
|
||||
(defn make-server-agent
|
||||
([>remote events new-states states]
|
||||
(make-server-agent >remote events new-states states state/empty-state))
|
||||
([>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 (state/state= shadow v)
|
||||
(swap! states state/add v)
|
||||
(>! >remote (diff-msg shadow v)))
|
||||
(recur shadow))
|
||||
(let [event {:type :new-state
|
||||
:new-state v
|
||||
:shadow shadow
|
||||
:states states
|
||||
:client? false}
|
||||
{:keys [out-event new-shadow]} (handle-event event)]
|
||||
(when out-event (a/put! >remote out-event))
|
||||
(recur (if new-shadow new-shadow shadow)))
|
||||
(= c events)
|
||||
(let [event (assoc v
|
||||
:states states
|
||||
:client? client?
|
||||
:client? false
|
||||
: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))
|
||||
(defn make-client-agent
|
||||
([>remote events new-states states]
|
||||
(make-client-agent >remote events new-states states state/empty-state))
|
||||
([>remote events new-states states initial-shadow]
|
||||
(go (loop [shadow initial-shadow
|
||||
out-event nil]
|
||||
(when out-event (>! >remote out-event))
|
||||
(let [timeout (a/timeout 1000)
|
||||
[v c] (if out-event
|
||||
(a/alts! [events timeout])
|
||||
(a/alts! [new-states events] :priority true))]
|
||||
(cond (= c timeout) (recur shadow out-event)
|
||||
(nil? v) nil ;; drop out of loop
|
||||
(= c new-states)
|
||||
(let [event {:type :new-state
|
||||
:new-state v
|
||||
:shadow shadow
|
||||
:states states
|
||||
:client? true}
|
||||
{:keys [out-event]} (handle-event event)]
|
||||
(recur shadow out-event))
|
||||
(= c events)
|
||||
(let [event (assoc v
|
||||
:states states
|
||||
:client? true
|
||||
:shadow shadow)
|
||||
{:keys [new-shadow out-event]} (handle-event event)]
|
||||
(recur (if new-shadow new-shadow shadow) out-event))))))))
|
||||
|
||||
#+cljs
|
||||
(defn sync-client! [>remote events new-states states]
|
||||
(let [new-states* (chan)]
|
||||
(let [new-states* (chan (a/sliding-buffer 1))]
|
||||
(go (loop []
|
||||
(let [v (<! new-states)]
|
||||
(<! (a/timeout 1000))
|
||||
(>! new-states* v)
|
||||
(recur))))
|
||||
(make-client-agent >remote events new-states* states)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
(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))
|
|
@ -130,37 +130,6 @@
|
|||
(fact "Diff of many changes has all changes"
|
||||
(diff/diff-states before-state after-state) => expected-diff)
|
||||
|
||||
(fact "Diff and patch of many changes returns original state with new tag"
|
||||
(let [diff (diff/diff-states before-state after-state)
|
||||
result (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})
|
||||
(fact "Diff and patch of many changes returns original state"
|
||||
(let [diff (diff/diff-states before-state after-state)]
|
||||
(diff/patch-state before-state diff) => after-state))
|
||||
|
|
|
@ -3,34 +3,33 @@
|
|||
[midje.sweet :refer :all]))
|
||||
|
||||
(fact "Get current state returns last state"
|
||||
(let [states [{:tag "1" :a :b}
|
||||
{:tag "2" :c :d}]]
|
||||
(s/get-latest states) => {:tag "2" :c :d}))
|
||||
(let [states [{:tag 1 :a :b}
|
||||
{:tag 2 :c :d}]]
|
||||
(s/get-latest states) => {:tag 2 :c :d}))
|
||||
|
||||
(fact "Get history state returns state with given hash"
|
||||
(let [states [{:tag "1" :a :b}
|
||||
{:tag "2" :c :d}
|
||||
{:tag "3" :e :f}]]
|
||||
(s/get-tagged states "1") => {:tag "1" :a :b}
|
||||
(s/get-tagged states "2") => {:tag "2" :c :d}
|
||||
(s/get-tagged states "3") => {:tag "3" :e :f}))
|
||||
(let [states [{:tag 1 :a :b}
|
||||
{:tag 2 :c :d}
|
||||
{:tag 3 :e :f}]]
|
||||
(s/get-tagged states 1) => {:tag 1 :a :b}
|
||||
(s/get-tagged states 2) => {:tag 2 :c :d}
|
||||
(s/get-tagged states 3) => {:tag 3 :e :f}))
|
||||
|
||||
(fact "Add history state appends state to the end"
|
||||
(let [states [{:tag "1" :a :b}
|
||||
{:tag "2" :c :d}]]
|
||||
(-> (s/add states {:e :f})
|
||||
(last)
|
||||
(dissoc :tag))
|
||||
=> {:e :f}))
|
||||
(fact "Add history state appends state to the end and increments tag"
|
||||
(let [states [{:tag 1 :a :b}
|
||||
{:tag 2 :c :d}]]
|
||||
(s/add states {:e :f}) => [{:tag 1 :a :b}
|
||||
{:tag 2 :c :d}
|
||||
{:tag 3 :e :f}]))
|
||||
|
||||
(fact "Add history state appends state to the end and drops first state if full"
|
||||
(let [states (into [] (for [i (range 20)] {:tag (str i) :i i}))
|
||||
(let [states (into [] (for [i (range 20)] {:tag i :i i}))
|
||||
new-states (s/add states {:i 21})]
|
||||
(count new-states) => 20
|
||||
(dissoc (last new-states) :tag) => {:i 21}
|
||||
(first new-states) => {:tag "1" :i 1}))
|
||||
(first new-states) => {:tag 1 :i 1}))
|
||||
|
||||
(fact "Add history state does not add consecutive duplicate states"
|
||||
(let [states [{:tag "1" :a :b}
|
||||
{:tag "2" :c :d}]]
|
||||
(s/add states {:tag "2" :c :d}) => states))
|
||||
(let [states [{:tag 1 :a :b}
|
||||
{:tag 2 :c :d}]]
|
||||
(s/add states {:tag 2 :c :d}) => states))
|
||||
|
|
|
@ -3,150 +3,159 @@
|
|||
[grub.sync :as sync]
|
||||
[midje.sweet :refer :all]))
|
||||
|
||||
(facts "Server diff"
|
||||
(fact "Server applies diff, returns empty diff with client tag, new server tag when no server changes"
|
||||
(let [{:keys [new-states out-event new-shadow]}
|
||||
(facts "Server"
|
||||
(fact "Diff, no server changes - Apply diff, return empty diff"
|
||||
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||
{:keys [out-event new-shadow]}
|
||||
(sync/handle-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 {}}])
|
||||
:tag 4
|
||||
:shadow-tag 0
|
||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||
:states states
|
||||
: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 {}}))
|
||||
@states => (just (just {:tag 0
|
||||
:grubs {"1" {:completed false, :text "2 apples"}}
|
||||
:recipes {}})
|
||||
(just {:tag 1
|
||||
: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}})})
|
||||
:diff {:grubs {:- #{} :+ nil} :recipes {:- #{}, :+ nil}}
|
||||
:shadow-tag 4
|
||||
:tag 1})
|
||||
new-shadow => {:tag 4
|
||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||
:recipes {}}))
|
||||
|
||||
(fact "Server applies diff and returns changes when server has changed"
|
||||
(let [{:keys [new-states new-shadow out-event]}
|
||||
(fact "Diff, server changes - Apply diff, return changes"
|
||||
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||
"2" {:text "3 onions" :completed false}}
|
||||
:recipes {}}])
|
||||
{:keys [new-shadow out-event]}
|
||||
(sync/handle-event
|
||||
{:type :diff
|
||||
:diff {:shadow-tag 0 :tag 4
|
||||
:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||
"2" {:text "3 onions" :completed false}}
|
||||
:recipes {}}])
|
||||
:shadow-tag 0
|
||||
:tag 4
|
||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||
:states states
|
||||
:shadow state/empty-state
|
||||
:client? false})]
|
||||
new-states =>
|
||||
@states =>
|
||||
(just {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||
"2" {:text "3 onions" :completed false}}
|
||||
:recipes {}}
|
||||
(just {:tag #(not (nil? %)) :grubs {"1" {:text "2 apples" :completed true}
|
||||
"2" {:text "3 onions" :completed false}}
|
||||
:recipes {}}))
|
||||
"2" {:text "3 onions" :completed false}} :recipes {}}
|
||||
{:tag 2 :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}})})
|
||||
:shadow-tag 4
|
||||
:tag 2
|
||||
:diff {: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"
|
||||
(fact "Diff, client out of sync - Force full 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 #{}}}
|
||||
:shadow-tag 3
|
||||
:tag 12
|
||||
:diff {: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
|
||||
{:keys [new-shadow out-event]} (sync/handle-event event)]
|
||||
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 "New state - Update state, send diff, update shadow assuming diff received"
|
||||
(let [states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||
event {:type :new-state
|
||||
:states states
|
||||
:shadow {:tag 3 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||
:client? false
|
||||
:new-state {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}}
|
||||
{:keys [new-shadow out-event]} (sync/handle-event event)]
|
||||
@states => [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||
{:tag 15 :grubs {"1" {:text "2 apples" :completed true}} :recipes {}}]
|
||||
new-shadow => {:tag 4 :grubs {"1" {:text "2 apples" :completed true}} :recipes {}}
|
||||
out-event => {:type :diff
|
||||
:shadow-tag 3
|
||||
:tag 15
|
||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}
|
||||
:recipes {:+ nil :- #{}}}}))
|
||||
|
||||
(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 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-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}}
|
||||
:recipes {}}}))
|
||||
)
|
||||
|
||||
(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 {}}])
|
||||
(fact "Client applies diff, does not return diff when no client changes"
|
||||
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||
event {:type :diff
|
||||
:shadow-tag 0
|
||||
:tag 4
|
||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||
:states states
|
||||
: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 =>
|
||||
{:keys [new-shadow out-event]} (sync/handle-event event)]
|
||||
@states =>
|
||||
(just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
||||
(just {:tag #(not (nil? %))
|
||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||
:recipes {}}))
|
||||
{:tag 1
|
||||
: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}})})))
|
||||
out-event => 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 {}}])
|
||||
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])]
|
||||
(sync/handle-event
|
||||
{:type :diff
|
||||
:shadow-tag 0
|
||||
:tag 4
|
||||
:diff {:grubs {:+ nil :- #{}}}
|
||||
:states states
|
||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||
:client? true})
|
||||
@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 :- #{}}}
|
||||
:shadow-tag 0
|
||||
:tag 4
|
||||
:diff {: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})
|
||||
|
@ -156,7 +165,9 @@
|
|||
(fact "Client updates server shadow on empty diff"
|
||||
(-> (sync/handle-event
|
||||
{:type :diff
|
||||
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
||||
:shadow-tag 0
|
||||
:tag 4
|
||||
:diff {: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})
|
||||
|
@ -171,7 +182,6 @@
|
|||
{: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}
|
||||
|
|
Loading…
Reference in a new issue