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:
Nicholas Kariniemi 2014-10-25 15:43:39 +03:00
parent 79e64596ae
commit 5ee40a6471
10 changed files with 224 additions and 223 deletions

View file

@ -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)]

View file

@ -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))

View file

@ -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.)

View file

@ -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 {})))

View file

@ -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)))))

View file

@ -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)

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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}