Separate server and client syncing
- They vary in important ways anyway so it's more clear this way
This commit is contained in:
parent
455cf54bc9
commit
8b11c119f2
6 changed files with 117 additions and 105 deletions
|
@ -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]
|
||||
|
|
|
@ -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))))))))
|
85
src/cljc/grub/server_sync.cljc
Normal file
85
src/cljc/grub/server_sync.cljc
Normal 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)))))))))
|
|
@ -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]])
|
||||
|
|
|
@ -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 {}}))
|
||||
|
||||
|
|
|
@ -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 {}}})))
|
||||
|
|
Loading…
Reference in a new issue