Working sync for single client (no changes from others)

This commit is contained in:
Nicholas Kariniemi 2015-11-20 02:14:07 -05:00
parent a8d8ae56c5
commit 798f1a5891
10 changed files with 386 additions and 388 deletions

View file

@ -49,18 +49,29 @@
:port 3000
:stop-server nil})
(defn sync-client-with-db! [ws-channel db-conn]
(let [from-client (chan)
to-client (chan)
diffs (chan)
full-sync-reqs (chan)
on-close (fn []
(a/close! from-client)
(a/close! to-client)
(a/close! diffs)
(a/close! full-sync-reqs))]
(ws/add-connected-client! ws-channel to-client from-client on-close)
(sync/sync-server! to-client diffs full-sync-reqs db-conn)
(go (loop [] (let [event (<! from-client)]
(cond
(nil? event) nil ;; drop out of loop
(= (:type event) :diff) (do (>! diffs event) (recur))
(= (:type event) :full-sync-request) (do (>! full-sync-reqs event) (recur))
:else (do (println "Unknown event:" event) (recur))))))))
(defn handle-websocket [handler db-conn]
(fn [{:keys [websocket?] :as request}]
(if websocket?
(httpkit/with-channel request ws-channel
(let [up (chan)
down (chan)
saved (chan)
on-close (fn []
(a/close! up)
(a/close! down))]
(ws/add-connected-client! ws-channel down up on-close)
(sync/make-server-agent up down saved db-conn)))
(httpkit/with-channel request ws-channel (sync-client-with-db! ws-channel db-conn))
(handler request))))
(defn handle-root [handler index]

View file

@ -72,11 +72,11 @@
conn))
(def all-grubs-query
[:find '?id '?text '?complete
[:find '?id '?text '?completed
:where
['?e :grub/id '?id]
['?e :grub/text '?text]
['?e :grub/completed '?complete]])
['?e :grub/completed '?completed]])
(def all-recipes-query
[:find '?id '?name '?grubs '?directions
@ -86,8 +86,8 @@
['?e :recipe/grubs '?grubs]
['?e :recipe/directions '?directions]])
(defn grub-as-map [[id text complete]]
{:id id :text text :complete complete})
(defn grub-as-map [[id text completed]]
{:id id :text text :completed completed})
(defn recipe-as-map [[id name grubs directions]]
{:id id :name name :grubs grubs :directions directions})

View file

