diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index de58984..e9b2190 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -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 (! 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] diff --git a/src/clj/grub/db.clj b/src/clj/grub/db.clj index 3322775..9eb2b3d 100644 --- a/src/clj/grub/db.clj +++ b/src/clj/grub/db.clj @@ -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}) diff --git a/src/clj/grub/server_sync.clj b/src/clj/grub/server_sync.clj index 036ddf2..4858f66 100644 --- a/src/clj/grub/server_sync.clj +++ b/src/clj/grub/server_sync.clj @@ -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)))))))) diff --git a/src/clj/grub/websocket.clj b/src/clj/grub/websocket.clj index e3439da..4cc3029 100644 --- a/src/clj/grub/websocket.clj +++ b/src/clj/grub/websocket.clj @@ -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) diff --git a/src/cljc/grub/client_sync.cljc b/src/cljc/grub/client_sync.cljc index 9cdafcb..970fe23 100644 --- a/src/cljc/grub/client_sync.cljc +++ b/src/cljc/grub/client_sync.cljc @@ -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)) - (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)] - (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")))))) + (let [[event ch] (a/alts! channels)] + (when DEBUG (println event)) + (when-not (nil? event) + (condp = ch + 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-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))) diff --git a/src/cljc/grub/diff.cljc b/src/cljc/grub/diff.cljc index 183641a..e45503d 100644 --- a/src/cljc/grub/diff.cljc +++ b/src/cljc/grub/diff.cljc @@ -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 - (keys) - (map (fn [k] [k (diff-maps (k prev) (k next))])) - (filter #(not (nil? (second %)))) - (into {}))) + (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*))])) + (filter #(not (nil? (second %)))) + (into {})))) (defn patch-map [state diff] (-> state diff --git a/src/cljs/grub/view/grub.cljs b/src/cljs/grub/view/grub.cljs index 04e5961..5f99b39 100644 --- a/src/cljs/grub/view/grub.cljs +++ b/src/cljs/grub/view/grub.cljs @@ -8,8 +8,8 @@ [cljs.core.async.macros :refer [go go-loop]])) (defn new-grub [text] - {:id (str "grub-" (uuid/make-random)) - :text text + {:id (keyword (str "grub-" (uuid/make-random))) + :text text :completed false}) (def transitions diff --git a/src/test/grub/test/integration/synchronization.clj b/src/test/grub/test/integration/synchronization.clj index f0a8265..c8e6781 100644 --- a/src/test/grub/test/integration/synchronization.clj +++ b/src/test/grub/test/integration/synchronization.clj @@ -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 [] - (!! 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 [] +; (!! 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 {}})) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 0b2b618..1c768ca 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -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)) diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj index f9c855a..eb768d6 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/sync.clj @@ -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 {}}))