From 8b11c119f27e56fbeff589503b145639c6e29273 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Wed, 15 Jul 2015 10:21:21 +0300 Subject: [PATCH] Separate server and client syncing - They vary in important ways anyway so it's more clear this way --- src/clj/grub/core.clj | 2 +- src/cljc/grub/{sync.cljc => client_sync.cljc} | 79 ++++------------- src/cljc/grub/server_sync.cljc | 85 +++++++++++++++++++ src/cljs/grub/core.cljs | 2 +- .../grub/test/integration/synchronization.clj | 13 +-- src/test/grub/test/unit/sync.clj | 41 +++------ 6 files changed, 117 insertions(+), 105 deletions(-) rename src/cljc/grub/{sync.cljc => client_sync.cljc} (50%) create mode 100644 src/cljc/grub/server_sync.cljc diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index 45ad08b..d80f676 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -3,7 +3,7 @@ (:require [grub.websocket :as ws] [grub.db :as db] [grub.state :as state] - [grub.sync :as sync] + [grub.server-sync :as sync] [ring.middleware.resource :as resource] [ring.middleware.content-type :as content-type] [ring.util.response :as resp] diff --git a/src/cljc/grub/sync.cljc b/src/cljc/grub/client_sync.cljc similarity index 50% rename from src/cljc/grub/sync.cljc rename to src/cljc/grub/client_sync.cljc index 6e4d414..c0ece28 100644 --- a/src/cljc/grub/sync.cljc +++ b/src/cljc/grub/client_sync.cljc @@ -1,17 +1,13 @@ -(ns grub.sync +(ns grub.client-sync (:require [grub.diff :as diff] [grub.state :as state] - #?(:clj [clojure.core.async :as a :refer [! chan go]] - :cljs [cljs.core.async :as a :refer [! chan]])) + #?(:cljs [cljs.core.async :as a :refer [! chan]] + :clj [clojure.core.async :as a :refer [! chan go]])) #?(:cljs (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go]]))) (def full-sync-request {:type :full-sync-request}) -(defn full-sync [state] - {:type :full-sync - :full-state state}) - (def empty-state state/empty-state) (defn update-states [states diff] @@ -28,76 +24,33 @@ (defmulti handle-event (fn [event] (:type event))) -(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 (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))})) +(defn apply-diff [states diff shadow new-shadow-tag] + (swap! states update-states diff) + (let [new-shadow (assoc (diff/patch-state shadow diff) :tag new-shadow-tag)] + {:new-shadow new-shadow :out-event nil})) -(defmethod handle-event :diff [{:keys [diff states shadow shadow-tag tag client?]}] +(defmethod handle-event :diff [{:keys [diff states shadow shadow-tag tag]}] (let [history-shadow (state/get-tagged @states shadow-tag)] (if history-shadow - (apply-diff states diff history-shadow tag client?) - (if client? - {:out-event full-sync-request - :new-shadow shadow} - (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)})) + (apply-diff states diff history-shadow tag) + {:out-event full-sync-request + :new-shadow shadow} ))) (defmethod handle-event :full-sync [{:keys [full-state states]}] (reset! states (state/new-states full-state)) {:new-shadow full-state}) -(defmethod handle-event :new-state [{:keys [shadow states new-state client?]}] +(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 (and (not client?) - (not (state/state= shadow latest-state))) - (assoc latest-state :tag (inc (:tag shadow))))})) + :new-shadow nil})) (defmethod handle-event :default [msg] - #?(:cljs (logs "Unhandled message:" msg) - :clj (println "Unhandled message:" msg)) + #?(:cljs (logs "Unhandled message:" msg)) {}) -(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) - (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? 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))))))))) - (defn make-client-agent ([>remote events new-states states] (make-client-agent >remote events new-states states state/empty-state)) @@ -115,14 +68,12 @@ (let [event {:type :new-state :new-state v :shadow shadow - :states states - :client? true} + :states states} {: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)))))))) diff --git a/src/cljc/grub/server_sync.cljc b/src/cljc/grub/server_sync.cljc new file mode 100644 index 0000000..6fc5626 --- /dev/null +++ b/src/cljc/grub/server_sync.cljc @@ -0,0 +1,85 @@ +(ns grub.server-sync + (:require [grub.diff :as diff] + [grub.state :as state] + #?(:cljs [cljs.core.async :as a :refer [! chan]] + :clj [clojure.core.async :as a :refer [! chan go]])) + #?(:cljs (:require-macros [grub.macros :refer [log logs]] + [cljs.core.async.macros :refer [go]]))) + +(defn full-sync [state] + {:type :full-sync + :full-state state}) + +(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] + (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] + #?(:clj (println "Unhandled message:" msg)) + {}) + +(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) + (let [event {:type :new-state + :new-state v + :shadow shadow + :states states} + {: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 + :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))))))))) diff --git a/src/cljs/grub/core.cljs b/src/cljs/grub/core.cljs index 76f94d8..260abb5 100644 --- a/src/cljs/grub/core.cljs +++ b/src/cljs/grub/core.cljs @@ -1,6 +1,6 @@ (ns grub.core (:require [grub.state :as state] - [grub.sync :as sync] + [grub.client-sync :as sync] [grub.websocket :as websocket] [grub.view.app :as view] [cljs.core.async :as a :refer [! chan]]) diff --git a/src/test/grub/test/integration/synchronization.clj b/src/test/grub/test/integration/synchronization.clj index 3557c35..f0a8265 100644 --- a/src/test/grub/test/integration/synchronization.clj +++ b/src/test/grub/test/integration/synchronization.clj @@ -1,14 +1,10 @@ (ns grub.test.integration.synchronization - (:require [grub.sync :as sync] - [grub.state :as state] + (:require [grub.client-sync :as client-sync] + [grub.server-sync :as server-sync] [clojure.test :refer :all] [midje.sweet :refer :all] [clojure.core.async :as a :refer [!! chan go]])) -(defn client (chan) new-server-states (chan) >server (chan)] - (sync/make-client-agent >server >client new-client-states client-states server-shadow) - (sync/make-server-agent >client >server new-server-states server-states client-shadow) + (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})) @@ -92,4 +88,3 @@ (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true} "2" {:text "milk" :completed false}} :recipes {}})) - diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj index 2c69617..f9c855a 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/sync.clj @@ -1,13 +1,14 @@ (ns grub.test.unit.sync (:require [grub.state :as state] - [grub.sync :as sync] + [grub.client-sync :as client-sync] + [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]} - (sync/handle-event + (server-sync/handle-event {:type :diff :tag 4 :shadow-tag 0 @@ -35,7 +36,7 @@ "2" {:text "3 onions" :completed false}} :recipes {}}]) {:keys [new-shadow out-event]} - (sync/handle-event + (server-sync/handle-event {:type :diff :shadow-tag 0 :tag 4 @@ -66,7 +67,7 @@ :states states :shadow state/empty-state :client? false} - {:keys [new-shadow out-event]} (sync/handle-event event)] + {: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} @@ -80,7 +81,7 @@ :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)] + {: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 {}} @@ -91,7 +92,7 @@ :recipes {:+ nil :- #{}}}})) (fact "Server sends full sync if client requests it" - (let [result (sync/handle-event + (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} @@ -122,7 +123,7 @@ :shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}} :client? true} - {:keys [new-shadow out-event]} (sync/handle-event event)] + {:keys [new-shadow out-event]} (client-sync/handle-event event)] @states => (just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} {:tag 1 @@ -133,7 +134,7 @@ (fact "Client state is unchanged on receiving empty diff" (let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])] - (sync/handle-event + (client-sync/handle-event {:type :diff :shadow-tag 0 :tag 4 @@ -146,7 +147,7 @@ :recipes {}}])) (fact "Client returns no response on empty diff" - (-> (sync/handle-event + (-> (client-sync/handle-event {:type :diff :shadow-tag 0 :tag 4 @@ -158,7 +159,7 @@ => nil) (fact "Client updates server shadow on empty diff" - (-> (sync/handle-event + (-> (client-sync/handle-event {:type :diff :shadow-tag 0 :tag 4 @@ -168,23 +169,3 @@ :client? true}) :new-shadow) => {:tag 4 :grubs {"1" {:completed false, :text "2 apples"}} :recipes {}})) - -(facts "Full sync" - (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 {}}})))