@ -1,85 +1,53 @@
(ns grub.server-sync
(:require [grub.diff :as diff]
[grub.state :as state]
[datomic.api :as d]
[clojure.core.async :as a :refer [<! >! chan go]]
[grub.db :as db]
[clojure.pprint :refer [pprint]]))
(defn full-sync [state]
(defn full-sync [state tag]
{:type :full-sync
:full-state state})
:full-state state
:tag tag})
(def empty-state state/empty-state)
(defn update-states [states diff]
(let [state (state/get-latest states)
new-state (diff/patch-state state diff)]
(state/add states new-state)))
(defn diff-msg [shadow state]
(println "diff-msg")
(let [diff (diff/diff-states shadow state)]
{:type :diff
:diff diff
:tag (:tag state)
:shadow-tag (:tag shadow)}))
(defmulti handle-event (fn [event] (:type event)))
(defn apply-diff [states diff shadow new-shadow-tag]
(let [new-states (swap! states update-states diff)
new-state (state/get-latest new-states)
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 (state/state= new-state new-shadow)
(diff-msg new-shadow new-state))}))
(defmethod handle-event :diff [{:keys [diff states shadow-tag tag]}]
(let [history-shadow (state/get-tagged @states shadow-tag)]
(if history-shadow
(apply-diff states diff history-shadow tag)
(let [state (state/get-latest @states)]
{:out-event (full-sync state)
:new-shadow state}))))
(defmethod handle-event :full-sync-request [{:keys [states]}]
(let [state (state/get-latest @states)]
{:new-shadow state
:out-event (full-sync state)}))
(defmethod handle-event :new-state [{:keys [shadow states new-state]}]
(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-not (state/state= shadow latest-state)
(assoc latest-state :tag (inc (:tag shadow))))}))
(defmethod handle-event :default [msg]
(println "Unhandled message:" msg)
{})
(defn make-server-agent [up down saved db-conn]
(go (loop [shadow (db/get-current-state db-conn)]
(let [[event c] (a/alts! [up saved] :priority true)]
(defn sync-server! [to-client diffs full-sync-reqs db-conn]
(go (loop []
(let [[event ch] (a/alts! [full-sync-reqs diffs])]
(when-not (nil? event)
(case (:type event)
(condp = ch
diffs
(let [{:keys [diff shadow-tag tag]} event
client-shadow-db (d/as-of (d/db db-conn) shadow-tag)
client-shadow-state (db/get-current-db-state client-shadow-db)
{:keys [db-after]} (db/patch-state! db-conn diff)
new-tag (d/basis-t db-after)
new-state (assoc (db/get-current-db-state db-after) :tag new-tag)
new-shadow (assoc (diff/patch-state client-shadow-state diff) :tag tag)
return-diff (diff-msg new-shadow new-state)]
(println "************************* as-of:" new-tag)
(println "client-shadow:" (pprint (dissoc client-shadow-state :recipes)))
(println "new-state:" (pprint (dissoc new-state :recipes)))
(println "new-shadow" (pprint (dissoc new-shadow :recipes)))
;(println "**************************history-state:" history-state)
;(println "**************************new-state:" new-state)
;(println "**************************new-shadow:" new-shadow)
;(println "return diff:" return-diff)
(>! to-client return-diff)
(recur))
:diff
(let [history-state (db/get-history-state db-conn (:shadow-tag event))
new-state (db/patch-state! db-conn (:diff event))
new-shadow (diff/patch-state history-state (:diff event))
return-diff (diff/diff-states new-shadow new-state)]
(println "**************************history-state:" history-state)
(println "**************************new-state:" new-state)
(println "**************************new-shadow:" new-shadow)
(println "return diff:" return-diff)
(>! down return-diff)
(recur new-shadow))
:full-sync-request
(do (>! down (full-sync (db/get-current-state db-conn)))
(recur shadow))
full-sync-reqs
(do (>! to-client (full-sync (db/get-current-state db-conn) (d/basis-t (d/db db-conn))))
(recur))
(do (println "Unhandled event:" event)
(recur shadow))))))))
(recur))))))))

View file

