Separate server and client syncing

- They vary in important ways anyway so it's more clear this way
This commit is contained in:
Nicholas Kariniemi 2015-07-15 10:21:21 +03:00
parent 455cf54bc9
commit 8b11c119f2
6 changed files with 117 additions and 105 deletions

View file

@ -3,7 +3,7 @@
(:require [grub.websocket :as ws] (:require [grub.websocket :as ws]
[grub.db :as db] [grub.db :as db]
[grub.state :as state] [grub.state :as state]
[grub.sync :as sync] [grub.server-sync :as sync]
[ring.middleware.resource :as resource] [ring.middleware.resource :as resource]
[ring.middleware.content-type :as content-type] [ring.middleware.content-type :as content-type]
[ring.util.response :as resp] [ring.util.response :as resp]

View file

@ -1,17 +1,13 @@
(ns grub.sync (ns grub.client-sync
(:require [grub.diff :as diff] (:require [grub.diff :as diff]
[grub.state :as state] [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 (:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go]]))) [cljs.core.async.macros :refer [go]])))
(def full-sync-request {:type :full-sync-request}) (def full-sync-request {:type :full-sync-request})
(defn full-sync [state]
{:type :full-sync
:full-state state})
(def empty-state state/empty-state) (def empty-state state/empty-state)
(defn update-states [states diff] (defn update-states [states diff]
@ -28,76 +24,33 @@
(defmulti handle-event (fn [event] (:type event))) (defmulti handle-event (fn [event] (:type event)))
(defn apply-diff [states diff shadow new-shadow-tag client?] (defn apply-diff [states diff shadow new-shadow-tag]
(let [new-states (swap! states update-states diff) (swap! states update-states diff)
new-state (state/get-latest new-states) (let [new-shadow (assoc (diff/patch-state shadow diff) :tag new-shadow-tag)]
new-shadow (assoc (diff/patch-state shadow diff) {:new-shadow new-shadow :out-event nil}))
: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 shadow-tag tag client?]}] (defmethod handle-event :diff [{:keys [diff states shadow shadow-tag tag]}]
(let [history-shadow (state/get-tagged @states shadow-tag)] (let [history-shadow (state/get-tagged @states shadow-tag)]
(if history-shadow (if history-shadow
(apply-diff states diff history-shadow tag client?) (apply-diff states diff history-shadow tag)
(if client? {:out-event full-sync-request
{:out-event full-sync-request :new-shadow shadow} )))
: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)}))
(defmethod handle-event :full-sync [{:keys [full-state states]}] (defmethod handle-event :full-sync [{:keys [full-state states]}]
(reset! states (state/new-states full-state)) (reset! states (state/new-states full-state))
{:new-shadow full-state}) {:new-shadow full-state})
(defmethod handle-event :new-state [{:keys [shadow states new-state client?]}] (defmethod handle-event :new-state [{:keys [shadow states new-state]}]
(let [new-states (swap! states state/add new-state) (let [new-states (swap! states state/add new-state)
latest-state (state/get-latest new-states)] latest-state (state/get-latest new-states)]
{:out-event (when-not (state/state= shadow latest-state) {:out-event (when-not (state/state= shadow latest-state)
(diff-msg shadow latest-state)) (diff-msg shadow latest-state))
:new-shadow (when (and (not client?) :new-shadow nil}))
(not (state/state= shadow latest-state)))
(assoc latest-state :tag (inc (:tag shadow))))}))
(defmethod handle-event :default [msg] (defmethod handle-event :default [msg]
#?(:cljs (logs "Unhandled message:" msg) #?(:cljs (logs "Unhandled message:" msg))
:clj (println "Unhandled message:" msg))
{}) {})
(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 (defn make-client-agent
([>remote events new-states states] ([>remote events new-states states]
(make-client-agent >remote events new-states states state/empty-state)) (make-client-agent >remote events new-states states state/empty-state))
@ -115,14 +68,12 @@
(let [event {:type :new-state (let [event {:type :new-state
:new-state v :new-state v
:shadow shadow :shadow shadow
:states states :states states}
:client? true}
{:keys [out-event]} (handle-event event)] {:keys [out-event]} (handle-event event)]
(recur shadow out-event)) (recur shadow out-event))
(= c events) (= c events)
(let [event (assoc v (let [event (assoc v
:states states :states states
:client? true
:shadow shadow) :shadow shadow)
{:keys [new-shadow out-event]} (handle-event event)] {:keys [new-shadow out-event]} (handle-event event)]
(recur (if new-shadow new-shadow shadow) out-event)))))))) (recur (if new-shadow new-shadow shadow) out-event))))))))

View file

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

View file

