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
|
:stop-server nil
|
||||||
:states (atom nil)})
|
:states (atom nil)})
|
||||||
|
|
||||||
(defn handle-websocket [handler states new-states]
|
(defn handle-websocket [handler states new-states-pub]
|
||||||
(fn [{:keys [websocket?] :as request}]
|
(fn [{:keys [websocket?] :as request}]
|
||||||
(if websocket?
|
(if websocket?
|
||||||
(httpkit/with-channel request ws-channel
|
(httpkit/with-channel request ws-channel
|
||||||
(let [to-client (chan)
|
(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)
|
(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))))
|
(handler request))))
|
||||||
|
|
||||||
(defn handle-root [handler index]
|
(defn handle-root [handler index]
|
||||||
|
@ -80,20 +82,21 @@
|
||||||
:body ""}
|
:body ""}
|
||||||
(handler req))))
|
(handler req))))
|
||||||
|
|
||||||
(defn make-handler [{:keys [index states]} new-states]
|
(defn make-handler [{:keys [index states]} new-states-pub]
|
||||||
(-> (fn [req] "Not found")
|
(-> (fn [req] "Not found")
|
||||||
(file/wrap-file "public")
|
(file/wrap-file "public")
|
||||||
(content-type/wrap-content-type)
|
(content-type/wrap-content-type)
|
||||||
(handle-root index)
|
(handle-root index)
|
||||||
(handle-websocket states new-states)
|
(handle-websocket states new-states-pub)
|
||||||
(wrap-bounce-favicon)))
|
(wrap-bounce-favicon)))
|
||||||
|
|
||||||
(defn start [{:keys [port db-name states] :as system}]
|
(defn start [{:keys [port db-name states] :as system}]
|
||||||
(let [{:keys [db conn]} (db/connect db-name)
|
(let [{:keys [db conn]} (db/connect db-name)
|
||||||
new-states (chan)
|
new-states (chan)
|
||||||
|
new-states-pub (a/pub new-states (fn [_] :new-state))
|
||||||
db-state (db/get-current-state db)
|
db-state (db/get-current-state db)
|
||||||
_ (reset! states (state/new-states (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-pub) {: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-latest new)]
|
(let [new-state (state/get-latest new)]
|
||||||
|
|
|
@ -18,8 +18,9 @@
|
||||||
|
|
||||||
(defn read-msg [msg]
|
(defn read-msg [msg]
|
||||||
(let [in (ByteArrayInputStream. (.getBytes msg))
|
(let [in (ByteArrayInputStream. (.getBytes msg))
|
||||||
reader (t/reader in :json)]
|
reader (t/reader in :json)
|
||||||
(t/read reader)))
|
received (t/read reader)]
|
||||||
|
received))
|
||||||
|
|
||||||
(defn add-connected-client! [ws-channel to from]
|
(defn add-connected-client! [ws-channel to from]
|
||||||
(println "Client connected:" (.toString ws-channel))
|
(println "Client connected:" (.toString ws-channel))
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
(send-pending-msg websocket pending-msg))
|
(send-pending-msg websocket pending-msg))
|
||||||
|
|
||||||
(defn read-msg [msg]
|
(defn read-msg [msg]
|
||||||
(t/read reader (.-message msg)))
|
(let [received (t/read reader (.-message msg))]
|
||||||
|
received))
|
||||||
|
|
||||||
(defn connect [pending-msg in out]
|
(defn connect [pending-msg in out]
|
||||||
(let [ws (goog.net.WebSocket.)
|
(let [ws (goog.net.WebSocket.)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
(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))))
|
||||||
|
@ -22,27 +21,19 @@
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(defn diff-states [prev next]
|
(defn diff-states [prev next]
|
||||||
(let [key-diffs (diff-keys prev next)]
|
(->> prev
|
||||||
(if (and (:tag prev) (:tag next))
|
(keys)
|
||||||
(assoc key-diffs
|
(map (fn [k] [k (diff-maps (k prev) (k next))]))
|
||||||
:shadow-tag (:tag prev)
|
(filter #(not (nil? (second %))))
|
||||||
:tag (:tag next))
|
(into {})))
|
||||||
key-diffs)))
|
|
||||||
|
|
||||||
(defn patch-map [state diff]
|
(defn patch-map [state diff]
|
||||||
(-> state
|
(-> state
|
||||||
(#(apply dissoc % (into [] (:- diff))))
|
(#(apply dissoc % (into [] (:- diff))))
|
||||||
(#(merge-with merge % (:+ diff)))))
|
(#(merge-with merge % (:+ diff)))))
|
||||||
|
|
||||||
(defn patch-state
|
(defn patch-state [state diff]
|
||||||
([state diff] (patch-state state diff false))
|
(->> state
|
||||||
([state diff use-diff-tag?]
|
(keys)
|
||||||
(let [patched (->> state
|
(map (fn [k] [k (patch-map (k state) (k diff))]))
|
||||||
(keys)
|
(into {})))
|
||||||
(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,14 +1,13 @@
|
||||||
(ns grub.state
|
(ns grub.state
|
||||||
(:require [grub.diff :as diff]
|
(:require [grub.diff :as diff]
|
||||||
[grub.util :as util]
|
[grub.util :as util]))
|
||||||
[grub.tag :as tag]))
|
|
||||||
|
|
||||||
(def num-history-states 20)
|
(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]
|
(defn new-states [state]
|
||||||
[(assoc state :tag (tag/new-tag))])
|
[(assoc state :tag 0)])
|
||||||
|
|
||||||
(defn get-latest [states]
|
(defn get-latest [states]
|
||||||
(last states))
|
(last states))
|
||||||
|
@ -22,7 +21,7 @@
|
||||||
(let [last-state (last states)]
|
(let [last-state (last states)]
|
||||||
(if (= last-state new-state)
|
(if (= last-state new-state)
|
||||||
states
|
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)
|
(if (>= (count states) num-history-states)
|
||||||
(into [] (rest new-states))
|
(into [] (rest new-states))
|
||||||
new-states)))))
|
new-states)))))
|
||||||
|
|
|
@ -22,24 +22,27 @@
|
||||||
(defn diff-msg [shadow state]
|
(defn diff-msg [shadow state]
|
||||||
(let [diff (diff/diff-states shadow state)]
|
(let [diff (diff/diff-states shadow state)]
|
||||||
{:type :diff
|
{:type :diff
|
||||||
:diff diff}))
|
:diff diff
|
||||||
|
:tag (:tag state)
|
||||||
|
:shadow-tag (:tag shadow)}))
|
||||||
|
|
||||||
(defmulti handle-event (fn [event]
|
(defmulti handle-event (fn [event] (:type 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)
|
(let [new-states (swap! states update-states diff)
|
||||||
new-state (state/get-latest new-states)
|
new-state (state/get-latest new-states)
|
||||||
new-shadow (diff/patch-state shadow diff true)]
|
new-shadow (assoc (diff/patch-state shadow diff)
|
||||||
{:out-event (when-not (state/state= shadow new-state)
|
:tag new-shadow-tag)]
|
||||||
(diff-msg new-shadow new-state))
|
{:new-shadow new-shadow
|
||||||
:new-states new-states
|
;; Workaround to send an "ACK" diff when there are no changes
|
||||||
:new-shadow new-shadow}))
|
: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?]}]
|
(defmethod handle-event :diff [{:keys [diff states shadow shadow-tag tag client?]}]
|
||||||
(let [history-shadow (state/get-tagged @states (:shadow-tag diff))]
|
(let [history-shadow (state/get-tagged @states shadow-tag)]
|
||||||
(if history-shadow
|
(if history-shadow
|
||||||
(apply-diff states diff history-shadow)
|
(apply-diff states diff history-shadow tag client?)
|
||||||
(if client?
|
(if client?
|
||||||
{:out-event full-sync-request
|
{:out-event full-sync-request
|
||||||
:new-shadow shadow}
|
:new-shadow shadow}
|
||||||
|
@ -56,45 +59,79 @@
|
||||||
(reset! states (state/new-states full-state))
|
(reset! states (state/new-states full-state))
|
||||||
{:new-shadow 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]
|
(defmethod handle-event :default [msg]
|
||||||
#+cljs (logs "Unhandled message:" msg)
|
#+cljs (logs "Unhandled message:" msg)
|
||||||
#+clj (println "Unhandled message:" msg)
|
#+clj (println "Unhandled message:" msg)
|
||||||
{})
|
{})
|
||||||
|
|
||||||
(defn make-agent
|
(defn make-server-agent
|
||||||
([client? >remote events new-states states]
|
([>remote events new-states states]
|
||||||
(make-agent client? >remote events new-states states state/empty-state))
|
(make-server-agent >remote events new-states states state/empty-state))
|
||||||
([client? >remote events new-states states initial-shadow]
|
([>remote events new-states states initial-shadow]
|
||||||
(go (loop [shadow initial-shadow]
|
(go (loop [shadow initial-shadow]
|
||||||
(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 (state/state= shadow v)
|
(let [event {:type :new-state
|
||||||
(swap! states state/add v)
|
:new-state v
|
||||||
(>! >remote (diff-msg shadow v)))
|
:shadow shadow
|
||||||
(recur 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)
|
(= c events)
|
||||||
(let [event (assoc v
|
(let [event (assoc v
|
||||||
:states states
|
:states states
|
||||||
:client? client?
|
:client? false
|
||||||
:shadow shadow)
|
:shadow shadow)
|
||||||
{:keys [new-shadow out-event]} (handle-event event)]
|
{:keys [new-shadow out-event]} (handle-event event)]
|
||||||
(when out-event (a/put! >remote out-event))
|
(when out-event (a/put! >remote out-event))
|
||||||
(recur (if new-shadow new-shadow shadow)))))))))
|
(recur (if new-shadow new-shadow shadow)))))))))
|
||||||
|
|
||||||
(def make-server-agent (partial make-agent false))
|
(defn make-client-agent
|
||||||
(def make-client-agent (partial make-agent true))
|
([>remote events new-states states]
|
||||||
|
(make-client-agent >remote events new-states states state/empty-state))
|
||||||
#+clj
|
([>remote events new-states states initial-shadow]
|
||||||
(defn sync-new-client! [>remote events new-states states]
|
(go (loop [shadow initial-shadow
|
||||||
(make-server-agent >remote events new-states states))
|
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
|
#+cljs
|
||||||
(defn sync-client! [>remote events new-states states]
|
(defn sync-client! [>remote events new-states states]
|
||||||
(let [new-states* (chan)]
|
(let [new-states* (chan (a/sliding-buffer 1))]
|
||||||
(go (loop []
|
(go (loop []
|
||||||
(let [v (<! new-states)]
|
(let [v (<! new-states)]
|
||||||
(<! (a/timeout 1000))
|
|
||||||
(>! new-states* v)
|
(>! new-states* v)
|
||||||
(recur))))
|
(recur))))
|
||||||
(make-client-agent >remote events new-states* states)
|
(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"
|
(fact "Diff of many changes has all changes"
|
||||||
(diff/diff-states before-state after-state) => expected-diff)
|
(diff/diff-states before-state after-state) => expected-diff)
|
||||||
|
|
||||||
(fact "Diff and patch of many changes returns original state with new tag"
|
(fact "Diff and patch of many changes returns original state"
|
||||||
(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)]
|
(diff/patch-state before-state diff) => after-state))
|
||||||
(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})
|
|
||||||
|
|
|
@ -3,34 +3,33 @@
|
||||||
[midje.sweet :refer :all]))
|
[midje.sweet :refer :all]))
|
||||||
|
|
||||||
(fact "Get current state returns last state"
|
(fact "Get current state returns last state"
|
||||||
(let [states [{:tag "1" :a :b}
|
(let [states [{:tag 1 :a :b}
|
||||||
{:tag "2" :c :d}]]
|
{:tag 2 :c :d}]]
|
||||||
(s/get-latest states) => {:tag "2" :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 [{:tag "1" :a :b}
|
(let [states [{:tag 1 :a :b}
|
||||||
{:tag "2" :c :d}
|
{:tag 2 :c :d}
|
||||||
{:tag "3" :e :f}]]
|
{:tag 3 :e :f}]]
|
||||||
(s/get-tagged states "1") => {:tag "1" :a :b}
|
(s/get-tagged states 1) => {:tag 1 :a :b}
|
||||||
(s/get-tagged states "2") => {:tag "2" :c :d}
|
(s/get-tagged states 2) => {:tag 2 :c :d}
|
||||||
(s/get-tagged states "3") => {:tag "3" :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 and increments tag"
|
||||||
(let [states [{:tag "1" :a :b}
|
(let [states [{:tag 1 :a :b}
|
||||||
{:tag "2" :c :d}]]
|
{:tag 2 :c :d}]]
|
||||||
(-> (s/add states {:e :f})
|
(s/add states {:e :f}) => [{:tag 1 :a :b}
|
||||||
(last)
|
{:tag 2 :c :d}
|
||||||
(dissoc :tag))
|
{:tag 3 :e :f}]))
|
||||||
=> {: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)] {:tag (str i) :i i}))
|
(let [states (into [] (for [i (range 20)] {:tag i :i i}))
|
||||||
new-states (s/add states {:i 21})]
|
new-states (s/add states {:i 21})]
|
||||||
(count new-states) => 20
|
(count new-states) => 20
|
||||||
(dissoc (last new-states) :tag) => {:i 21}
|
(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"
|
(fact "Add history state does not add consecutive duplicate states"
|
||||||
(let [states [{:tag "1" :a :b}
|
(let [states [{:tag 1 :a :b}
|
||||||
{:tag "2" :c :d}]]
|
{:tag 2 :c :d}]]
|
||||||
(s/add states {:tag "2" :c :d}) => states))
|
(s/add states {:tag 2 :c :d}) => states))
|
||||||
|
|
|
@ -3,150 +3,159 @@
|
||||||
[grub.sync :as sync]
|
[grub.sync :as sync]
|
||||||
[midje.sweet :refer :all]))
|
[midje.sweet :refer :all]))
|
||||||
|
|
||||||
(facts "Server diff"
|
(facts "Server"
|
||||||
(fact "Server applies diff, returns empty diff with client tag, new server tag when no server changes"
|
(fact "Diff, no server changes - Apply diff, return empty diff"
|
||||||
(let [{:keys [new-states out-event new-shadow]}
|
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
|
{:keys [out-event new-shadow]}
|
||||||
(sync/handle-event
|
(sync/handle-event
|
||||||
{:type :diff
|
{:type :diff
|
||||||
:diff {:tag 4 :shadow-tag 0 :grubs {:+ {"1" {:completed true}} :- #{}}}
|
:tag 4
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
:shadow-tag 0
|
||||||
|
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
|
:states states
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:client? false})]
|
:client? false})]
|
||||||
new-states => (just (just {:tag 0
|
@states => (just (just {:tag 0
|
||||||
:grubs {"1" {:completed false, :text "2 apples"}}
|
:grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
:recipes {}})
|
:recipes {}})
|
||||||
(just {:tag #(not (nil? %))
|
(just {:tag 1
|
||||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
:recipes {}}))
|
:recipes {}}))
|
||||||
out-event => (just {:type :diff
|
out-event => (just {:type :diff
|
||||||
:diff (just {:shadow-tag 4
|
:diff {:grubs {:- #{} :+ nil} :recipes {:- #{}, :+ nil}}
|
||||||
:tag #(not (nil? %))
|
:shadow-tag 4
|
||||||
:grubs {:- #{} :+ nil}
|
:tag 1})
|
||||||
:recipes {:- #{}, :+ nil}})})
|
|
||||||
new-shadow => {:tag 4
|
new-shadow => {:tag 4
|
||||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
:recipes {}}))
|
:recipes {}}))
|
||||||
|
|
||||||
(fact "Server applies diff and returns changes when server has changed"
|
(fact "Diff, server changes - Apply diff, return changes"
|
||||||
(let [{:keys [new-states new-shadow out-event]}
|
(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
|
(sync/handle-event
|
||||||
{:type :diff
|
{:type :diff
|
||||||
:diff {:shadow-tag 0 :tag 4
|
:shadow-tag 0
|
||||||
:grubs {:+ {"1" {:completed true}} :- #{}}}
|
:tag 4
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
:states states
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}])
|
|
||||||
:shadow state/empty-state
|
:shadow state/empty-state
|
||||||
:client? false})]
|
:client? false})]
|
||||||
new-states =>
|
@states =>
|
||||||
(just {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
(just {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
"2" {:text "3 onions" :completed false}} :recipes {}}
|
||||||
:recipes {}}
|
{:tag 2 :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 {}})
|
||||||
"2" {:text "3 onions" :completed false}}
|
|
||||||
:recipes {}}))
|
|
||||||
out-event =>
|
out-event =>
|
||||||
(just {:type :diff
|
(just {:type :diff
|
||||||
:diff (just {:shadow-tag 4
|
:shadow-tag 4
|
||||||
:tag #(not (nil? %))
|
:tag 2
|
||||||
:grubs {:- #{} :+ {"2" {:completed false, :text "3 onions"}}}
|
:diff {:grubs {:- #{} :+ {"2" {:completed false, :text "3 onions"}}}
|
||||||
:recipes {:- #{}, :+ nil}})})
|
:recipes {:- #{}, :+ nil}}})
|
||||||
new-shadow => {:tag 4
|
new-shadow => {:tag 4
|
||||||
:grubs {"1" {:text "2 apples" :completed true}}
|
:grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}))
|
: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 {}}
|
(let [states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
"2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}])
|
:recipes {}}])
|
||||||
event {:type :diff
|
event {:type :diff
|
||||||
:diff {:shadow-tag 3 :tag 12
|
:shadow-tag 3
|
||||||
:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
:tag 12
|
||||||
|
:diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
||||||
:states states
|
:states states
|
||||||
:shadow state/empty-state
|
:shadow state/empty-state
|
||||||
:client? false}
|
:client? false}
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
{:keys [new-shadow out-event]} (sync/handle-event event)]
|
||||||
new-states => nil
|
|
||||||
out-event => {:type :full-sync
|
out-event => {:type :full-sync
|
||||||
:full-state {:tag 15
|
:full-state {:tag 15
|
||||||
:grubs {"1" {:text "2 apples" :completed false}
|
:grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
"2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}}))
|
:recipes {}}}))
|
||||||
|
|
||||||
(fact "Server state is unchanged on receiving empty diff"
|
(fact "New state - Update state, send diff, update shadow assuming diff received"
|
||||||
(-> (sync/handle-event
|
(let [states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
{:type :diff
|
event {:type :new-state
|
||||||
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
:states states
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
:shadow {:tag 3 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
:client? false
|
||||||
:client? false})
|
:new-state {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}}
|
||||||
:new-states)
|
{:keys [new-shadow out-event]} (sync/handle-event event)]
|
||||||
=> [{:tag 0
|
@states => [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:grubs {"1" {:completed false, :text "2 apples"}}
|
{:tag 15 :grubs {"1" {:text "2 apples" :completed true}} :recipes {}}]
|
||||||
: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"
|
(fact "Server sends full sync if client requests it"
|
||||||
(-> (sync/handle-event
|
(let [result (sync/handle-event
|
||||||
{:type :diff
|
{:type :full-sync-request
|
||||||
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
:states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
"2" {:text "3 onions" :completed false}}
|
||||||
:client? false})
|
:recipes {}}])})]
|
||||||
:out-event)
|
(:new-shadow result) =>
|
||||||
=> nil)
|
(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"
|
(facts "Client diffs"
|
||||||
(fact "Client applies diff, returns empty diff with server tag, new client tag when no client changes"
|
(fact "Client applies diff, does not return diff when no client changes"
|
||||||
(let [event {:type :diff
|
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
:diff {:tag 4 :shadow-tag 0 :grubs {:+ {"1" {:completed true}} :- #{}}}
|
event {:type :diff
|
||||||
:states (atom
|
:shadow-tag 0
|
||||||
[{:tag 0 :grubs {"1" {:text "2 apples" :completed false}}
|
:tag 4
|
||||||
:recipes {}}])
|
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
|
:states states
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}}
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}
|
:recipes {}}
|
||||||
:client? true}
|
:client? true}
|
||||||
{:keys [new-states new-shadow out-event]} (sync/handle-event event)]
|
{:keys [new-shadow out-event]} (sync/handle-event event)]
|
||||||
new-states =>
|
@states =>
|
||||||
(just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
(just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
||||||
(just {:tag #(not (nil? %))
|
{:tag 1
|
||||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
:grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
:recipes {}}))
|
:recipes {}})
|
||||||
new-shadow {:tag 4 :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
|
out-event => nil))
|
||||||
:diff (just {:shadow-tag 4
|
|
||||||
:tag #(not (nil? %))
|
|
||||||
:grubs {:- #{} :+ nil}
|
|
||||||
:recipes {:- #{}, :+ nil}})})))
|
|
||||||
|
|
||||||
(fact "Client state is unchanged on receiving empty diff"
|
(fact "Client state is unchanged on receiving empty diff"
|
||||||
(-> (sync/handle-event
|
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])]
|
||||||
{:type :diff
|
(sync/handle-event
|
||||||
:diff {:tag 4 :shadow-tag 0 :grubs {:+ nil :- #{}}}
|
{:type :diff
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
:shadow-tag 0
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
:tag 4
|
||||||
:client? true})
|
:diff {:grubs {:+ nil :- #{}}}
|
||||||
:new-states)
|
:states states
|
||||||
=> [{:tag 0
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:grubs {"1" {:completed false, :text "2 apples"}}
|
:client? true})
|
||||||
:recipes {}}])
|
@states => [{:tag 0
|
||||||
|
:grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
|
:recipes {}}]))
|
||||||
|
|
||||||
(fact "Client returns no response on empty diff"
|
(fact "Client returns no response on empty diff"
|
||||||
(-> (sync/handle-event
|
(-> (sync/handle-event
|
||||||
{:type :diff
|
{: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 {}}])
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:client? true})
|
:client? true})
|
||||||
|
@ -156,7 +165,9 @@
|
||||||
(fact "Client updates server shadow on empty diff"
|
(fact "Client updates server shadow on empty diff"
|
||||||
(-> (sync/handle-event
|
(-> (sync/handle-event
|
||||||
{:type :diff
|
{: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 {}}])
|
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:client? true})
|
:client? true})
|
||||||
|
@ -171,7 +182,6 @@
|
||||||
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
{:tag 15 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
"2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}])})]
|
:recipes {}}])})]
|
||||||
(:new-states result) => nil
|
|
||||||
(:new-shadow result) =>
|
(:new-shadow result) =>
|
||||||
(just {:tag #(not (nil? %))
|
(just {:tag #(not (nil? %))
|
||||||
:grubs {"1" {:text "2 apples" :completed false}
|
:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
|
Loading…
Reference in a new issue