@ -4,7 +4,7 @@
[cognitect.transit :as t])
(:import [java.io ByteArrayInputStream ByteArrayOutputStream]))
(def DEBUG true)
(def DEBUG false)
(defn write-msg [msg]
(let [out (ByteArrayOutputStream. 4096)

View file

@ -25,30 +25,33 @@
(let [ui-state-buffer (chan (a/sliding-buffer 1))]
(a/pipe new-ui-states ui-state-buffer)
(reset! ui-state state/empty-state)
(go (loop [states (state/new-states @ui-state)
shadow (state/get-latest states)
(go (loop [state (assoc @ui-state :tag 0)
shadow state
awaiting-ack? false]
(let [channels (if awaiting-ack? [diffs full-syncs] [diffs full-syncs ui-state-buffer])]
(let [[val ch] (a/alts! channels)]
(when DEBUG (println val))
(let [[event ch] (a/alts! channels)]
(when DEBUG (println event))
(when-not (nil? event)
(condp = ch
ui-state-buffer (let [new-state val
new-states (state/add states new-state)
latest-state (state/get-latest new-states)]
(>! to-server (diff-msg shadow latest-state))
(recur new-states shadow true))
full-syncs (let [full-state (:full-state val)
new-states (state/new-states full-state)
latest-state (state/get-latest new-states)]
ui-state-buffer (let [new-state (assoc event :tag (inc (:tag state)))]
(println "new-state:\n" new-state)
(>! to-server (diff-msg shadow new-state))
(recur new-state shadow true))
full-syncs (let [{:keys [full-state tag]} event
new-tag (inc (:tag state))
new-state (assoc full-state :tag new-tag)]
(reset! ui-state full-state)
(recur new-states latest-state false))
diffs (let [{:keys [diff shadow-tag tag]} val
history-shadow (state/get-tagged states shadow-tag)]
(if history-shadow
(let [new-states (update-states states diff)
new-shadow (assoc (diff/patch-state shadow diff) :tag tag)]
(recur new-states new-shadow false))
(do (>! to-server full-sync-request)
(recur states shadow true))))
(println "An error occurred, received value on unknown channel"))))))
(recur new-state (assoc full-state :tag tag) false))
diffs (let [{:keys [diff shadow-tag tag]} event]
(cond (< shadow-tag (:tag state)) (recur state shadow false)
(= shadow-tag (:tag state))
(let [new-shadow (assoc (diff/patch-state state diff) :tag tag)
new-state (assoc (swap! ui-state diff/patch-state diff) :tag (inc (:tag state)))]
(if (state/state= new-shadow new-state)
(recur new-state new-shadow false)
(do (>! to-server (diff-msg new-shadow new-state))
(recur new-state new-shadow true))))
:else (do (>! to-server (full-sync-request (:tag shadow)))
(recur state shadow true))))
(println "An error occurred, received value on unknown channel")))))))
(a/put! to-server full-sync-request)))

View file

@ -6,8 +6,19 @@
(set/difference (into #{} (keys a)) (into #{} (keys b))))
(defn updated [a b]
(println "*******************updated")
(println "********************a:" a "\n\n\n")
(println "********************b:" b "\n\n\n")
(println "diff:" (second (data/diff a b)) "\n\n\n")
(second (data/diff a b)))
(def a {:grub-e1ff4b5a-05eb-4364-8884-fc124ac1091c {:id :grub-e1ff4b5a-05eb-4364-8884-fc124ac1091c, :text "a", :completed false}})
(def b {:grub-e1ff4b5a-05eb-4364-8884-fc124ac1091c {:id :grub-e1ff4b5a-05eb-4364-8884-fc124ac1091c, :text "a", :completed false}})
(def d (second (data/diff a b)))
(defn diff-maps [a b]
(when (and (map? a) (map? b))
{:- (deleted a b)
@ -21,11 +32,16 @@
(into {})))
(defn diff-states [prev next]
(->> prev
(println "diff states")
(println "prev:" (dissoc prev :recipes))
(println "next:" (dissoc next :recipes))
(let [prev* (dissoc prev :tag)
next* (dissoc next :tag)]
(->> prev*
(keys)
(map (fn [k] [k (diff-maps (k prev) (k next))]))
(map (fn [k] [k (diff-maps (k prev*) (k next*))]))
(filter #(not (nil? (second %))))
(into {})))
(into {}))))
(defn patch-map [state diff]
(-> state

View file

@ -8,7 +8,7 @@
[cljs.core.async.macros :refer [go go-loop]]))
(defn new-grub [text]
{:id (str "grub-" (uuid/make-random))
{:id (keyword (str "grub-" (uuid/make-random)))
:text text
:completed false})

View file

@ -5,86 +5,86 @@
[midje.sweet :refer :all]
[clojure.core.async :as a :refer [<!! >!! chan go]]))
(defn client-server [client-states server-states]
(let [server-shadow (last @server-states)
client-shadow (last @client-states)
new-client-states (chan)
>client (chan)
new-server-states (chan)
>server (chan)]
(client-sync/make-client-agent >server >client new-client-states client-states server-shadow)
(server-sync/make-server-agent >client >server new-server-states server-states client-shadow)
{:new-client-states new-client-states
:new-server-states new-server-states}))
(defn states-in-sync? [a b]
(let [last-a (dissoc (last a) :tag)
last-b (dissoc (last b) :tag)]
last-a => last-b))
(defn last-state [states]
(-> states
(last)
(dissoc :tag)))
(defn short-delay []
(<!! (a/timeout 300)))
(fact "Client-only changes sync with server"
(let [client (atom [{:tag 1
:grubs {"1" {:text "2 apples" :completed false}}
:recipes {}}])
server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
:recipes {}}])
{:keys [new-client-states]} (client-server client server)
client-change {:tag 2
:grubs {"1" {:text "2 apples" :completed true}}
:recipes {}}]
(swap! client conj client-change)
(>!! new-client-states client-change)
(short-delay)
(states-in-sync? @client @server)
(last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
:recipes {}}))
(fact "Other client changes synced with client"
(let [client (atom [{:tag 1
:grubs {"1" {:text "2 apples" :completed false}}
:recipes {}}])
server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
:recipes {}}])
{:keys [new-server-states]} (client-server client server)
server-change {:tag 2
:grubs {"1" {:text "2 apples" :completed true}}
:recipes {}}]
(swap! server conj server-change)
(>!! new-server-states server-change)
(short-delay)
(states-in-sync? @client @server)
(last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
:recipes {}}))
(fact "Client changes and simultaneous server changes synced"
(let [client (atom [{:tag 1
:grubs {"1" {:text "2 apples" :completed false}}
:recipes {}}])
server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
:recipes {}}])
{:keys [new-client-states new-server-states]} (client-server client server)
client-change {:tag 2
:grubs {"1" {:text "2 apples" :completed true}}
:recipes {}}
server-change {:tag 45
:grubs {"1" {:text "2 apples" :completed false}
"2" {:text "milk" :completed false}}
:recipes {}}]
(swap! client conj client-change)
(swap! server conj server-change)
(>!! new-client-states client-change)
(short-delay)
(>!! new-server-states (last @server))
(short-delay)
(states-in-sync? @client @server)
(last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}
"2" {:text "milk" :completed false}}
:recipes {}}))
;(defn client-server [client-states server-states]
; (let [server-shadow (last @server-states)
; client-shadow (last @client-states)
; new-client-states (chan)
; >client (chan)
; new-server-states (chan)
; >server (chan)]
; (client-sync/make-client-agent >server >client new-client-states client-states server-shadow)
; (server-sync/sync-server! >client >server new-server-states server-states client-shadow)
; {:new-client-states new-client-states
; :new-server-states new-server-states}))
;
;(defn states-in-sync? [a b]
; (let [last-a (dissoc (last a) :tag)
; last-b (dissoc (last b) :tag)]
; last-a => last-b))
;
;(defn last-state [states]
; (-> states
; (last)
; (dissoc :tag)))
;
;(defn short-delay []
; (<!! (a/timeout 300)))
;
;(fact "Client-only changes sync with server"
; (let [client (atom [{:tag 1
; :grubs {"1" {:text "2 apples" :completed false}}
; :recipes {}}])
; server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
; :recipes {}}])
; {:keys [new-client-states]} (client-server client server)
; client-change {:tag 2
; :grubs {"1" {:text "2 apples" :completed true}}
; :recipes {}}]
; (swap! client conj client-change)
; (>!! new-client-states client-change)
; (short-delay)
; (states-in-sync? @client @server)
; (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
; :recipes {}}))
;
;(fact "Other client changes synced with client"
; (let [client (atom [{:tag 1
; :grubs {"1" {:text "2 apples" :completed false}}
; :recipes {}}])
; server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
; :recipes {}}])
; {:keys [new-server-states]} (client-server client server)
; server-change {:tag 2
; :grubs {"1" {:text "2 apples" :completed true}}
; :recipes {}}]
; (swap! server conj server-change)
; (>!! new-server-states server-change)
; (short-delay)
; (states-in-sync? @client @server)
; (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
; :recipes {}}))
;
;(fact "Client changes and simultaneous server changes synced"
; (let [client (atom [{:tag 1
; :grubs {"1" {:text "2 apples" :completed false}}
; :recipes {}}])
; server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
; :recipes {}}])
; {:keys [new-client-states new-server-states]} (client-server client server)
; client-change {:tag 2
; :grubs {"1" {:text "2 apples" :completed true}}
; :recipes {}}
; server-change {:tag 45
; :grubs {"1" {:text "2 apples" :completed false}
; "2" {:text "milk" :completed false}}
; :recipes {}}]
; (swap! client conj client-change)
; (swap! server conj server-change)
; (>!! new-client-states client-change)
; (short-delay)
; (>!! new-server-states (last @server))
; (short-delay)
; (states-in-sync? @client @server)
; (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}
; "2" {:text "milk" :completed false}}
; :recipes {}}))