@ -1,6 +1,6 @@
(ns grub.core (ns grub.core
(:require [grub.state :as state] (:require [grub.state :as state]
[grub.sync :as sync] [grub.client-sync :as sync]
[grub.websocket :as websocket] [grub.websocket :as websocket]
[grub.view.app :as view] [grub.view.app :as view]
[cljs.core.async :as a :refer [<! >! chan]]) [cljs.core.async :as a :refer [<! >! chan]])

View file

@ -1,14 +1,10 @@
(ns grub.test.integration.synchronization (ns grub.test.integration.synchronization
(:require [grub.sync :as sync] (:require [grub.client-sync :as client-sync]
[grub.state :as state] [grub.server-sync :as server-sync]
[clojure.test :refer :all] [clojure.test :refer :all]
[midje.sweet :refer :all] [midje.sweet :refer :all]
[clojure.core.async :as a :refer [<!! >!! chan go]])) [clojure.core.async :as a :refer [<!! >!! chan go]]))
(defn <!!? [c]
(let [[v p] (a/alts!! [c (a/timeout 100)])]
v))
(defn client-server [client-states server-states] (defn client-server [client-states server-states]
(let [server-shadow (last @server-states) (let [server-shadow (last @server-states)
client-shadow (last @client-states) client-shadow (last @client-states)
@ -16,8 +12,8 @@
>client (chan) >client (chan)
new-server-states (chan) new-server-states (chan)
>server (chan)] >server (chan)]
(sync/make-client-agent >server >client new-client-states client-states server-shadow) (client-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) (server-sync/make-server-agent >client >server new-server-states server-states client-shadow)
{:new-client-states new-client-states {:new-client-states new-client-states
:new-server-states new-server-states})) :new-server-states new-server-states}))
@ -92,4 +88,3 @@
(last-state @client) => {:grubs {"1" {:text "2 apples" :completed true} (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}
"2" {:text "milk" :completed false}} "2" {:text "milk" :completed false}}
:recipes {}})) :recipes {}}))

View file

@ -1,13 +1,14 @@
(ns grub.test.unit.sync (ns grub.test.unit.sync
(:require [grub.state :as state] (: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])) [midje.sweet :refer :all]))
(facts "Server" (facts "Server"
(fact "Diff, no server changes - Apply diff, return empty diff" (fact "Diff, no server changes - Apply diff, return empty diff"
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}]) (let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
{:keys [out-event new-shadow]} {:keys [out-event new-shadow]}
(sync/handle-event (server-sync/handle-event
{:type :diff {:type :diff
:tag 4 :tag 4
:shadow-tag 0 :shadow-tag 0
@ -35,7 +36,7 @@
"2" {:text "3 onions" :completed false}} "2" {:text "3 onions" :completed false}}
:recipes {}}]) :recipes {}}])
{:keys [new-shadow out-event]} {:keys [new-shadow out-event]}
(sync/handle-event (server-sync/handle-event
{:type :diff {:type :diff
:shadow-tag 0 :shadow-tag 0
:tag 4 :tag 4
@ -66,7 +67,7 @@
:states states :states states
:shadow state/empty-state :shadow state/empty-state
:client? false} :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 out-event => {:type :full-sync
:full-state {:tag 15 :full-state {:tag 15
:grubs {"1" {:text "2 apples" :completed false} :grubs {"1" {:text "2 apples" :completed false}
@ -80,7 +81,7 @@
:shadow {:tag 3 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}} :shadow {:tag 3 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
:client? false :client? false
:new-state {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}} :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 {}} @states => [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
{:tag 15 :grubs {"1" {:text "2 apples" :completed true}} :recipes {}}] {:tag 15 :grubs {"1" {:text "2 apples" :completed true}} :recipes {}}]
new-shadow => {:tag 4 :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 :- #{}}}})) :recipes {:+ nil :- #{}}}}))
(fact "Server sends full sync if client requests it" (fact "Server sends full sync if client requests it"
(let [result (sync/handle-event (let [result (server-sync/handle-event
{:type :full-sync-request {:type :full-sync-request
:states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}} :states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
{:tag 15 :grubs {"1" {:text "2 apples" :completed false} {:tag 15 :grubs {"1" {:text "2 apples" :completed false}
@ -122,7 +123,7 @@
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}}
:recipes {}} :recipes {}}
:client? true} :client? true}
{:keys [new-shadow out-event]} (sync/handle-event event)] {:keys [new-shadow out-event]} (client-sync/handle-event event)]
@states => @states =>
(just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} (just {:tag 0 :grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
{:tag 1 {:tag 1
@ -133,7 +134,7 @@
(fact "Client state is unchanged on receiving empty diff" (fact "Client state is unchanged on receiving empty diff"
(let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])] (let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])]
(sync/handle-event (client-sync/handle-event
{:type :diff {:type :diff
:shadow-tag 0 :shadow-tag 0
:tag 4 :tag 4
@ -146,7 +147,7 @@
:recipes {}}])) :recipes {}}]))
(fact "Client returns no response on empty diff" (fact "Client returns no response on empty diff"
(-> (sync/handle-event (-> (client-sync/handle-event
{:type :diff {:type :diff
:shadow-tag 0 :shadow-tag 0
:tag 4 :tag 4
@ -158,7 +159,7 @@
=> nil) => nil)
(fact "Client updates server shadow on empty diff" (fact "Client updates server shadow on empty diff"
(-> (sync/handle-event (-> (client-sync/handle-event
{:type :diff {:type :diff
:shadow-tag 0 :shadow-tag 0
:tag 4 :tag 4
@ -168,23 +169,3 @@
:client? true}) :client? true})
:new-shadow) :new-shadow)
=> {:tag 4 :grubs {"1" {:completed false, :text "2 apples"}} :recipes {}})) => {: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 {}}})))