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]
[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]

View file

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

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
(: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]])

View file

@ -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 <!!? [c]
(let [[v p] (a/alts!! [c (a/timeout 100)])]
v))
(defn client-server [client-states server-states]
(let [server-shadow (last @server-states)
client-shadow (last @client-states)
@ -16,8 +12,8 @@
>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 {}}))

View file

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