View file

@ -2,34 +2,34 @@
(:require [grub.state :as s]
[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}))
(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}))
(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 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}))
(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))
;(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}))
;
;(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}))
;
;(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 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}))
;
;(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))

View file

@ -4,168 +4,168 @@
[grub.server-sync :as server-sync]
[midje.sweet :refer :all]))
(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]}
(server-sync/handle-event
{:type :diff
: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})]
@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 {:grubs {:- #{} :+ nil} :recipes {:- #{}, :+ nil}}
:shadow-tag 4
:tag 1})
new-shadow => {:tag 4
:grubs {"1" {:completed true, :text "2 apples"}}
:recipes {}}))
(fact "Diff, server changes - Apply diff, don't return changes (now)"
(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]}
(server-sync/handle-event
{:type :diff
:shadow-tag 0
:tag 4
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
:states states
:shadow state/empty-state
:client? false})]
@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 {}}
{:tag 2 :grubs {"1" {:text "2 apples" :completed true}
"2" {:text "3 onions" :completed false}} :recipes {}})
out-event => nil
new-shadow => {:tag 4
:grubs {"1" {:text "2 apples" :completed true}}
:recipes {}}))
(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
:shadow-tag 3
:tag 12
:diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
:states states
:shadow state/empty-state
:client? false}
{:keys [new-shadow out-event]} (server-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 "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]} (server-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 sends full sync if client requests it"
(let [result (server-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 {}}}))
)
(facts "Client diffs"
(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-shadow out-event]} (client-sync/handle-event event)]
@states =>
(just {:tag 0 :grubs {"1" {:completed false, :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 => nil))
(fact "Client state is unchanged on receiving empty diff"
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])]
(client-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"
(-> (client-sync/handle-event
{:type :diff
: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})
:out-event)
=> nil)
(fact "Client updates server shadow on empty diff"
(-> (client-sync/handle-event
{:type :diff
: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})
:new-shadow)
=> {:tag 4 :grubs {"1" {:completed false, :text "2 apples"}} :recipes {}}))
;(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]}
; (server-sync/handle-event
; {:type :diff
; :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})]
; @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 {:grubs {:- #{} :+ nil} :recipes {:- #{}, :+ nil}}
; :shadow-tag 4
; :tag 1})
; new-shadow => {:tag 4
; :grubs {"1" {:completed true, :text "2 apples"}}
; :recipes {}}))
;
; (fact "Diff, server changes - Apply diff, don't return changes (now)"
; (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]}
; (server-sync/handle-event
; {:type :diff
; :shadow-tag 0
; :tag 4
; :diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
; :states states
; :shadow state/empty-state
; :client? false})]
; @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 {}}
; {:tag 2 :grubs {"1" {:text "2 apples" :completed true}
; "2" {:text "3 onions" :completed false}} :recipes {}})
; out-event => nil
; new-shadow => {:tag 4
; :grubs {"1" {:text "2 apples" :completed true}}
; :recipes {}}))
;
; (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
; :shadow-tag 3
; :tag 12
; :diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
; :states states
; :shadow state/empty-state
; :client? false}
; {:keys [new-shadow out-event]} (server-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 "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]} (server-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 sends full sync if client requests it"
; (let [result (server-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 {}}}))
; )
;
;
;(facts "Client diffs"
; (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-shadow out-event]} (client-sync/handle-event event)]
; @states =>
; (just {:tag 0 :grubs {"1" {:completed false, :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 => nil))
;
; (fact "Client state is unchanged on receiving empty diff"
; (let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])]
; (client-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"
; (-> (client-sync/handle-event
; {:type :diff
; :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})
; :out-event)
; => nil)
;
; (fact "Client updates server shadow on empty diff"
; (-> (client-sync/handle-event
; {:type :diff
; :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})
; :new-shadow)
; => {:tag 4 :grubs {"1" {:completed false, :text "2 apples"}} :recipes {}}))