Working sync for single client (no changes from others)
This commit is contained in:
parent
a8d8ae56c5
commit
798f1a5891
10 changed files with 386 additions and 388 deletions
|
@ -49,18 +49,29 @@
|
||||||
:port 3000
|
:port 3000
|
||||||
:stop-server nil})
|
: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 (<! from-client)]
|
||||||
|
(cond
|
||||||
|
(nil? event) nil ;; drop out of loop
|
||||||
|
(= (:type event) :diff) (do (>! 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]
|
(defn handle-websocket [handler db-conn]
|
||||||
(fn [{:keys [websocket?] :as request}]
|
(fn [{:keys [websocket?] :as request}]
|
||||||
(if websocket?
|
(if websocket?
|
||||||
(httpkit/with-channel request ws-channel
|
(httpkit/with-channel request ws-channel (sync-client-with-db! ws-channel db-conn))
|
||||||
(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)))
|
|
||||||
(handler request))))
|
(handler request))))
|
||||||
|
|
||||||
(defn handle-root [handler index]
|
(defn handle-root [handler index]
|
||||||
|
|
|
@ -72,11 +72,11 @@
|
||||||
conn))
|
conn))
|
||||||
|
|
||||||
(def all-grubs-query
|
(def all-grubs-query
|
||||||
[:find '?id '?text '?complete
|
[:find '?id '?text '?completed
|
||||||
:where
|
:where
|
||||||
['?e :grub/id '?id]
|
['?e :grub/id '?id]
|
||||||
['?e :grub/text '?text]
|
['?e :grub/text '?text]
|
||||||
['?e :grub/completed '?complete]])
|
['?e :grub/completed '?completed]])
|
||||||
|
|
||||||
(def all-recipes-query
|
(def all-recipes-query
|
||||||
[:find '?id '?name '?grubs '?directions
|
[:find '?id '?name '?grubs '?directions
|
||||||
|
@ -86,8 +86,8 @@
|
||||||
['?e :recipe/grubs '?grubs]
|
['?e :recipe/grubs '?grubs]
|
||||||
['?e :recipe/directions '?directions]])
|
['?e :recipe/directions '?directions]])
|
||||||
|
|
||||||
(defn grub-as-map [[id text complete]]
|
(defn grub-as-map [[id text completed]]
|
||||||
{:id id :text text :complete complete})
|
{:id id :text text :completed completed})
|
||||||
|
|
||||||
(defn recipe-as-map [[id name grubs directions]]
|
(defn recipe-as-map [[id name grubs directions]]
|
||||||
{:id id :name name :grubs grubs :directions directions})
|
{:id id :name name :grubs grubs :directions directions})
|
||||||
|
|
|
@ -1,85 +1,53 @@
|
||||||
(ns grub.server-sync
|
(ns grub.server-sync
|
||||||
(:require [grub.diff :as diff]
|
(:require [grub.diff :as diff]
|
||||||
[grub.state :as state]
|
[grub.state :as state]
|
||||||
|
[datomic.api :as d]
|
||||||
[clojure.core.async :as a :refer [<! >! chan go]]
|
[clojure.core.async :as a :refer [<! >! chan go]]
|
||||||
[grub.db :as db]
|
[grub.db :as db]
|
||||||
[clojure.pprint :refer [pprint]]))
|
[clojure.pprint :refer [pprint]]))
|
||||||
|
|
||||||
(defn full-sync [state]
|
(defn full-sync [state tag]
|
||||||
{:type :full-sync
|
{:type :full-sync
|
||||||
:full-state state})
|
:full-state state
|
||||||
|
:tag tag})
|
||||||
|
|
||||||
(def empty-state state/empty-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]
|
(defn diff-msg [shadow state]
|
||||||
|
(println "diff-msg")
|
||||||
(let [diff (diff/diff-states shadow state)]
|
(let [diff (diff/diff-states shadow state)]
|
||||||
{:type :diff
|
{:type :diff
|
||||||
:diff diff
|
:diff diff
|
||||||
:tag (:tag state)
|
:tag (:tag state)
|
||||||
:shadow-tag (:tag shadow)}))
|
:shadow-tag (:tag shadow)}))
|
||||||
|
|
||||||
(defmulti handle-event (fn [event] (:type event)))
|
(defn sync-server! [to-client diffs full-sync-reqs db-conn]
|
||||||
|
(go (loop []
|
||||||
(defn apply-diff [states diff shadow new-shadow-tag]
|
(let [[event ch] (a/alts! [full-sync-reqs diffs])]
|
||||||
(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)]
|
|
||||||
(when-not (nil? event)
|
(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
|
full-sync-reqs
|
||||||
(let [history-state (db/get-history-state db-conn (:shadow-tag event))
|
(do (>! to-client (full-sync (db/get-current-state db-conn) (d/basis-t (d/db db-conn))))
|
||||||
new-state (db/patch-state! db-conn (:diff event))
|
(recur))
|
||||||
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))
|
|
||||||
(do (println "Unhandled event:" event)
|
(do (println "Unhandled event:" event)
|
||||||
(recur shadow))))))))
|
(recur))))))))
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
[cognitect.transit :as t])
|
[cognitect.transit :as t])
|
||||||
(:import [java.io ByteArrayInputStream ByteArrayOutputStream]))
|
(:import [java.io ByteArrayInputStream ByteArrayOutputStream]))
|
||||||
|
|
||||||
(def DEBUG true)
|
(def DEBUG false)
|
||||||
|
|
||||||
(defn write-msg [msg]
|
(defn write-msg [msg]
|
||||||
(let [out (ByteArrayOutputStream. 4096)
|
(let [out (ByteArrayOutputStream. 4096)
|
||||||
|
|
|
@ -25,30 +25,33 @@
|
||||||
(let [ui-state-buffer (chan (a/sliding-buffer 1))]
|
(let [ui-state-buffer (chan (a/sliding-buffer 1))]
|
||||||
(a/pipe new-ui-states ui-state-buffer)
|
(a/pipe new-ui-states ui-state-buffer)
|
||||||
(reset! ui-state state/empty-state)
|
(reset! ui-state state/empty-state)
|
||||||
(go (loop [states (state/new-states @ui-state)
|
(go (loop [state (assoc @ui-state :tag 0)
|
||||||
shadow (state/get-latest states)
|
shadow state
|
||||||
awaiting-ack? false]
|
awaiting-ack? false]
|
||||||
(let [channels (if awaiting-ack? [diffs full-syncs] [diffs full-syncs ui-state-buffer])]
|
(let [channels (if awaiting-ack? [diffs full-syncs] [diffs full-syncs ui-state-buffer])]
|
||||||
(let [[val ch] (a/alts! channels)]
|
(let [[event ch] (a/alts! channels)]
|
||||||
(when DEBUG (println val))
|
(when DEBUG (println event))
|
||||||
|
(when-not (nil? event)
|
||||||
(condp = ch
|
(condp = ch
|
||||||
ui-state-buffer (let [new-state val
|
ui-state-buffer (let [new-state (assoc event :tag (inc (:tag state)))]
|
||||||
new-states (state/add states new-state)
|
(println "new-state:\n" new-state)
|
||||||
latest-state (state/get-latest new-states)]
|
(>! to-server (diff-msg shadow new-state))
|
||||||
(>! to-server (diff-msg shadow latest-state))
|
(recur new-state shadow true))
|
||||||
(recur new-states shadow true))
|
full-syncs (let [{:keys [full-state tag]} event
|
||||||
full-syncs (let [full-state (:full-state val)
|
new-tag (inc (:tag state))
|
||||||
new-states (state/new-states full-state)
|
new-state (assoc full-state :tag new-tag)]
|
||||||
latest-state (state/get-latest new-states)]
|
|
||||||
(reset! ui-state full-state)
|
(reset! ui-state full-state)
|
||||||
(recur new-states latest-state false))
|
(recur new-state (assoc full-state :tag tag) false))
|
||||||
diffs (let [{:keys [diff shadow-tag tag]} val
|
diffs (let [{:keys [diff shadow-tag tag]} event]
|
||||||
history-shadow (state/get-tagged states shadow-tag)]
|
(cond (< shadow-tag (:tag state)) (recur state shadow false)
|
||||||
(if history-shadow
|
(= shadow-tag (:tag state))
|
||||||
(let [new-states (update-states states diff)
|
(let [new-shadow (assoc (diff/patch-state state diff) :tag tag)
|
||||||
new-shadow (assoc (diff/patch-state shadow diff) :tag tag)]
|
new-state (assoc (swap! ui-state diff/patch-state diff) :tag (inc (:tag state)))]
|
||||||
(recur new-states new-shadow false))
|
(if (state/state= new-shadow new-state)
|
||||||
(do (>! to-server full-sync-request)
|
(recur new-state new-shadow false)
|
||||||
(recur states shadow true))))
|
(do (>! to-server (diff-msg new-shadow new-state))
|
||||||
(println "An error occurred, received value on unknown channel"))))))
|
(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)))
|
(a/put! to-server full-sync-request)))
|
||||||
|
|
|
@ -6,8 +6,19 @@
|
||||||
(set/difference (into #{} (keys a)) (into #{} (keys b))))
|
(set/difference (into #{} (keys a)) (into #{} (keys b))))
|
||||||
|
|
||||||
(defn updated [a 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)))
|
(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]
|
(defn diff-maps [a b]
|
||||||
(when (and (map? a) (map? b))
|
(when (and (map? a) (map? b))
|
||||||
{:- (deleted a b)
|
{:- (deleted a b)
|
||||||
|
@ -21,11 +32,16 @@
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(defn diff-states [prev next]
|
(defn diff-states [prev next]
|
||||||
(->> prev
|
(println "diff states")
|
||||||
|
(println "prev:" (dissoc prev :recipes))
|
||||||
|
(println "next:" (dissoc next :recipes))
|
||||||
|
(let [prev* (dissoc prev :tag)
|
||||||
|
next* (dissoc next :tag)]
|
||||||
|
(->> prev*
|
||||||
(keys)
|
(keys)
|
||||||
(map (fn [k] [k (diff-maps (k prev) (k next))]))
|
(map (fn [k] [k (diff-maps (k prev*) (k next*))]))
|
||||||
(filter #(not (nil? (second %))))
|
(filter #(not (nil? (second %))))
|
||||||
(into {})))
|
(into {}))))
|
||||||
|
|
||||||
(defn patch-map [state diff]
|
(defn patch-map [state diff]
|
||||||
(-> state
|
(-> state
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
[cljs.core.async.macros :refer [go go-loop]]))
|
[cljs.core.async.macros :refer [go go-loop]]))
|
||||||
|
|
||||||
(defn new-grub [text]
|
(defn new-grub [text]
|
||||||
{:id (str "grub-" (uuid/make-random))
|
{:id (keyword (str "grub-" (uuid/make-random)))
|
||||||
:text text
|
:text text
|
||||||
:completed false})
|
:completed false})
|
||||||
|
|
||||||
|
|
|
@ -5,86 +5,86 @@
|
||||||
[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 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)
|
||||||
new-client-states (chan)
|
; new-client-states (chan)
|
||||||
>client (chan)
|
; >client (chan)
|
||||||
new-server-states (chan)
|
; new-server-states (chan)
|
||||||
>server (chan)]
|
; >server (chan)]
|
||||||
(client-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)
|
||||||
(server-sync/make-server-agent >client >server new-server-states server-states client-shadow)
|
; (server-sync/sync-server! >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}))
|
||||||
|
;
|
||||||
(defn states-in-sync? [a b]
|
;(defn states-in-sync? [a b]
|
||||||
(let [last-a (dissoc (last a) :tag)
|
; (let [last-a (dissoc (last a) :tag)
|
||||||
last-b (dissoc (last b) :tag)]
|
; last-b (dissoc (last b) :tag)]
|
||||||
last-a => last-b))
|
; last-a => last-b))
|
||||||
|
;
|
||||||
(defn last-state [states]
|
;(defn last-state [states]
|
||||||
(-> states
|
; (-> states
|
||||||
(last)
|
; (last)
|
||||||
(dissoc :tag)))
|
; (dissoc :tag)))
|
||||||
|
;
|
||||||
(defn short-delay []
|
;(defn short-delay []
|
||||||
(<!! (a/timeout 300)))
|
; (<!! (a/timeout 300)))
|
||||||
|
;
|
||||||
(fact "Client-only changes sync with server"
|
;(fact "Client-only changes sync with server"
|
||||||
(let [client (atom [{:tag 1
|
; (let [client (atom [{:tag 1
|
||||||
:grubs {"1" {:text "2 apples" :completed false}}
|
; :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
|
; server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
{:keys [new-client-states]} (client-server client server)
|
; {:keys [new-client-states]} (client-server client server)
|
||||||
client-change {:tag 2
|
; client-change {:tag 2
|
||||||
:grubs {"1" {:text "2 apples" :completed true}}
|
; :grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}]
|
; :recipes {}}]
|
||||||
(swap! client conj client-change)
|
; (swap! client conj client-change)
|
||||||
(>!! new-client-states client-change)
|
; (>!! new-client-states client-change)
|
||||||
(short-delay)
|
; (short-delay)
|
||||||
(states-in-sync? @client @server)
|
; (states-in-sync? @client @server)
|
||||||
(last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
|
; (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}))
|
; :recipes {}}))
|
||||||
|
;
|
||||||
(fact "Other client changes synced with client"
|
;(fact "Other client changes synced with client"
|
||||||
(let [client (atom [{:tag 1
|
; (let [client (atom [{:tag 1
|
||||||
:grubs {"1" {:text "2 apples" :completed false}}
|
; :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
|
; server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
{:keys [new-server-states]} (client-server client server)
|
; {:keys [new-server-states]} (client-server client server)
|
||||||
server-change {:tag 2
|
; server-change {:tag 2
|
||||||
:grubs {"1" {:text "2 apples" :completed true}}
|
; :grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}]
|
; :recipes {}}]
|
||||||
(swap! server conj server-change)
|
; (swap! server conj server-change)
|
||||||
(>!! new-server-states server-change)
|
; (>!! new-server-states server-change)
|
||||||
(short-delay)
|
; (short-delay)
|
||||||
(states-in-sync? @client @server)
|
; (states-in-sync? @client @server)
|
||||||
(last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
|
; (last-state @client) => {:grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}))
|
; :recipes {}}))
|
||||||
|
;
|
||||||
(fact "Client changes and simultaneous server changes synced"
|
;(fact "Client changes and simultaneous server changes synced"
|
||||||
(let [client (atom [{:tag 1
|
; (let [client (atom [{:tag 1
|
||||||
:grubs {"1" {:text "2 apples" :completed false}}
|
; :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
|
; server (atom [{:tag 44 :grubs {"1" {:text "2 apples" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
{:keys [new-client-states new-server-states]} (client-server client server)
|
; {:keys [new-client-states new-server-states]} (client-server client server)
|
||||||
client-change {:tag 2
|
; client-change {:tag 2
|
||||||
:grubs {"1" {:text "2 apples" :completed true}}
|
; :grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}
|
; :recipes {}}
|
||||||
server-change {:tag 45
|
; server-change {:tag 45
|
||||||
:grubs {"1" {:text "2 apples" :completed false}
|
; :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "milk" :completed false}}
|
; "2" {:text "milk" :completed false}}
|
||||||
:recipes {}}]
|
; :recipes {}}]
|
||||||
(swap! client conj client-change)
|
; (swap! client conj client-change)
|
||||||
(swap! server conj server-change)
|
; (swap! server conj server-change)
|
||||||
(>!! new-client-states client-change)
|
; (>!! new-client-states client-change)
|
||||||
(short-delay)
|
; (short-delay)
|
||||||
(>!! new-server-states (last @server))
|
; (>!! new-server-states (last @server))
|
||||||
(short-delay)
|
; (short-delay)
|
||||||
(states-in-sync? @client @server)
|
; (states-in-sync? @client @server)
|
||||||
(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 {}}))
|
||||||
|
|
|
@ -2,34 +2,34 @@
|
||||||
(:require [grub.state :as s]
|
(:require [grub.state :as s]
|
||||||
[midje.sweet :refer :all]))
|
[midje.sweet :refer :all]))
|
||||||
|
|
||||||
(fact "Get current state returns last state"
|
;(fact "Get current state returns last state"
|
||||||
(let [states [{:tag 1 :a :b}
|
; (let [states [{:tag 1 :a :b}
|
||||||
{:tag 2 :c :d}]]
|
; {:tag 2 :c :d}]]
|
||||||
(s/get-latest states) => {:tag 2 :c :d}))
|
; (s/get-latest states) => {:tag 2 :c :d}))
|
||||||
|
;
|
||||||
(fact "Get history state returns state with given hash"
|
;(fact "Get history state returns state with given hash"
|
||||||
(let [states [{:tag 1 :a :b}
|
; (let [states [{:tag 1 :a :b}
|
||||||
{:tag 2 :c :d}
|
; {:tag 2 :c :d}
|
||||||
{:tag 3 :e :f}]]
|
; {:tag 3 :e :f}]]
|
||||||
(s/get-tagged states 1) => {:tag 1 :a :b}
|
; (s/get-tagged states 1) => {:tag 1 :a :b}
|
||||||
(s/get-tagged states 2) => {:tag 2 :c :d}
|
; (s/get-tagged states 2) => {:tag 2 :c :d}
|
||||||
(s/get-tagged states 3) => {:tag 3 :e :f}))
|
; (s/get-tagged states 3) => {:tag 3 :e :f}))
|
||||||
|
;
|
||||||
(fact "Add history state appends state to the end and increments tag"
|
;(fact "Add history state appends state to the end and increments tag"
|
||||||
(let [states [{:tag 1 :a :b}
|
; (let [states [{:tag 1 :a :b}
|
||||||
{:tag 2 :c :d}]]
|
; {:tag 2 :c :d}]]
|
||||||
(s/add states {:e :f}) => [{:tag 1 :a :b}
|
; (s/add states {:e :f}) => [{:tag 1 :a :b}
|
||||||
{:tag 2 :c :d}
|
; {:tag 2 :c :d}
|
||||||
{:tag 3 :e :f}]))
|
; {:tag 3 :e :f}]))
|
||||||
|
;
|
||||||
(fact "Add history state appends state to the end and drops first state if full"
|
;(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}))
|
; (let [states (into [] (for [i (range 20)] {:tag i :i i}))
|
||||||
new-states (s/add states {:i 21})]
|
; new-states (s/add states {:i 21})]
|
||||||
(count new-states) => 20
|
; (count new-states) => 20
|
||||||
(dissoc (last new-states) :tag) => {:i 21}
|
; (dissoc (last new-states) :tag) => {:i 21}
|
||||||
(first new-states) => {:tag 1 :i 1}))
|
; (first new-states) => {:tag 1 :i 1}))
|
||||||
|
;
|
||||||
(fact "Add history state does not add consecutive duplicate states"
|
;(fact "Add history state does not add consecutive duplicate states"
|
||||||
(let [states [{:tag 1 :a :b}
|
; (let [states [{:tag 1 :a :b}
|
||||||
{:tag 2 :c :d}]]
|
; {:tag 2 :c :d}]]
|
||||||
(s/add states {:tag 2 :c :d}) => states))
|
; (s/add states {:tag 2 :c :d}) => states))
|
||||||
|
|
|
@ -4,168 +4,168 @@
|
||||||
[grub.server-sync :as server-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]}
|
||||||
(server-sync/handle-event
|
; (server-sync/handle-event
|
||||||
{:type :diff
|
; {:type :diff
|
||||||
:tag 4
|
; :tag 4
|
||||||
:shadow-tag 0
|
; :shadow-tag 0
|
||||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
; :diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
:states states
|
; :states states
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
; :shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:client? false})]
|
; :client? false})]
|
||||||
@states => (just (just {:tag 0
|
; @states => (just (just {:tag 0
|
||||||
:grubs {"1" {:completed false, :text "2 apples"}}
|
; :grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
:recipes {}})
|
; :recipes {}})
|
||||||
(just {:tag 1
|
; (just {:tag 1
|
||||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
; :grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
:recipes {}}))
|
; :recipes {}}))
|
||||||
out-event => (just {:type :diff
|
; out-event => (just {:type :diff
|
||||||
:diff {:grubs {:- #{} :+ nil} :recipes {:- #{}, :+ nil}}
|
; :diff {:grubs {:- #{} :+ nil} :recipes {:- #{}, :+ nil}}
|
||||||
:shadow-tag 4
|
; :shadow-tag 4
|
||||||
:tag 1})
|
; :tag 1})
|
||||||
new-shadow => {:tag 4
|
; new-shadow => {:tag 4
|
||||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
; :grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
:recipes {}}))
|
; :recipes {}}))
|
||||||
|
;
|
||||||
(fact "Diff, server changes - Apply diff, don't return changes (now)"
|
; (fact "Diff, server changes - Apply diff, don't return changes (now)"
|
||||||
(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 {}}
|
||||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
; {:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
; "2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
{:keys [new-shadow out-event]}
|
; {:keys [new-shadow out-event]}
|
||||||
(server-sync/handle-event
|
; (server-sync/handle-event
|
||||||
{:type :diff
|
; {:type :diff
|
||||||
:shadow-tag 0
|
; :shadow-tag 0
|
||||||
:tag 4
|
; :tag 4
|
||||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
; :diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
:states states
|
; :states states
|
||||||
:shadow state/empty-state
|
; :shadow state/empty-state
|
||||||
:client? false})]
|
; :client? false})]
|
||||||
@states =>
|
; @states =>
|
||||||
(just {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
; (just {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
{:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
; {:tag 1 :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}} :recipes {}}
|
; "2" {:text "3 onions" :completed false}} :recipes {}}
|
||||||
{:tag 2 :grubs {"1" {:text "2 apples" :completed true}
|
; {:tag 2 :grubs {"1" {:text "2 apples" :completed true}
|
||||||
"2" {:text "3 onions" :completed false}} :recipes {}})
|
; "2" {:text "3 onions" :completed false}} :recipes {}})
|
||||||
out-event => nil
|
; out-event => nil
|
||||||
new-shadow => {:tag 4
|
; new-shadow => {:tag 4
|
||||||
:grubs {"1" {:text "2 apples" :completed true}}
|
; :grubs {"1" {:text "2 apples" :completed true}}
|
||||||
:recipes {}}))
|
; :recipes {}}))
|
||||||
|
;
|
||||||
(fact "Diff, client out of sync - Force full sync"
|
; (fact "Diff, client out of sync - Force full sync"
|
||||||
(let [states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
; (let [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}
|
||||||
"2" {:text "3 onions" :completed false}}
|
; "2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}])
|
; :recipes {}}])
|
||||||
event {:type :diff
|
; event {:type :diff
|
||||||
:shadow-tag 3
|
; :shadow-tag 3
|
||||||
:tag 12
|
; :tag 12
|
||||||
:diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
; :diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
||||||
:states states
|
; :states states
|
||||||
:shadow state/empty-state
|
; :shadow state/empty-state
|
||||||
:client? false}
|
; :client? false}
|
||||||
{:keys [new-shadow out-event]} (server-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}
|
||||||
"2" {:text "3 onions" :completed false}}
|
; "2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}}))
|
; :recipes {}}}))
|
||||||
|
;
|
||||||
(fact "New state - Update state, send diff, update shadow assuming diff received"
|
; (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 {}}])
|
; (let [states (atom [{:tag 14 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
event {:type :new-state
|
; event {:type :new-state
|
||||||
:states states
|
; :states states
|
||||||
: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]} (server-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 {}}
|
||||||
out-event => {:type :diff
|
; out-event => {:type :diff
|
||||||
:shadow-tag 3
|
; :shadow-tag 3
|
||||||
:tag 15
|
; :tag 15
|
||||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}
|
; :diff {:grubs {:+ {"1" {:completed true}} :- #{}}
|
||||||
:recipes {:+ nil :- #{}}}}))
|
; :recipes {:+ nil :- #{}}}}))
|
||||||
|
;
|
||||||
(fact "Server sends full sync if client requests it"
|
; (fact "Server sends full sync if client requests it"
|
||||||
(let [result (server-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}
|
||||||
"2" {:text "3 onions" :completed false}}
|
; "2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}])})]
|
; :recipes {}}])})]
|
||||||
(:new-shadow result) =>
|
; (:new-shadow result) =>
|
||||||
(just {:tag #(not (nil? %))
|
; (just {:tag #(not (nil? %))
|
||||||
:grubs {"1" {:text "2 apples" :completed false}
|
; :grubs {"1" {:text "2 apples" :completed false}
|
||||||
"2" {:text "3 onions" :completed false}}
|
; "2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}})
|
; :recipes {}})
|
||||||
(:out-event result) =>
|
; (:out-event result) =>
|
||||||
{:type :full-sync
|
; {: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}
|
||||||
"2" {:text "3 onions" :completed false}}
|
; "2" {:text "3 onions" :completed false}}
|
||||||
:recipes {}}}))
|
; :recipes {}}}))
|
||||||
)
|
; )
|
||||||
|
;
|
||||||
|
;
|
||||||
(facts "Client diffs"
|
;(facts "Client diffs"
|
||||||
(fact "Client applies diff, does not return diff when no client changes"
|
; (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 {}}])
|
; (let [states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
event {:type :diff
|
; event {:type :diff
|
||||||
:shadow-tag 0
|
; :shadow-tag 0
|
||||||
:tag 4
|
; :tag 4
|
||||||
:diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
; :diff {:grubs {:+ {"1" {:completed true}} :- #{}}}
|
||||||
:states states
|
; :states states
|
||||||
: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]} (client-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
|
||||||
:grubs {"1" {:completed true, :text "2 apples"}}
|
; :grubs {"1" {:completed true, :text "2 apples"}}
|
||||||
:recipes {}})
|
; :recipes {}})
|
||||||
new-shadow {:tag 4 :grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}
|
; new-shadow {:tag 4 :grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}
|
||||||
out-event => nil))
|
; out-event => nil))
|
||||||
|
;
|
||||||
(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 {}}])]
|
||||||
(client-sync/handle-event
|
; (client-sync/handle-event
|
||||||
{:type :diff
|
; {:type :diff
|
||||||
:shadow-tag 0
|
; :shadow-tag 0
|
||||||
:tag 4
|
; :tag 4
|
||||||
:diff {:grubs {:+ nil :- #{}}}
|
; :diff {:grubs {:+ nil :- #{}}}
|
||||||
:states states
|
; :states states
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
; :shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:client? true})
|
; :client? true})
|
||||||
@states => [{:tag 0
|
; @states => [{:tag 0
|
||||||
:grubs {"1" {:completed false, :text "2 apples"}}
|
; :grubs {"1" {:completed false, :text "2 apples"}}
|
||||||
:recipes {}}]))
|
; :recipes {}}]))
|
||||||
|
;
|
||||||
(fact "Client returns no response on empty diff"
|
; (fact "Client returns no response on empty diff"
|
||||||
(-> (client-sync/handle-event
|
; (-> (client-sync/handle-event
|
||||||
{:type :diff
|
; {:type :diff
|
||||||
:shadow-tag 0
|
; :shadow-tag 0
|
||||||
:tag 4
|
; :tag 4
|
||||||
:diff {:grubs {:+ nil :- #{}}}
|
; :diff {:grubs {:+ nil :- #{}}}
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
; :states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
; :shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
:client? true})
|
; :client? true})
|
||||||
:out-event)
|
; :out-event)
|
||||||
=> nil)
|
; => nil)
|
||||||
|
;
|
||||||
(fact "Client updates server shadow on empty diff"
|
; (fact "Client updates server shadow on empty diff"
|
||||||
(-> (client-sync/handle-event
|
; (-> (client-sync/handle-event
|
||||||
{:type :diff
|
; {:type :diff
|
||||||
:shadow-tag 0
|
; :shadow-tag 0
|
||||||
:tag 4
|
; :tag 4
|
||||||
:diff {:grubs {:+ nil :- #{}}}
|
; :diff {:grubs {:+ nil :- #{}}}
|
||||||
:states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
; :states (atom [{:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}])
|
||||||
:shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
; :shadow {:tag 0 :grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
: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 {}}))
|
||||||
|
|
Loading…
Reference in a new issue