From d775f1dab7fe43f71f9d8fc841090f6a2f7053b4 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 9 Aug 2014 15:31:31 +0300 Subject: [PATCH 01/40] Diff two states --- src/clj/grub/sync.clj | 21 +++++++++ src/test/grub/test/unit/sync.clj | 76 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/clj/grub/sync.clj create mode 100644 src/test/grub/test/unit/sync.clj diff --git a/src/clj/grub/sync.clj b/src/clj/grub/sync.clj new file mode 100644 index 0000000..e4cc300 --- /dev/null +++ b/src/clj/grub/sync.clj @@ -0,0 +1,21 @@ +(ns grub.sync + (:require [clojure.data :as data] + [clojure.pprint :as pprint :refer [pprint]] + [clojure.set :as set])) + +(defn deleted [a b] + (set/difference (into #{} (keys a)) (into #{} (keys b)))) + +(defn updated [a b] + (second (data/diff a b))) + +(defn diff [a b] + {:deleted (deleted a b) + :updated (changed a b)}) + +(defn diff-states [prev next] + (->> prev + (keys) + (map (fn [k] [k (diff (k prev) (k next))])) + (into {}))) + diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj new file mode 100644 index 0000000..c552fd3 --- /dev/null +++ b/src/test/grub/test/unit/sync.clj @@ -0,0 +1,76 @@ +(ns grub.test.unit.sync + (:require [grub.sync :as sync] + [clojure.test :refer :all])) + +(def server-state + {:grubs + {"grub-same" {:id "grub-same", + :completed false, + :grub "3 garlic cloves"} + "grub-completed" {:id "grub-completed", + :completed false, + :grub "2 tomatoes"} + "grub-updated" {:id "grub-updated", + :completed false, + :grub "BBQ sauce"} + "grub-deleted" {:id "grub-deleted" + :completed true + :grub "diapers"}} + :recipes + {"recipe-same" {:id "recipe-same" + :grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread" + :name "Blue Cheese Soup"} + "recipe-updated" {:id "recipe-updated" + :grubs "450 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n350 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n3 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)" + :name "Beef Stew"} + "recipe-deleted" {:id "recipe-deleted" + :grubs "8 slices rye bread\n400 g chicken breast\nBBQ sauce\nketchup\nmustard\nbutter\n1 package rocket\n4 tomatoes\n2 red onions\n1 bottle Coca Cola" + :name "Chickenburgers"}}}) + +(def client-state + {:grubs + {"grub-same" {:id "grub-same", + :completed false, + :grub "3 garlic cloves"} + "grub-completed" {:id "grub-completed", + :completed true, + :grub "2 tomatoes"} + "grub-updated" {:id "grub-updated", + :completed false, + :grub "Ketchup"} + "grub-added" {:id "grub-added" + :completed false + :grub "Toothpaste"}} + :recipes + {"recipe-same" {:id "recipe-same" + :grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread" + :name "Blue Cheese Soup"} + "recipe-updated" {:id "recipe-updated" + :grubs "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)" + :name "Beef Stew"} + "recipe-added" {:id "recipe-added" + :grubs "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola" + :name "Burgers"}}}) + +(def expected-diff + {:recipes + {:deleted #{"recipe-deleted"}, + :updated + {"recipe-added" + {:name "Burgers", + :id "recipe-added", + :grubs + "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"}, + "recipe-updated" + {:grubs + "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"}}}, + :grubs + {:deleted #{"grub-deleted"}, + :updated + {"grub-completed" {:completed true}, + "grub-updated" {:grub "Ketchup"}, + "grub-added" + {:completed false, :grub "Toothpaste", :id "grub-added"}}}}) + +(deftest diffing + (is (= (sync/diff-states server-state client-state) expected-diff))) From 5230dc9601be288e45b6dff4a0d64aea30ccd895 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 9 Aug 2014 16:03:06 +0300 Subject: [PATCH 02/40] Patch diff onto state --- src/clj/grub/sync.clj | 15 +++++++++++++-- src/test/grub/test/unit/sync.clj | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/clj/grub/sync.clj b/src/clj/grub/sync.clj index e4cc300..aa2ff0b 100644 --- a/src/clj/grub/sync.clj +++ b/src/clj/grub/sync.clj @@ -9,13 +9,24 @@ (defn updated [a b] (second (data/diff a b))) -(defn diff [a b] +(defn diff-maps [a b] {:deleted (deleted a b) :updated (changed a b)}) (defn diff-states [prev next] (->> prev (keys) - (map (fn [k] [k (diff (k prev) (k next))])) + (map (fn [k] [k (diff-maps (k prev) (k next))])) + (into {}))) + +(defn patch-map [state diff] + (-> state + (#(apply dissoc % (into [] (:deleted diff)))) + (#(merge-with merge % (:updated diff))))) + +(defn patch-state [state diff] + (->> state + (keys) + (map (fn [k] [k (patch-map (k state) (k diff))])) (into {}))) diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj index c552fd3..af44755 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/sync.clj @@ -73,4 +73,9 @@ {:completed false, :grub "Toothpaste", :id "grub-added"}}}}) (deftest diffing - (is (= (sync/diff-states server-state client-state) expected-diff))) + (is (= expected-diff (sync/diff-states server-state client-state)))) + +(deftest patching + (is + (let [diff (sync/diff-states server-state client-state)] + (= client-state (sync/patch-state server-state diff))))) From 11597a9b63d6f887621ac876cd17e639807d2834 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 9 Aug 2014 18:18:27 +0300 Subject: [PATCH 03/40] Update local state directly with om cursors --- src/cljs/grub/state.cljs | 8 ++--- src/cljs/grub/view/app.cljs | 29 ++++------------ src/cljs/grub/view/grub.cljs | 52 ++++++++------------------- src/cljs/grub/view/grub_list.cljs | 54 ++++++++++++++++++----------- src/cljs/grub/view/recipe.cljs | 51 +++++++++++---------------- src/cljs/grub/view/recipe_list.cljs | 43 +++++++++++++++-------- 6 files changed, 106 insertions(+), 131 deletions(-) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 81df91e..03317f1 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -66,13 +66,11 @@ (assoc state :recipes (dissoc (:recipes state) (:id event)))) (defn update-state-and-render [remote] - (let [out (chan) - view-events (view/render-app app-state)] + (view/render-app app-state) + (let [out (chan)] (go-loop [] - (let [[event ch] (alts! [remote view-events]) + (let [event (! out event)) (recur))) out)) diff --git a/src/cljs/grub/view/app.cljs b/src/cljs/grub/view/app.cljs index dfa73be..5f1463e 100644 --- a/src/cljs/grub/view/app.cljs +++ b/src/cljs/grub/view/app.cljs @@ -26,29 +26,12 @@ (dom/on-window-scroll #(put! >events {:type :body-scroll :event %})))))) (defn render-app [state] - (let [grub-add (chan) - grub-update (chan) - grub-clear-all (chan) - grub-remove (chan) - recipe-add (chan) - recipe-add-grubs (chan) - recipe-update (chan) - recipe-remove (chan) - out (a/merge [grub-add grub-update grub-clear-all grub-remove - recipe-add recipe-add-grubs recipe-update recipe-remove]) - >events (chan) - events :type)] + (let [>events (chan) + events :type) + add-grubs-ch (chan)] (om/root app-view state {:target (.getElementById js/document "container") - :shared {:grub-add grub-add - :grub-update grub-update - :grub-clear-all grub-clear-all - :grub-remove grub-remove - :recipe-add recipe-add - :recipe-add-grubs recipe-add-grubs - :recipe-update recipe-update - :recipe-remove recipe-remove - :>events >events - :events >events + :> grubs-str (clojure.string/split-lines) (map grub-view/new-grub) (into []))) +(defn map-by-key [key coll] + (->> coll + (map (fn [a] [(get a key) a])) + (into {}))) + (defn add-grubs [add-grubs-ch grubs-str] (let [grubs (parse-grubs-from-str grubs-str) - event (grub-view/add-list-event grubs)] - (put! add-grubs-ch event))) + grubs-map (map-by-key :id grubs)] + (put! add-grubs-ch grubs-map))) (def transitions {:waiting {:click :editing} @@ -44,38 +38,35 @@ (let [current (om/get-state owner :edit-state) next (or (get-in transitions [current event]) current)] (condp = [current next] - [:editing :waiting] (let [update-ch (om/get-shared owner :recipe-update) - id (:id @(om/get-props owner)) + [:editing :waiting] (let [recipe (om/get-props owner) name (om/get-state owner :name) - grubs (om/get-state owner :grubs) - event (update-event id name grubs)] - (put! update-ch event)) + grubs (om/get-state owner :grubs)] + (om/transact! recipe #(assoc % :name name :grubs grubs))) nil) (om/set-state! owner :edit-state next))) (defn num-newlines [str] (count (re-seq #"\n" str))) -(defn view [{:keys [id] :as props} owner] +(defn view [{:keys [id] :as recipe} owner {:keys [remove-recipe-ch]}] (reify om/IInitState (init-state [_] (let [publisher (chan)] {:edit-state :waiting - :name (:name props) - :grubs (:grubs props) + :name (:name recipe) + :grubs (:grubs recipe) :unmounted false})) om/IWillReceiveProps - (will-receive-props [this next-props] - (om/set-state! owner :name (:name next-props)) - (om/set-state! owner :grubs (:grubs next-props))) + (will-receive-props [this next-recipe] + (om/set-state! owner :name (:name next-recipe)) + (om/set-state! owner :grubs (:grubs next-recipe))) om/IRenderState (render-state [this {:keys [edit-state name grubs]}] - (let [update (om/get-shared owner :recipe-update) - add-grubs-ch (om/get-shared owner :recipe-add-grubs)] + (let [update (om/get-shared owner :recipe-update)] (html [:div.panel.panel-default.recipe-panel {:on-click @@ -92,7 +83,7 @@ {:type "button" :class (when (= edit-state :editing) "hidden") :ref :add-grubs-btn - :on-click #(add-grubs add-grubs-ch grubs)} + :on-click #(add-grubs (om/get-shared owner :add-grubs-ch) grubs)} [:span.glyphicon.glyphicon-plus] " Grubs"]] [:div.panel-body.recipe-grubs @@ -105,7 +96,7 @@ :on-change #(om/set-state! owner :grubs (dom/event-val %))}] [:button.btn.btn-danger.pull-left.recipe-remove-btn {:type "button" - :on-click #(put! (om/get-shared owner :recipe-remove) (remove-event id))} + :on-click #(put! remove-recipe-ch id)} [:span.glyphicon.glyphicon-trash]] [:button.btn.btn-primary.pull-right.recipe-done-btn {:type "button" diff --git a/src/cljs/grub/view/recipe_list.cljs b/src/cljs/grub/view/recipe_list.cljs index b16bc53..205493e 100644 --- a/src/cljs/grub/view/recipe_list.cljs +++ b/src/cljs/grub/view/recipe_list.cljs @@ -1,7 +1,7 @@ (ns grub.view.recipe-list (:require [om.core :as om :include-macros true] [sablono.core :as html :refer-macros [html]] - [cljs.core.async :as a :refer [ Date: Sat, 9 Aug 2014 18:40:36 +0300 Subject: [PATCH 04/40] Fix display of recipe titles so whole title shows --- public/css/styles.css | 15 ++++++++++++--- src/cljs/grub/view/recipe.cljs | 3 ++- src/cljs/grub/view/recipe_list.cljs | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/css/styles.css b/public/css/styles.css index 3a452c5..fec267f 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -23,9 +23,9 @@ h3 { } .recipe-add-grubs-btn { - float: right; - clear: both; + position: absolute; margin: 2px; + right: 16px; } .hidden { @@ -133,16 +133,25 @@ tr:hover .grub-close { .recipe-header { margin: 0px; padding: 0px; + padding-right: 72px; border-bottom: none; } +.recipe-header.edit { + padding-right: 0px; +} + +.recipe-header.new { + padding-right: 0px; +} + .recipe-header-input { border: none; box-shadow: none; transition: none; border-bottom: none; display: inline; - width: inherit; + width: 100%; } .recipe-header-input[readonly] { diff --git a/src/cljs/grub/view/recipe.cljs b/src/cljs/grub/view/recipe.cljs index 1f1ed57..5d7d0be 100644 --- a/src/cljs/grub/view/recipe.cljs +++ b/src/cljs/grub/view/recipe.cljs @@ -74,12 +74,13 @@ (dom/click-on-elem? % (om/get-node owner :save-btn)))) (transition-state owner :click))} [:div.panel-heading.recipe-header + {:class (when (= edit-state :editing) "edit")} [:input.form-control.recipe-header-input {:type "text" :readOnly (if (= edit-state :editing) "" "readonly") :value name :on-change #(om/set-state! owner :name (dom/event-val %))}] - [:button.btn.btn-primary.btn-sm.recipe-add-grubs-btn + [:button.btn.btn-primary.btn-sm.recipe-add-grubs-btn {:type "button" :class (when (= edit-state :editing) "hidden") :ref :add-grubs-btn diff --git a/src/cljs/grub/view/recipe_list.cljs b/src/cljs/grub/view/recipe_list.cljs index 205493e..bc2c390 100644 --- a/src/cljs/grub/view/recipe_list.cljs +++ b/src/cljs/grub/view/recipe_list.cljs @@ -49,7 +49,7 @@ [:div.panel.panel-default.recipe-panel {:on-click #(when (not (dom/click-on-elem? % (om/get-node owner :save-btn))) (transition-state owner :click))} - [:div.panel-heading.recipe-header + [:div.panel-heading.recipe-header.new [:input.form-control.recipe-header-input {:id "new-recipe-name" :type "text" From becfb42627d023015545a959d2027d9b65f5670d Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sun, 10 Aug 2014 00:26:35 +0300 Subject: [PATCH 05/40] Sync state using diffs instead of events --- project.clj | 14 ++- src/clj/grub/core.clj | 32 +++-- src/clj/grub/db.clj | 114 ++++++------------ src/clj/grub/state.clj | 40 ++++++ src/clj/grub/websocket.clj | 95 +++------------ src/cljs/grub/core.cljs | 11 +- src/cljs/grub/state.cljs | 73 ++--------- src/cljs/grub/view/app.cljs | 10 +- src/cljs/grub/view/grub.cljs | 8 +- src/cljs/grub/view/grub_list.cljs | 18 +-- src/cljs/grub/view/recipe.cljs | 16 +-- src/cljs/grub/view/recipe_list.cljs | 12 +- src/cljs/grub/websocket.cljs | 1 - .../grub/sync.clj => cljx/grub/sync.cljx} | 4 +- src/cljx/grub/util.cljx | 7 ++ src/test/grub/test/integration/core.clj | 16 +-- src/test/grub/test/unit/sync.clj | 75 +++++------- 17 files changed, 216 insertions(+), 330 deletions(-) create mode 100644 src/clj/grub/state.clj rename src/{clj/grub/sync.clj => cljx/grub/sync.cljx} (88%) create mode 100644 src/cljx/grub/util.cljx diff --git a/project.clj b/project.clj index 5a44bb6..28ea934 100644 --- a/project.clj +++ b/project.clj @@ -21,18 +21,26 @@ :profiles {:uberjar {:aot :all}} :min-lein-version "2.1.2" :plugins [[lein-cljsbuild "1.0.3"] - [lein-ring "0.8.6"]] - :cljsbuild {:builds {:dev {:source-paths ["src/cljs"] + [lein-ring "0.8.6"] + [com.keminglabs/cljx "0.4.0"]] + :cljsbuild {:builds {:dev {:source-paths ["src/cljs" "target/generated/cljs"] :compiler {:output-dir "public/js/out" :output-to "public/js/grub.js" :optimizations :none :source-map true}} - :prod {:source-paths ["src/cljs"] + :prod {:source-paths ["src/cljs" "target/generated/cljs"] :compiler {:output-to "public/js/grub.min.js" :optimizations :advanced :pretty-print false :preamble ["react/react.min.js"] :externs ["react/externs/react.js"]}}}} + :cljx {:builds [{:source-paths ["src/cljx"] + :output-path "target/classes" + :rules :clj} + {:source-paths ["src/cljx"] + :output-path "target/generated/cljs" + :rules :cljs}]} + :hooks [cljx.hooks] :source-paths ["src/clj" "src/test"] :test-paths ["spec/clj"] :ring {:handler grub.core/app} diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index 05b5d4e..f4f1313 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -2,13 +2,14 @@ (:require [grub.websocket :as ws] [grub.db :as db] [grub.test.integration.core :as integration-test] - [ring.middleware.reload :as reload] + [grub.state :as state] [ring.middleware.file :as file] [ring.util.response :as resp] [compojure.core :refer [defroutes GET POST]] [compojure.handler :as handler] [compojure.route :as route] [org.httpkit.server :as httpkit] + [clojure.core.async :as a :refer [! chan go]] [hiccup [page :refer [html5]] [page :refer [include-js include-css]]] @@ -41,40 +42,45 @@ (def index-page (atom dev-index-page)) +(defn websocket-handler [request] + (when (:websocket? request) + (httpkit/with-channel request ws-channel + (let [to-client (chan) + from-client (chan)] + (ws/add-client! ws-channel to-client from-client) + (state/add-client! to-client from-client))))) + (defroutes routes - (GET "/" [] ws/websocket-handler) + (GET "/" [] websocket-handler) (GET "/" [] @index-page) (GET "*/src/cljs/grub/:file" [file] (resp/file-response file {:root "src/cljs/grub"})) (GET "/js/public/js/:file" [file] (resp/redirect (str "/js/" file))) (route/files "/") (route/not-found "

Page not found.

")) -(def app - (let [dev? true] - (if dev? - (reload/wrap-reload (handler/site #'routes) {:dirs ["src/clj"]}) - (handler/site routes)))) - (def default-port 3000) (defn start-server [port] - (httpkit/run-server app {:port port})) + (httpkit/run-server (handler/site routes) {:port port})) (defn run-integration-test [] (let [stop-server (start-server integration-test/server-port)] + (println "Starting integration test server on localhost:" integration-test/server-port) (integration-test/run) (stop-server))) (defn start-production-server [{:keys [port mongo-url]}] (reset! index-page prod-index-page) - (let [db-chan (db/connect-production-database mongo-url)] - (ws/pass-received-events-to-clients-and-db db-chan) + (let [to-db (chan)] + (db/connect-production-database to-db mongo-url) + (state/init to-db (db/get-current-grubs) (db/get-current-recipes)) (println "Starting production server on localhost:" port) (start-server port))) (defn start-development-server [{:keys [port]}] - (let [db-chan (db/connect-development-database)] - (ws/pass-received-events-to-clients-and-db db-chan) + (let [to-db (chan)] + (db/connect-development-database to-db) + (state/init to-db (db/get-current-grubs) (db/get-current-recipes)) (println "Starting development server on localhost:" port) (start-server port))) diff --git a/src/clj/grub/db.clj b/src/clj/grub/db.clj index 6769897..7e2765b 100644 --- a/src/clj/grub/db.clj +++ b/src/clj/grub/db.clj @@ -1,5 +1,6 @@ (ns grub.db - (:require [monger.core :as m] + (:require [grub.util :as util] + [monger.core :as m] [monger.collection :as mc] [monger.operators :as mo] [clojure.core.async :as a :refer [! chan go]])) @@ -8,6 +9,8 @@ (def db (atom nil)) (def grub-collection "grubs") (def recipe-collection "recipes") +(def production-db "grub") +(def development-db "grub-dev") (defn clear-grubs [] (mc/drop @db grub-collection)) @@ -19,80 +22,33 @@ (clear-grubs) (clear-recipes)) -(defmulti handle-event :event :default :unknown-event) - -(defn insert-grub [event] - (let [grub (-> event - (select-keys [:id :grub :completed]) - (clojure.set/rename-keys {:id :_id}))] - (mc/insert @db grub-collection grub))) - -(defmethod handle-event :add-grub [event] - (insert-grub event)) - -(defmethod handle-event :add-grub-list [event] - (doseq [grub-event (:grubs event)] - (insert-grub grub-event))) - -(defmethod handle-event :complete-grub [event] - (mc/update @db grub-collection - {:_id (:id event)} - {mo/$set {:completed true}})) - -(defmethod handle-event :uncomplete-grub [event] - (mc/update @db grub-collection - {:_id (:id event)} - {mo/$set {:completed false}})) - -(defmethod handle-event :update-grub [event] - (let [orig (mc/find-one-as-map @db grub-collection {:_id (:id event)}) - new (dissoc event :event-type :id)] - (mc/update-by-id @db grub-collection (:id event) (merge orig new)))) - -(defmethod handle-event :clear-all-grubs [event] - (clear-grubs)) - -(defmethod handle-event :remove-grub [event] - (mc/remove-by-id @db grub-collection (:id event))) - -(defmethod handle-event :add-recipe [event] - (let [recipe (-> event - (select-keys [:id :name :grubs]) - (clojure.set/rename-keys {:id :_id}))] - (mc/insert @db recipe-collection recipe))) - -(defmethod handle-event :update-recipe [event] - (mc/update @db recipe-collection - {:_id (:id event)} - {mo/$set {:name (:name event) :grubs (:grubs event)}})) - -(defmethod handle-event :remove-recipe [event] - (mc/remove-by-id @db recipe-collection (:id event))) - -(defmethod handle-event :unknown-event [event] - (println "Cannot handle unknown event:" event)) +(defn update-db! [{:keys [grubs recipes]}] + (let [deleted-grubs (:deleted grubs) + updated-grubs (->> (:updated grubs) + (seq) + (map (fn [[k v]] (assoc v :_id v)))) + deleted-recipes (:deleted recipes) + updated-recipes (->> (:updated recipes) + (seq) + (map (fn [[k v]] (assoc v :_id v))))] + (doseq [g deleted-grubs] + (mc/remove-by-id @db grub-collection g)) + (doseq [g updated-grubs] + (mc/update-by-id @db grub-collection (:_id g) g {:upsert true})) + (doseq [r deleted-recipes] + (mc/remove-by-id @db recipe-collection r)) + (doseq [r updated-recipes] + (mc/update-by-id @db recipe-collection (:_id r) r {:upsert true})))) (defn get-current-grubs [] (->> (mc/find-maps @db grub-collection) (sort-by :_id) - (map #(select-keys % [:_id :grub :completed])) - (map #(clojure.set/rename-keys % {:_id :id})) - (vec))) + (map #(clojure.set/rename-keys % {:_id :id})))) (defn get-current-recipes [] (->> (mc/find-maps @db recipe-collection) (sort-by :_id) - (map #(select-keys % [:_id :name :grubs])) - (map #(clojure.set/rename-keys % {:_id :id})) - (vec))) - -(def production-db "grub") -(def development-db "grub-dev") - -(defn handle-incoming-events [in] - (a/go-loop [] (let [event (! chan go]])) + +(def empty-state + {:grubs {} + :recipes {}}) + +(def state (atom empty-state)) +(def to-db (atom nil)) +(def to-all (chan)) +(def from-all (a/mult to-all)) + +(defn get-initial-state [grubs recipes] + {:grubs (util/map-by-key :id grubs) + :recipes (util/map-by-key :id recipes)}) + +(defn add-client! [to from] + (let [client-id (java.util.UUID/randomUUID)] + (println "New client id:" client-id) + (a/go-loop [] + (when-let [diff (! @to-db diff) + (>! to-all {:diff diff :source-id client-id}) + (recur))) + (let [all-diffs (chan)] + (a/tap from-all all-diffs) + (a/go-loop [] (if-let [{:keys [diff source-id] :as event} (! to diff)) + (recur)) + (a/untap from-all all-diffs)))) + (a/put! to (sync/diff-states empty-state @state)))) + +(defn init [_to-db grubs recipes] + (reset! state (get-initial-state grubs recipes)) + (reset! to-db _to-db)) diff --git a/src/clj/grub/websocket.clj b/src/clj/grub/websocket.clj index dbade3c..fdc9538 100644 --- a/src/clj/grub/websocket.clj +++ b/src/clj/grub/websocket.clj @@ -3,83 +3,18 @@ [org.httpkit.server :as httpkit] [clojure.core.async :as a :refer [! chan go]])) -(def incoming-events (chan)) - -(def connected-clients (atom {})) - -(def ws-channel-id-count (atom 0)) - -(defn get-unique-ws-id [] - (swap! ws-channel-id-count inc)) - -(defn add-connected-client! [ws-channel] - (let [ws-channel-id (get-unique-ws-id) - client-chan (chan)] - (swap! connected-clients #(assoc % ws-channel-id client-chan)) - [ws-channel-id client-chan])) - -(defn remove-connected-client! [status ws-channel ws-channel-id client-chan] - (println "Client disconnected:" - (.toString ws-channel) - (str "(" ws-channel-id ")") - "with status" status) - (swap! connected-clients #(dissoc % ws-channel-id)) - (println (count @connected-clients) "client(s) still connected") - (a/close! client-chan)) - -(defn send-current-grubs-and-recipes-to-client [client-chan] - (let [add-grubs-event {:event :add-grub-list - :grubs (db/get-current-grubs)} - add-recipes-event {:event :add-recipe-list - :recipes (db/get-current-recipes)}] - (go (>! client-chan add-grubs-event) - (>! client-chan add-recipes-event)))) - -(defn on-receive [raw-event ws-channel-id client-chan] - (let [parsed-event (read-string raw-event) - event (assoc parsed-event :ws-channel ws-channel-id)] - (println "Received event" event) - (if (= (:event event) :send-all-items) - (send-current-grubs-and-recipes-to-client client-chan) - (go (>! incoming-events event))))) - -(defn forward-other-events-to-client [c ws-channel] - (a/go-loop [] - (when-let [event ( @connected-clients - (dissoc my-ws-channel-id) - (vals))) - -(defn push-event-to-others [orig-event] - (let [my-ws-channel-id (:ws-channel orig-event) - event (dissoc orig-event :ws-channel)] - (go (doseq [c (get-other-client-channels my-ws-channel-id)] - (>! c event))))) - -(defn pass-received-events-to-clients-and-db [db-chan] - (let [in' (a/mult incoming-events) - to-others (chan) - to-database (chan)] - (a/tap in' to-others) - (a/tap in' to-database) - (a/go-loop [] (let [event ( #(dissoc % :ws-channel) db-chan)))) +(defn add-client! [ws-channel to from] + (println "Client connected:" (.toString ws-channel)) + (httpkit/on-close ws-channel + (fn [status] + (println "Client disconnected:" (.toString ws-channel) + "with status" status) + (a/close! to) + (a/close! from))) + (httpkit/on-receive ws-channel #(a/put! from (read-string %))) + (a/go-loop [] + (if-let [event (! chan]]) - (:require-macros [grub.macros :refer [log logs]] - [cljs.core.async.macros :refer [go]])) + (:require-macros [grub.macros :refer [log logs]])) -(defn wire-channels-together [] +(defn init-app [] + (view/render-app state/app-state) (let [to-remote (chan) to-state (chan) from-remote (ws/get-remote-chan to-remote) - from-state (state/update-state-and-render to-state)] + from-state (state/update-state-on-event! to-state)] (a/pipe from-remote to-state) (a/pipe from-state to-remote))) -(wire-channels-together) +(init-app) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 03317f1..b107bbe 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -1,5 +1,5 @@ (ns grub.state - (:require [grub.view.app :as view] + (:require [grub.sync :as sync] [cljs.core.async :as a :refer [! chan]]) (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go go-loop]])) @@ -7,70 +7,15 @@ (def app-state (atom {:grubs {} :recipes {}})) -(defmulti handle-event (fn [event state] (:event event)) - :default :unknown-event) - -(defmethod handle-event :unknown-event [event state] - state) - -(defn new-grub [id grub completed] - {:id id :grub grub :completed completed}) - -(defmethod handle-event :add-grub [event state] - (let [grub (new-grub (:id event) (:grub event) (:completed event))] - (assoc-in state [:grubs (:id grub)] grub))) - -(defn map-by-key [key coll] - (->> coll - (map (fn [a] [(get a key) a])) - (into {}))) - -(defmethod handle-event :add-grub-list [event state] - (->> event - :grubs - (map-by-key :id) - (merge (:grubs state)) - (assoc state :grubs))) - -(defmethod handle-event :update-grub [event state] - (let [new-grub-info (dissoc event :event-type) - orig-grub (get-in state [:grubs (:id event)])] - (assoc-in state [:grubs (:id event)] (merge orig-grub new-grub-info)))) - -(defmethod handle-event :clear-all-grubs [event state] - (assoc state :grubs {})) - -(defmethod handle-event :remove-grub [event state] - (assoc state :grubs (dissoc (:grubs state) (:id event)))) - -(defn new-recipe [id name grubs] - {:id id :name name :grubs grubs}) - -(defmethod handle-event :add-recipe [event state] - (let [recipe (new-recipe (:id event) (:name event) (:grubs event))] - (assoc-in state [:recipes (:id recipe)] recipe))) - -(defmethod handle-event :add-recipe-list [event state] - (->> event - :recipes - (map-by-key :id) - (merge (:recipes state)) - (assoc state :recipes))) - -(defmethod handle-event :update-recipe [event state] - (-> state - (assoc-in [:recipes (:id event) :name] (:name event)) - (assoc-in [:recipes (:id event) :grubs] (:grubs event)))) - -(defmethod handle-event :remove-recipe [event state] - (assoc state :recipes (dissoc (:recipes state) (:id event)))) - -(defn update-state-and-render [remote] - (view/render-app app-state) +(defn update-state-on-event! [in] (let [out (chan)] + (add-watch app-state :app-state + (fn [key ref old new] + (when-not (= old new) + (let [diff (sync/diff-states old new)] + (a/put! out diff))))) (go-loop [] - (let [event (> grubs + (vals) + (sort-by (juxt :completed get-grub-ingredient :text)))) (defn add-grub [owner grubs new-grub-text] (when (not (empty? new-grub-text)) @@ -47,7 +49,7 @@ {:id "add-grub-btn" :type "button" :on-click #(add-grub owner grubs new-grub-text)} - [:span.glyphicon.glyphicon-plus]]] + [:span.glyphicon.glyphicon-plus#add-grub-btn]]] [:ul#grub-list.list-group (for [grub (sort-grubs grubs)] (om/build grub-view/view grub {:key :id :opts {:remove-ch remove-grub-ch}}))] diff --git a/src/cljs/grub/view/recipe.cljs b/src/cljs/grub/view/recipe.cljs index 5d7d0be..63c2bd5 100644 --- a/src/cljs/grub/view/recipe.cljs +++ b/src/cljs/grub/view/recipe.cljs @@ -1,10 +1,11 @@ (ns grub.view.recipe - (:require [om.core :as om :include-macros true] + (:require [grub.view.dom :as dom] + [grub.view.grub :as grub-view] + [grub.util :as util] + [om.core :as om :include-macros true] [sablono.core :as html :refer-macros [html]] [cljs.core.async :as a :refer [> coll - (map (fn [a] [(get a key) a])) - (into {}))) - (defn add-grubs [add-grubs-ch grubs-str] (let [grubs (parse-grubs-from-str grubs-str) - grubs-map (map-by-key :id grubs)] + grubs-map (util/map-by-key :id grubs)] (put! add-grubs-ch grubs-map))) (def transitions diff --git a/src/cljs/grub/view/recipe_list.cljs b/src/cljs/grub/view/recipe_list.cljs index bc2c390..4c184d9 100644 --- a/src/cljs/grub/view/recipe_list.cljs +++ b/src/cljs/grub/view/recipe_list.cljs @@ -1,11 +1,11 @@ (ns grub.view.recipe-list - (:require [om.core :as om :include-macros true] + (:require [grub.view.dom :as dom] + [grub.view.grub :as grub-view] + [grub.view.recipe :as recipe] + [om.core :as om :include-macros true] [sablono.core :as html :refer-macros [html]] [cljs.core.async :as a :refer [! to-remote {:event :send-all-items})) (.open @websocket* server-url) remote-events)) diff --git a/src/clj/grub/sync.clj b/src/cljx/grub/sync.cljx similarity index 88% rename from src/clj/grub/sync.clj rename to src/cljx/grub/sync.cljx index aa2ff0b..f18f26c 100644 --- a/src/clj/grub/sync.clj +++ b/src/cljx/grub/sync.cljx @@ -1,6 +1,5 @@ (ns grub.sync (:require [clojure.data :as data] - [clojure.pprint :as pprint :refer [pprint]] [clojure.set :as set])) (defn deleted [a b] @@ -11,7 +10,7 @@ (defn diff-maps [a b] {:deleted (deleted a b) - :updated (changed a b)}) + :updated (updated a b)}) (defn diff-states [prev next] (->> prev @@ -29,4 +28,3 @@ (keys) (map (fn [k] [k (patch-map (k state) (k diff))])) (into {}))) - diff --git a/src/cljx/grub/util.cljx b/src/cljx/grub/util.cljx new file mode 100644 index 0000000..861229a --- /dev/null +++ b/src/cljx/grub/util.cljx @@ -0,0 +1,7 @@ +(ns grub.util) + +(defn map-by-key [key coll] + (->> coll + (map (fn [a] [(get a key) a])) + (into {}))) + diff --git a/src/test/grub/test/integration/core.clj b/src/test/grub/test/integration/core.clj index e14123c..af172f3 100644 --- a/src/test/grub/test/integration/core.clj +++ b/src/test/grub/test/integration/core.clj @@ -1,6 +1,8 @@ (ns grub.test.integration.core (:require [grub.db :as db] [grub.websocket :as ws] + [grub.state :as state] + [clojure.core.async :as a :refer [! chan go]] [clj-webdriver.taxi :as taxi] [clj-webdriver.core :as webdriver] [clojure.test :as test])) @@ -20,7 +22,7 @@ (defn add-grub [driver grub-text] (taxi/input-text driver "#add-grub-input" grub-text) - (taxi/click driver {:text "Add"})) + (taxi/click driver "#add-grub-btn")) (defn test-grubs-saved-to-server [url driver] (taxi/to driver url) @@ -31,7 +33,7 @@ (taxi/refresh driver) (Thread/sleep 200) (doseq [grub grubs] - (test/is (taxi/find-element driver {:text grub}) + (test/is (taxi/find-element driver {:value grub}) "Previously added grubs should be loaded on refresh"))) (db/clear-grubs)) @@ -42,7 +44,7 @@ (doseq [grub grubs] (add-grub driver1 grub)) (doseq [grub grubs] - (test/is (taxi/find-element driver2 {:text grub}) + (test/is (taxi/find-element driver2 {:value grub}) "Added grubs should appear in other browser")))) (defn get-rand-recipe [] @@ -53,7 +55,7 @@ (taxi/click driver "#new-recipe-name") (taxi/input-text driver "#new-recipe-name" name) (taxi/input-text driver "#new-recipe-grubs" grubs) - (taxi/click driver {:text "Done"})) + (taxi/click driver "#save-recipe-btn")) (defn test-added-recipes-sync [url driver1 driver2] (taxi/to driver1 url) @@ -71,9 +73,9 @@ (test-added-recipes-sync site-url driver1 driver2)) (defn start-db-and-websocket-server! [] - (let [db-chan (db/connect-and-handle-events "grub-integration-test")] - (db/clear-all) - (ws/pass-received-events-to-clients-and-db db-chan))) + (let [to-db (chan)] + (db/connect-and-handle-events to-db "grub-integration-test") + (state/init to-db (db/get-current-grubs) (db/get-current-recipes)))) (defn run [] (println "Starting integration test") diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj index af44755..c11197e 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/sync.clj @@ -4,73 +4,58 @@ (def server-state {:grubs - {"grub-same" {:id "grub-same", - :completed false, - :grub "3 garlic cloves"} - "grub-completed" {:id "grub-completed", - :completed false, - :grub "2 tomatoes"} - "grub-updated" {:id "grub-updated", - :completed false, - :grub "BBQ sauce"} - "grub-deleted" {:id "grub-deleted" - :completed true - :grub "diapers"}} + {"grub-same" {:completed false + :text "3 garlic cloves"} + "grub-completed" {:completed false + :text "2 tomatoes"} + "grub-updated" {:completed false + :text "BBQ sauce"} + "grub-deleted" {:completed true + :text "diapers"}} :recipes - {"recipe-same" {:id "recipe-same" - :grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread" + {"recipe-same" {:grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread" :name "Blue Cheese Soup"} - "recipe-updated" {:id "recipe-updated" - :grubs "450 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n350 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n3 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)" + "recipe-updated" {:grubs "450 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n350 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n3 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)" :name "Beef Stew"} - "recipe-deleted" {:id "recipe-deleted" - :grubs "8 slices rye bread\n400 g chicken breast\nBBQ sauce\nketchup\nmustard\nbutter\n1 package rocket\n4 tomatoes\n2 red onions\n1 bottle Coca Cola" + "recipe-deleted" {:grubs "8 slices rye bread\n400 g chicken breast\nBBQ sauce\nketchup\nmustard\nbutter\n1 package rocket\n4 tomatoes\n2 red onions\n1 bottle Coca Cola" :name "Chickenburgers"}}}) (def client-state {:grubs - {"grub-same" {:id "grub-same", - :completed false, - :grub "3 garlic cloves"} - "grub-completed" {:id "grub-completed", - :completed true, - :grub "2 tomatoes"} - "grub-updated" {:id "grub-updated", - :completed false, - :grub "Ketchup"} - "grub-added" {:id "grub-added" - :completed false - :grub "Toothpaste"}} + {"grub-same" {:completed false, + :text "3 garlic cloves"} + "grub-completed" {:completed true, + :text "2 tomatoes"} + "grub-updated" {:completed false, + :text "Ketchup"} + "grub-added" {:completed false + :text "Toothpaste"}} :recipes - {"recipe-same" {:id "recipe-same" - :grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread" + {"recipe-same" {:grubs "3 T. butter\n1 yellow onion\n1 1/2 dl red pepper\n1 dl apple\n3 garlic cloves\n1 t. curry\n3 dl water\n2-2 1/2 T. wheat flour\n1 kasvisliemikuutio\n200 g blue cheese\n2 dl apple juice\n2 dl milk\n1 t. basil\n1 package take-and-bake french bread" :name "Blue Cheese Soup"} - "recipe-updated" {:id "recipe-updated" - :grubs "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)" + "recipe-updated" {:grubs "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)" :name "Beef Stew"} - "recipe-added" {:id "recipe-added" - :grubs "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola" + "recipe-added" {:grubs "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola" :name "Burgers"}}}) (def expected-diff {:recipes - {:deleted #{"recipe-deleted"}, + {:deleted #{"recipe-deleted"} :updated {"recipe-added" - {:name "Burgers", - :id "recipe-added", + {:name "Burgers" :grubs - "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"}, + "400 g ground beef\nhamburger buns\n2 red onions\n4 tomatoes\ncheddar cheese\nketchup\nmustard\npickles\nfresh basil\n1 bottle Coca Cola"} "recipe-updated" {:grubs - "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"}}}, + "300 g lean stew beef (lapa/naudan etuselkä), cut into 1-inch cubes\n2 T. vegetable oil\n5 dl water\n2 lihaliemikuutios\n400 ml burgundy (or another red wine)\n1 garlic clove\n1 bay leaf (laakerinlehti)\n1/2 t. basil\n2 carrots\n1 yellow onion\n4 potatoes\n1 cup celery\n2 tablespoons of cornstarch (maissijauho/maizena)"}}} :grubs - {:deleted #{"grub-deleted"}, + {:deleted #{"grub-deleted"} :updated - {"grub-completed" {:completed true}, - "grub-updated" {:grub "Ketchup"}, + {"grub-completed" {:completed true} + "grub-updated" {:text "Ketchup"} "grub-added" - {:completed false, :grub "Toothpaste", :id "grub-added"}}}}) + {:completed false :text "Toothpaste"}}}}) (deftest diffing (is (= expected-diff (sync/diff-states server-state client-state)))) From a672155a92cd40110ffa1c47e1204a7130e7bd2e Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sun, 10 Aug 2014 22:53:42 +0300 Subject: [PATCH 06/40] State checks - wip --- src/clj/grub/db.clj | 10 +++++-- src/clj/grub/state.clj | 53 +++++++++++++++++++++++++----------- src/cljs/grub/state.cljs | 24 +++++++++------- src/cljs/grub/websocket.cljs | 42 ++++++++++++++++------------ 4 files changed, 84 insertions(+), 45 deletions(-) diff --git a/src/clj/grub/db.clj b/src/clj/grub/db.clj index 7e2765b..90cdac5 100644 --- a/src/clj/grub/db.clj +++ b/src/clj/grub/db.clj @@ -26,11 +26,17 @@ (let [deleted-grubs (:deleted grubs) updated-grubs (->> (:updated grubs) (seq) - (map (fn [[k v]] (assoc v :_id v)))) + (map (fn [[k v]] + (-> v + (dissoc :id) + (assoc :_id k))))) deleted-recipes (:deleted recipes) updated-recipes (->> (:updated recipes) (seq) - (map (fn [[k v]] (assoc v :_id v))))] + (map (fn [[k v]] + (-> v + (dissoc :id) + (assoc :_id k)))))] (doseq [g deleted-grubs] (mc/remove-by-id @db grub-collection g)) (doseq [g updated-grubs] diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 0e7be18..0ed189b 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -16,24 +16,45 @@ {:grubs (util/map-by-key :id grubs) :recipes (util/map-by-key :id recipes)}) +(defn sync-remote-changes [to-client state* server-shadow] + (let [server-shadow* @server-shadow] + (when (not= state* server-shadow*) + (let [diff (sync/diff-states server-shadow* state*) + msg {:diff diff + :hash (hash state*) + :shadow-hash (hash server-shadow*)}] + (println "Sync because:") + (println "Server = " state*) + (println "Client = " server-shadow*) + (println "Diff:" diff) + (println "Send" (hash server-shadow*) "->" (hash state*)) + (a/put! to-client msg) + ;; TODO: only reset server shadow if send succeeds + (reset! server-shadow state*))))) + (defn add-client! [to from] - (let [client-id (java.util.UUID/randomUUID)] - (println "New client id:" client-id) + (let [client-id (java.util.UUID/randomUUID) + server-shadow (atom empty-state)] + (add-watch state client-id (fn [k ref old new] + (sync-remote-changes to new server-shadow))) (a/go-loop [] - (when-let [diff (! @to-db diff) - (>! to-all {:diff diff :source-id client-id}) - (recur))) - (let [all-diffs (chan)] - (a/tap from-all all-diffs) - (a/go-loop [] (if-let [{:keys [diff source-id] :as event} (! to diff)) - (recur)) - (a/untap from-all all-diffs)))) - (a/put! to (sync/diff-states empty-state @state)))) + (if-let [{:keys [diff hash shadow-hash]} (" hash) + (println "Before shadow:" (clojure.core/hash @server-shadow) @server-shadow) + (if (= (clojure.core/hash @server-shadow) shadow-hash) + (println "Before hash check: good") + (println "Before hash check: FAIL")) + (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) + new-state (swap! state #(sync/patch-state % diff))] + ;; TODO: check if hashes match + (println "After shadow:" (clojure.core/hash new-shadow) new-shadow) + (if (= (clojure.core/hash new-shadow) hash) + (println "After hash check: good") + (println "After hash check: FAIL")) + (>! @to-db diff) + (recur))) + (remove-watch state client-id))))) (defn init [_to-db grubs recipes] (reset! state (get-initial-state grubs recipes)) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index b107bbe..99b76db 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -4,18 +4,22 @@ (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go go-loop]])) -(def app-state (atom {:grubs {} - :recipes {}})) +(def empty-state {:grubs {} :recipes {}}) +(def app-state (atom empty-state)) +(def client-shadow (atom empty-state)) (defn update-state-on-event! [in] (let [out (chan)] - (add-watch app-state :app-state - (fn [key ref old new] - (when-not (= old new) - (let [diff (sync/diff-states old new)] - (a/put! out diff))))) (go-loop [] - (when-let [diff (" hash) + (if (= (cljs.core/hash @client-shadow) shadow-hash) + (log "Before hash check: good") + (log "Before hash check: FAIL")) + (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) + new-state (swap! app-state #(sync/patch-state % diff))] + (if (= (cljs.core/hash new-shadow) hash) + (log "After hash check: good") + (log "After hash check: FAIL")) + (recur)))) out)) diff --git a/src/cljs/grub/websocket.cljs b/src/cljs/grub/websocket.cljs index f1d805f..788567b 100644 --- a/src/cljs/grub/websocket.cljs +++ b/src/cljs/grub/websocket.cljs @@ -1,5 +1,7 @@ (ns grub.websocket - (:require [cljs.core.async :as a :refer [! chan]] + (:require [grub.state :as state] + [grub.sync :as sync] + [cljs.core.async :as a :refer [! chan]] [cljs.reader] goog.net.WebSocket goog.events.EventHandler @@ -8,27 +10,33 @@ [grub.macros :refer [log logs]])) (def websocket* (atom nil)) -(def pending-events (atom [])) + +(defn sync-local-changes [] + (when (and (.isOpen @websocket*) + (not= @state/app-state @state/client-shadow)) + (let [app-state @state/app-state + client-shadow @state/client-shadow + diff (sync/diff-states client-shadow app-state) + msg {:diff diff + :hash (hash app-state) + :shadow-hash (hash client-shadow)}] + (logs "Sync because:") + (logs "Server = " client-shadow) + (logs "Client = " app-state) + (logs "Diff:" diff) + (logs "Send" (hash client-shadow) "->" (hash app-state)) + ;; TODO: reset client shadow only if send succeeds + (.send @websocket* msg) + (reset! state/client-shadow app-state)))) (defn on-connected [event] (log "Connected:" event) - (when (> (count @pending-events)) - (doseq [event @pending-events] (.send @websocket* event)) - (reset! pending-events []))) - -(defn send-outgoing-events [ch] - (go-loop [] - (let [event (! out grub-event))))) - + (let [msg (cljs.reader/read-string (.-message event))] + (a/put! out msg)))) (defn get-remote-chan [to-remote] (let [server-url (str "ws://" (.-host (.-location js/document))) @@ -39,6 +47,6 @@ (.listen handler @websocket* goog.net.WebSocket.EventType.MESSAGE (on-message-fn remote-events) false) (.listen handler @websocket* goog.net.WebSocket.EventType.CLOSED #(log "Closed:" %) false) (.listen handler @websocket* goog.net.WebSocket.EventType.ERROR #(log "Error:" %) false) - (send-outgoing-events to-remote) + (add-watch state/app-state :app-state #(sync-local-changes)) (.open @websocket* server-url) remote-events)) From 4ac42b0cf35089f0029477291b9e629a0e70d8c0 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Wed, 13 Aug 2014 21:37:17 +0300 Subject: [PATCH 07/40] Working hash checks a la hasch --- project.clj | 3 ++- src/clj/grub/state.clj | 17 +++++++++-------- src/cljs/grub/state.cljs | 9 ++++++--- src/cljs/grub/websocket.cljs | 9 +++++---- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/project.clj b/project.clj index 28ea934..ff7d88a 100644 --- a/project.clj +++ b/project.clj @@ -17,7 +17,8 @@ [clj-webdriver "0.6.1" :exclusions [org.clojure/core.cache]] [om "0.7.0"] [sablono "0.2.17"] - [cljs-uuid "0.0.4"]] + [cljs-uuid "0.0.4"] + [net.polyc0l0r/hasch "0.2.3"]] :profiles {:uberjar {:aot :all}} :min-lein-version "2.1.2" :plugins [[lein-cljsbuild "1.0.3"] diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 0ed189b..a44eb94 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -1,7 +1,8 @@ (ns grub.state (:require [grub.sync :as sync] [grub.util :as util] - [clojure.core.async :as a :refer [! chan go]])) + [clojure.core.async :as a :refer [! chan go]] + [hasch.core :as hasch])) (def empty-state {:grubs {} @@ -21,13 +22,13 @@ (when (not= state* server-shadow*) (let [diff (sync/diff-states server-shadow* state*) msg {:diff diff - :hash (hash state*) - :shadow-hash (hash server-shadow*)}] + :hash (hasch/uuid state*) + :shadow-hash (hasch/uuid server-shadow*)}] (println "Sync because:") (println "Server = " state*) (println "Client = " server-shadow*) (println "Diff:" diff) - (println "Send" (hash server-shadow*) "->" (hash state*)) + (println "Send" (hasch/uuid server-shadow*) "->" (hasch/uuid state*)) (a/put! to-client msg) ;; TODO: only reset server shadow if send succeeds (reset! server-shadow state*))))) @@ -41,15 +42,15 @@ (if-let [{:keys [diff hash shadow-hash]} (" hash) - (println "Before shadow:" (clojure.core/hash @server-shadow) @server-shadow) - (if (= (clojure.core/hash @server-shadow) shadow-hash) + (println "Before shadow:" (hasch/uuid @server-shadow) @server-shadow) + (if (= (hasch/uuid @server-shadow) shadow-hash) (println "Before hash check: good") (println "Before hash check: FAIL")) (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) new-state (swap! state #(sync/patch-state % diff))] ;; TODO: check if hashes match - (println "After shadow:" (clojure.core/hash new-shadow) new-shadow) - (if (= (clojure.core/hash new-shadow) hash) + (println "After shadow:" (hasch/uuid new-shadow) new-shadow) + (if (= (hasch/uuid new-shadow) hash) (println "After hash check: good") (println "After hash check: FAIL")) (>! @to-db diff) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 99b76db..40aa4e6 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -1,6 +1,7 @@ (ns grub.state (:require [grub.sync :as sync] - [cljs.core.async :as a :refer [! chan]]) + [cljs.core.async :as a :refer [! chan]] + [hasch.core :as hasch]) (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go go-loop]])) @@ -13,12 +14,14 @@ (go-loop [] (when-let [{:keys [diff hash shadow-hash]} (" hash) - (if (= (cljs.core/hash @client-shadow) shadow-hash) + (logs "Before shadow:" (hasch/uuid @client-shadow) @client-shadow) + (if (= (hasch/uuid @client-shadow) shadow-hash) (log "Before hash check: good") (log "Before hash check: FAIL")) (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) new-state (swap! app-state #(sync/patch-state % diff))] - (if (= (cljs.core/hash new-shadow) hash) + (logs "After shadow:" (hasch/uuid @client-shadow) @client-shadow) + (if (= (hasch/uuid new-shadow) hash) (log "After hash check: good") (log "After hash check: FAIL")) (recur)))) diff --git a/src/cljs/grub/websocket.cljs b/src/cljs/grub/websocket.cljs index 788567b..1df0699 100644 --- a/src/cljs/grub/websocket.cljs +++ b/src/cljs/grub/websocket.cljs @@ -5,7 +5,8 @@ [cljs.reader] goog.net.WebSocket goog.events.EventHandler - goog.events.EventTarget) + goog.events.EventTarget + [hasch.core :as hasch]) (:require-macros [cljs.core.async.macros :refer [go go-loop]] [grub.macros :refer [log logs]])) @@ -18,13 +19,13 @@ client-shadow @state/client-shadow diff (sync/diff-states client-shadow app-state) msg {:diff diff - :hash (hash app-state) - :shadow-hash (hash client-shadow)}] + :hash (hasch/uuid app-state) + :shadow-hash (hasch/uuid client-shadow)}] (logs "Sync because:") (logs "Server = " client-shadow) (logs "Client = " app-state) (logs "Diff:" diff) - (logs "Send" (hash client-shadow) "->" (hash app-state)) + (logs "Send" (hasch/uuid client-shadow) "->" (hasch/uuid app-state)) ;; TODO: reset client shadow only if send succeeds (.send @websocket* msg) (reset! state/client-shadow app-state)))) From b7b094b2538b720b666bad83db6d6557dac3c7ef Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Tue, 19 Aug 2014 23:24:19 +0300 Subject: [PATCH 08/40] Refactor wip --- src/clj/grub/core.clj | 4 +-- src/clj/grub/state.clj | 58 +++++++++++++++++--------------- src/clj/grub/websocket.clj | 24 +++++++------- src/cljs/grub/core.cljs | 8 ++--- src/cljs/grub/state.cljs | 64 +++++++++++++++++++++++++----------- src/cljs/grub/websocket.cljs | 62 +++++++++++++++------------------- 6 files changed, 117 insertions(+), 103 deletions(-) diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index f4f1313..d892f09 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -47,8 +47,8 @@ (httpkit/with-channel request ws-channel (let [to-client (chan) from-client (chan)] - (ws/add-client! ws-channel to-client from-client) - (state/add-client! to-client from-client))))) + (ws/add-connected-client! ws-channel to-client from-client) + (state/sync-new-client! to-client from-client))))) (defroutes routes (GET "/" [] websocket-handler) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index a44eb94..1b4b2ef 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -1,19 +1,16 @@ (ns grub.state (:require [grub.sync :as sync] [grub.util :as util] + [grub.common-state :as cs] [clojure.core.async :as a :refer [! chan go]] [hasch.core :as hasch])) -(def empty-state - {:grubs {} - :recipes {}}) - -(def state (atom empty-state)) +(def state (atom cs/empty-state)) (def to-db (atom nil)) (def to-all (chan)) (def from-all (a/mult to-all)) -(defn get-initial-state [grubs recipes] +(defn initial-state [grubs recipes] {:grubs (util/map-by-key :id grubs) :recipes (util/map-by-key :id recipes)}) @@ -21,7 +18,8 @@ (let [server-shadow* @server-shadow] (when (not= state* server-shadow*) (let [diff (sync/diff-states server-shadow* state*) - msg {:diff diff + msg {:type :diff + :diff diff :hash (hasch/uuid state*) :shadow-hash (hasch/uuid server-shadow*)}] (println "Sync because:") @@ -33,30 +31,36 @@ ;; TODO: only reset server shadow if send succeeds (reset! server-shadow state*))))) -(defn add-client! [to from] +(defn sync-new-client! [to from] (let [client-id (java.util.UUID/randomUUID) - server-shadow (atom empty-state)] + server-shadow (atom cs/empty-state)] (add-watch state client-id (fn [k ref old new] (sync-remote-changes to new server-shadow))) (a/go-loop [] - (if-let [{:keys [diff hash shadow-hash]} (" hash) - (println "Before shadow:" (hasch/uuid @server-shadow) @server-shadow) - (if (= (hasch/uuid @server-shadow) shadow-hash) - (println "Before hash check: good") - (println "Before hash check: FAIL")) - (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) - new-state (swap! state #(sync/patch-state % diff))] - ;; TODO: check if hashes match - (println "After shadow:" (hasch/uuid new-shadow) new-shadow) - (if (= (hasch/uuid new-shadow) hash) - (println "After hash check: good") - (println "After hash check: FAIL")) - (>! @to-db diff) - (recur))) - (remove-watch state client-id))))) + (let [{:keys [type diff hash shadow-hash] :as msg} (" hash) + (println "Before shadow:" (hasch/uuid @server-shadow) @server-shadow) + (if (= (hasch/uuid @server-shadow) shadow-hash) + (println "Before hash check: good") + (println "Before hash check: FAIL")) + (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) + new-state (swap! state #(sync/patch-state % diff))] + ;; TODO: check if hashes match + (println "After shadow:" (hasch/uuid new-shadow) new-shadow) + (if (= (hasch/uuid new-shadow) hash) + (println "After hash check: good") + (println "After hash check: FAIL")) + (>! @to-db diff))) + :complete (let [new-state (reset! server-shadow @state)] + (a/put! to (cs/complete-sync-response new-state))) + (println "Invalid msg:" msg)) + (recur)) + (remove-watch state client-id)))))) (defn init [_to-db grubs recipes] - (reset! state (get-initial-state grubs recipes)) + (reset! state (initial-state grubs recipes)) (reset! to-db _to-db)) diff --git a/src/clj/grub/websocket.clj b/src/clj/grub/websocket.clj index fdc9538..57b4a0e 100644 --- a/src/clj/grub/websocket.clj +++ b/src/clj/grub/websocket.clj @@ -3,18 +3,16 @@ [org.httpkit.server :as httpkit] [clojure.core.async :as a :refer [! chan go]])) -(defn add-client! [ws-channel to from] +(defn disconnected [status ws-channel to from] + (println "Client disconnected:" (.toString ws-channel) "with status" status) + (a/close! to) + (a/close! from)) + +(defn add-connected-client! [ws-channel to from] (println "Client connected:" (.toString ws-channel)) - (httpkit/on-close ws-channel - (fn [status] - (println "Client disconnected:" (.toString ws-channel) - "with status" status) - (a/close! to) - (a/close! from))) + (a/go-loop [] (if-let [event (! chan]] [hasch.core :as hasch]) (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go go-loop]])) -(def empty-state {:grubs {} :recipes {}}) -(def app-state (atom empty-state)) -(def client-shadow (atom empty-state)) +(def app-state (atom cs/empty-state)) +(def client-shadow (atom cs/empty-state)) -(defn update-state-on-event! [in] - (let [out (chan)] - (go-loop [] - (when-let [{:keys [diff hash shadow-hash]} (" hash) - (logs "Before shadow:" (hasch/uuid @client-shadow) @client-shadow) - (if (= (hasch/uuid @client-shadow) shadow-hash) - (log "Before hash check: good") - (log "Before hash check: FAIL")) - (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) - new-state (swap! app-state #(sync/patch-state % diff))] - (logs "After shadow:" (hasch/uuid @client-shadow) @client-shadow) - (if (= (hasch/uuid new-shadow) hash) - (log "After hash check: good") - (log "After hash check: FAIL")) - (recur)))) - out)) +(defn sync-local-changes [to-remote state*] + (let [client-shadow* @client-shadow] + (when (not= state* client-shadow*) + (let [diff (sync/diff-states client-shadow* state*) + msg {:type :diff + :diff diff + :hash (hasch/uuid state*) + :shadow-hash (hasch/uuid client-shadow*)}] + (logs "Sync because:") + (logs "Server = " client-shadow*) + (logs "Client = " state*) + (logs "Diff:" diff) + (logs "Send" (hasch/uuid client-shadow*) "->" (hasch/uuid state*)) + ;; TODO: reset client shadow only if send succeeds + (a/put! to-remote msg) + (reset! client-shadow state*))))) + +(defn sync-state! [to from] + (go-loop [] + (when-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) + (logs "Before shadow:" (hasch/uuid @client-shadow) @client-shadow) + (if (= (hasch/uuid @client-shadow) shadow-hash) + (log "Before hash check: good") + (log "Before hash check: FAIL")) + (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) + new-state (swap! app-state #(sync/patch-state % diff))] + (logs "After shadow:" (hasch/uuid @client-shadow) @client-shadow) + (if (= (hasch/uuid new-shadow) hash) + (log "After hash check: good") + (log "After hash check: FAIL")))) + :complete (do (log "Received complete sync, reset state") + (logs msg) + (reset! client-shadow (:state msg)) + (reset! app-state (:state msg))) + (logs "Invalid msg:" msg)) + (recur))) + (add-watch app-state :app-state (fn [k ref old new] (sync-local-changes from new))) + (a/put! from cs/complete-sync-request)) diff --git a/src/cljs/grub/websocket.cljs b/src/cljs/grub/websocket.cljs index 1df0699..56455bd 100644 --- a/src/cljs/grub/websocket.cljs +++ b/src/cljs/grub/websocket.cljs @@ -10,44 +10,34 @@ (:require-macros [cljs.core.async.macros :refer [go go-loop]] [grub.macros :refer [log logs]])) -(def websocket* (atom nil)) +(def server-url (str "ws://" (.-host (.-location js/document)))) +(def pending-msg (atom nil)) -(defn sync-local-changes [] - (when (and (.isOpen @websocket*) - (not= @state/app-state @state/client-shadow)) - (let [app-state @state/app-state - client-shadow @state/client-shadow - diff (sync/diff-states client-shadow app-state) - msg {:diff diff - :hash (hasch/uuid app-state) - :shadow-hash (hasch/uuid client-shadow)}] - (logs "Sync because:") - (logs "Server = " client-shadow) - (logs "Client = " app-state) - (logs "Diff:" diff) - (logs "Send" (hasch/uuid client-shadow) "->" (hasch/uuid app-state)) - ;; TODO: reset client shadow only if send succeeds - (.send @websocket* msg) - (reset! state/client-shadow app-state)))) +(defn send-pending-msg [websocket] + (when (and (.isOpen websocket) + (not (nil? @pending-msg))) + (.send websocket @pending-msg) + (reset! pending-msg nil))) -(defn on-connected [event] +(defn on-connected [websocket event] (log "Connected:" event) - (sync-local-changes)) + (send-pending-msg websocket)) -(defn on-message-fn [out] - (fn [event] - (let [msg (cljs.reader/read-string (.-message event))] - (a/put! out msg)))) +(defn on-message [from event] + (let [msg (cljs.reader/read-string (.-message event))] + (a/put! from msg))) -(defn get-remote-chan [to-remote] - (let [server-url (str "ws://" (.-host (.-location js/document))) - handler (goog.events.EventHandler.) - remote-events (chan)] - (reset! websocket* (goog.net.WebSocket.)) - (.listen handler @websocket* goog.net.WebSocket.EventType.OPENED on-connected false) - (.listen handler @websocket* goog.net.WebSocket.EventType.MESSAGE (on-message-fn remote-events) false) - (.listen handler @websocket* goog.net.WebSocket.EventType.CLOSED #(log "Closed:" %) false) - (.listen handler @websocket* goog.net.WebSocket.EventType.ERROR #(log "Error:" %) false) - (add-watch state/app-state :app-state #(sync-local-changes)) - (.open @websocket* server-url) - remote-events)) +(defn connect-client! [to from] + (let [handler (goog.events.EventHandler.) + websocket (goog.net.WebSocket.) + listen (fn [type fun] (.listen handler websocket type fun false))] + (listen goog.net.WebSocket.EventType.OPENED (partial on-connected websocket)) + (listen goog.net.WebSocket.EventType.MESSAGE (partial on-message from)) + (listen goog.net.WebSocket.EventType.CLOSED #(log "Closed:" %)) + (listen goog.net.WebSocket.EventType.ERROR #(log "Error:" %)) + (go (loop [] + (when-let [msg ( Date: Sat, 30 Aug 2014 07:21:59 +0300 Subject: [PATCH 09/40] Factor out some client/server common state ops --- src/clj/grub/state.clj | 67 ++++++++++++--------------------- src/cljs/grub/state.cljs | 67 +++++++++++++-------------------- src/cljs/grub/websocket.cljs | 2 +- src/cljx/grub/common_state.cljx | 31 +++++++++++++++ 4 files changed, 83 insertions(+), 84 deletions(-) create mode 100644 src/cljx/grub/common_state.cljx diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 1b4b2ef..61ebe5a 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -14,52 +14,35 @@ {:grubs (util/map-by-key :id grubs) :recipes (util/map-by-key :id recipes)}) -(defn sync-remote-changes [to-client state* server-shadow] - (let [server-shadow* @server-shadow] - (when (not= state* server-shadow*) - (let [diff (sync/diff-states server-shadow* state*) - msg {:type :diff - :diff diff - :hash (hasch/uuid state*) - :shadow-hash (hasch/uuid server-shadow*)}] - (println "Sync because:") - (println "Server = " state*) - (println "Client = " server-shadow*) - (println "Diff:" diff) - (println "Send" (hasch/uuid server-shadow*) "->" (hasch/uuid state*)) - (a/put! to-client msg) - ;; TODO: only reset server shadow if send succeeds - (reset! server-shadow state*))))) - (defn sync-new-client! [to from] (let [client-id (java.util.UUID/randomUUID) server-shadow (atom cs/empty-state)] - (add-watch state client-id (fn [k ref old new] - (sync-remote-changes to new server-shadow))) + (add-watch state client-id (fn [_ _ _ new-state] + (when-let [msg (cs/diff-states new-state server-shadow)] + (a/put! to msg) + (reset! server-shadow new-state)))) (a/go-loop [] - (let [{:keys [type diff hash shadow-hash] :as msg} (" hash) - (println "Before shadow:" (hasch/uuid @server-shadow) @server-shadow) - (if (= (hasch/uuid @server-shadow) shadow-hash) - (println "Before hash check: good") - (println "Before hash check: FAIL")) - (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) - new-state (swap! state #(sync/patch-state % diff))] - ;; TODO: check if hashes match - (println "After shadow:" (hasch/uuid new-shadow) new-shadow) - (if (= (hasch/uuid new-shadow) hash) - (println "After hash check: good") - (println "After hash check: FAIL")) - (>! @to-db diff))) - :complete (let [new-state (reset! server-shadow @state)] - (a/put! to (cs/complete-sync-response new-state))) - (println "Invalid msg:" msg)) - (recur)) - (remove-watch state client-id)))))) + (when-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) + (println "Before shadow:" (hasch/uuid @server-shadow) @server-shadow) + (if (= (hasch/uuid @server-shadow) shadow-hash) + (println "Before hash check: good") + (println "Before hash check: FAIL")) + (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) + new-state (swap! state #(sync/patch-state % diff))] + ;; TODO: check if hashes match + (println "After shadow:" (hasch/uuid new-shadow) new-shadow) + (if (= (hasch/uuid new-shadow) hash) + (println "After hash check: good") + (println "After hash check: FAIL")) + (>! @to-db diff))) + :complete (let [new-state (reset! server-shadow @state)] + (a/put! to (cs/complete-sync-response new-state))) + (println "Invalid msg:" msg)) + (recur) + (remove-watch state client-id))))) (defn init [_to-db grubs recipes] (reset! state (initial-state grubs recipes)) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 1daad00..0deac20 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -7,46 +7,31 @@ [cljs.core.async.macros :refer [go go-loop]])) (def app-state (atom cs/empty-state)) -(def client-shadow (atom cs/empty-state)) - -(defn sync-local-changes [to-remote state*] - (let [client-shadow* @client-shadow] - (when (not= state* client-shadow*) - (let [diff (sync/diff-states client-shadow* state*) - msg {:type :diff - :diff diff - :hash (hasch/uuid state*) - :shadow-hash (hasch/uuid client-shadow*)}] - (logs "Sync because:") - (logs "Server = " client-shadow*) - (logs "Client = " state*) - (logs "Diff:" diff) - (logs "Send" (hasch/uuid client-shadow*) "->" (hasch/uuid state*)) - ;; TODO: reset client shadow only if send succeeds - (a/put! to-remote msg) - (reset! client-shadow state*))))) (defn sync-state! [to from] - (go-loop [] - (when-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) - (logs "Before shadow:" (hasch/uuid @client-shadow) @client-shadow) - (if (= (hasch/uuid @client-shadow) shadow-hash) - (log "Before hash check: good") - (log "Before hash check: FAIL")) - (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) - new-state (swap! app-state #(sync/patch-state % diff))] - (logs "After shadow:" (hasch/uuid @client-shadow) @client-shadow) - (if (= (hasch/uuid new-shadow) hash) - (log "After hash check: good") - (log "After hash check: FAIL")))) - :complete (do (log "Received complete sync, reset state") - (logs msg) - (reset! client-shadow (:state msg)) - (reset! app-state (:state msg))) - (logs "Invalid msg:" msg)) - (recur))) - (add-watch app-state :app-state (fn [k ref old new] (sync-local-changes from new))) - (a/put! from cs/complete-sync-request)) + (let [client-shadow (atom cs/empty-state)] + (add-watch app-state :app-state (fn [_ _ _ new] + (when-let [msg (cs/diff-states new @client-shadow)] + (a/put! from msg) + ;; TODO: reset shadow only if send succeeds + (reset! client-shadow new)))) + (go-loop [] + (when-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) + ;(logs "Before shadow:" (hasch/uuid @client-shadow) @client-shadow) + (if (= (hasch/uuid @client-shadow) shadow-hash) + (log "Before hash check: good") + (log "Before hash check: FAIL")) + (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) + new-state (swap! app-state #(sync/patch-state % diff))] + ;(logs "After shadow:" (hasch/uuid @client-shadow) @client-shadow) + (if (= (hasch/uuid new-shadow) hash) + (log "After hash check: good") + (log "After hash check: FAIL")))) + :complete (do (reset! client-shadow (:state msg)) + (reset! app-state (:state msg))) + (logs "Invalid msg:" msg)) + (recur))) + (a/put! from cs/complete-sync-request))) diff --git a/src/cljs/grub/websocket.cljs b/src/cljs/grub/websocket.cljs index 56455bd..1756418 100644 --- a/src/cljs/grub/websocket.cljs +++ b/src/cljs/grub/websocket.cljs @@ -16,7 +16,7 @@ (defn send-pending-msg [websocket] (when (and (.isOpen websocket) (not (nil? @pending-msg))) - (.send websocket @pending-msg) + (.send websocket (pr-str @pending-msg)) (reset! pending-msg nil))) (defn on-connected [websocket event] diff --git a/src/cljx/grub/common_state.cljx b/src/cljx/grub/common_state.cljx new file mode 100644 index 0000000..9d896a6 --- /dev/null +++ b/src/cljx/grub/common_state.cljx @@ -0,0 +1,31 @@ +(ns grub.common-state + (:require [grub.sync :as sync] + [hasch.core :as hasch])) + +(def empty-state {:grubs {} :recipes {}}) + +(def complete-sync-request {:type :complete}) +(defn complete-sync-response [state] + {:type :complete + :state state}) + +(defn diff-msg [diff hash shadow-hash] + {:type :diff + :diff diff + :hash hash + :shadow-hash shadow-hash}) + +(defn diff-states [state shadow*] + (let [shadow shadow*] + (when (not= state shadow) + (let [diff (sync/diff-states shadow state) + hash (hasch/uuid state) + shadow-hash (hasch/uuid shadow) + msg (diff-msg diff hash shadow-hash)] + msg + ;(logs "Sync because:") + ;(logs "Local = " state) + ;(logs "Remote = " shadow) + ;(logs "Diff:" diff) + ;(logs "Send" shadow-hash "->" hash) + )))) From 763e6f2fc80868678684127de46d2a64f36dff35 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 30 Aug 2014 07:35:43 +0300 Subject: [PATCH 10/40] Sync complete state on checksum failure --- src/clj/grub/state.clj | 36 +++++++++++++++--------------------- src/cljs/grub/state.cljs | 30 ++++++++++++------------------ 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 61ebe5a..5621299 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -18,30 +18,24 @@ (let [client-id (java.util.UUID/randomUUID) server-shadow (atom cs/empty-state)] (add-watch state client-id (fn [_ _ _ new-state] - (when-let [msg (cs/diff-states new-state server-shadow)] + (when-let [msg (cs/diff-states new-state @server-shadow)] (a/put! to msg) (reset! server-shadow new-state)))) (a/go-loop [] - (when-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) - (println "Before shadow:" (hasch/uuid @server-shadow) @server-shadow) - (if (= (hasch/uuid @server-shadow) shadow-hash) - (println "Before hash check: good") - (println "Before hash check: FAIL")) - (let [new-shadow (swap! server-shadow #(sync/patch-state % diff)) - new-state (swap! state #(sync/patch-state % diff))] - ;; TODO: check if hashes match - (println "After shadow:" (hasch/uuid new-shadow) new-shadow) - (if (= (hasch/uuid new-shadow) hash) - (println "After hash check: good") - (println "After hash check: FAIL")) - (>! @to-db diff))) - :complete (let [new-state (reset! server-shadow @state)] - (a/put! to (cs/complete-sync-response new-state))) - (println "Invalid msg:" msg)) - (recur) + (if-let [{:keys [type diff hash shadow-hash] :as msg} (! @to-db diff)) + (do (println "Hash check failed --> complete sync") + (let [sync-state @state] + (reset! server-shadow sync-state) + (a/put! to (cs/complete-sync-response sync-state)))))) + :complete (let [new-state (reset! server-shadow @state)] + (a/put! to (cs/complete-sync-response new-state))) + (println "Invalid msg:" msg)) + (recur)) (remove-watch state client-id))))) (defn init [_to-db grubs recipes] diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 0deac20..083c2b4 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -16,22 +16,16 @@ ;; TODO: reset shadow only if send succeeds (reset! client-shadow new)))) (go-loop [] - (when-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) - ;(logs "Before shadow:" (hasch/uuid @client-shadow) @client-shadow) - (if (= (hasch/uuid @client-shadow) shadow-hash) - (log "Before hash check: good") - (log "Before hash check: FAIL")) - (let [new-shadow (swap! client-shadow #(sync/patch-state % diff)) - new-state (swap! app-state #(sync/patch-state % diff))] - ;(logs "After shadow:" (hasch/uuid @client-shadow) @client-shadow) - (if (= (hasch/uuid new-shadow) hash) - (log "After hash check: good") - (log "After hash check: FAIL")))) - :complete (do (reset! client-shadow (:state msg)) - (reset! app-state (:state msg))) - (logs "Invalid msg:" msg)) - (recur))) + (if-let [{:keys [type diff hash shadow-hash] :as msg} ( complete sync") + (a/put! from cs/complete-sync-request)))) + :complete (do (reset! client-shadow (:state msg)) + (reset! app-state (:state msg))) + (logs "Invalid msg:" msg)) + (recur)) + (remove-watch app-state :app-state))) (a/put! from cs/complete-sync-request))) From a572b9e1ebdcb9b31a1486f5e4b8c1e91d8579d6 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 30 Aug 2014 11:47:09 +0300 Subject: [PATCH 11/40] Reset state from history - fails if server changes are made while client is disconnected --- src/clj/grub/state.clj | 68 +++++++++++++++++++++++++++------ src/cljs/grub/core.cljs | 9 +++-- src/cljs/grub/state.cljs | 12 +++--- src/cljs/grub/websocket.cljs | 3 ++ src/cljx/grub/common_state.cljx | 27 +++++++------ 5 files changed, 85 insertions(+), 34 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 5621299..d210363 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -10,6 +10,24 @@ (def to-all (chan)) (def from-all (a/mult to-all)) +(def state-history (atom [])) + +(defn save-history-state [history new-state] + (when-not (= (last history) new-state) + (println "Adding state to history: " (hasch/uuid new-state)) + (println "History size:" (inc (count history))) + (conj history new-state))) + +(defn get-history-state [hash] + (println "Look for history state:" hash) + (println "History:") + (doseq [s @state-history] + (println (hasch/uuid s))) + (first (filter #(= (hasch/uuid %) hash) @state-history))) + +(add-watch state :history (fn [_ _ _ new-state] + (swap! state-history save-history-state new-state))) + (defn initial-state [grubs recipes] {:grubs (util/map-by-key :id grubs) :recipes (util/map-by-key :id recipes)}) @@ -17,21 +35,49 @@ (defn sync-new-client! [to from] (let [client-id (java.util.UUID/randomUUID) server-shadow (atom cs/empty-state)] - (add-watch state client-id (fn [_ _ _ new-state] - (when-let [msg (cs/diff-states new-state @server-shadow)] + (add-watch state client-id (fn [_ _ _ current-state] + (when-let [msg (cs/diff-states @server-shadow current-state)] (a/put! to msg) - (reset! server-shadow new-state)))) + ;; TODO: reset only if send succeeds? + (reset! server-shadow current-state)))) (a/go-loop [] (if-let [{:keys [type diff hash shadow-hash] :as msg} (! @to-db diff)) - (do (println "Hash check failed --> complete sync") - (let [sync-state @state] - (reset! server-shadow sync-state) - (a/put! to (cs/complete-sync-response sync-state)))))) + :diff + (if (= (hasch/uuid @server-shadow) shadow-hash) + ;; we have what they thought we had + ;; apply changes normally + (let [new-shadow (swap! server-shadow sync/patch-state diff)] + (println "Hash matched state, apply changes") + (if (= (hasch/uuid new-shadow) hash) + (let [new-state (swap! state sync/patch-state diff)] + (>! @to-db diff)) + (do (println "Applying diff failed --> full sync") + (let [sync-state @state] + (reset! server-shadow sync-state) + (a/put! to (cs/complete-sync-response sync-state)))))) + ;; we have something different than they thought + ;; check history + (if-let [history-state (get-history-state shadow-hash)] + ;; Found what they thought in history, + ;; reset client state to this + ;; and continue as normal + (do + (println "Hash check failed --> Reset from history") + (reset! server-shadow history-state) + (let [new-shadow (swap! server-shadow sync/patch-state diff)] + (if (= (hasch/uuid new-shadow) hash) + (let [new-state (swap! state sync/patch-state diff)] + (>! @to-db diff)) + (do (println "Applying diff failed --> full sync") + (let [sync-state @state] + (reset! server-shadow sync-state) + (a/put! to (cs/complete-sync-response sync-state))))))) + ;; No history found, do complete sync + (do (println "Hash check failed, not in history --> full sync") + (let [sync-state @state] + (reset! server-shadow sync-state) + (a/put! to (cs/complete-sync-response sync-state)))))) :complete (let [new-state (reset! server-shadow @state)] (a/put! to (cs/complete-sync-response new-state))) (println "Invalid msg:" msg)) diff --git a/src/cljs/grub/core.cljs b/src/cljs/grub/core.cljs index 1064bc4..329485d 100644 --- a/src/cljs/grub/core.cljs +++ b/src/cljs/grub/core.cljs @@ -5,11 +5,14 @@ [cljs.core.async :as a :refer [! chan]]) (:require-macros [grub.macros :refer [log logs]])) -(defn init-app [] - (view/render-app state/app-state) +(defn connect-to-server [reset?] (let [to-remote (chan) from-remote (chan)] (ws/connect-client! to-remote from-remote) - (state/sync-state! from-remote to-remote))) + (state/sync-state! from-remote to-remote reset?))) + +(defn init-app [] + (view/render-app state/app-state) + (connect-to-server true)) (init-app) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 083c2b4..85b2825 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -8,13 +8,13 @@ (def app-state (atom cs/empty-state)) -(defn sync-state! [to from] +(defn sync-state! [to from reset?] (let [client-shadow (atom cs/empty-state)] - (add-watch app-state :app-state (fn [_ _ _ new] - (when-let [msg (cs/diff-states new @client-shadow)] + (add-watch app-state :app-state (fn [_ _ _ current-state] + (when-let [msg (cs/diff-states @client-shadow current-state)] (a/put! from msg) - ;; TODO: reset shadow only if send succeeds - (reset! client-shadow new)))) + ;; TODO: reset only if send succeeds + (reset! client-shadow current-state)))) (go-loop [] (if-let [{:keys [type diff hash shadow-hash] :as msg} (" hash) - )))) +(defn diff-states [shadow state] + (when (not= state shadow) + (let [diff (sync/diff-states shadow state) + hash (hasch/uuid state) + shadow-hash (hasch/uuid shadow) + msg (diff-msg diff hash shadow-hash)] + msg + ;(logs "Sync because:") + ;(logs "Local = " state) + ;(logs "Remote = " shadow) + ;(logs "Diff:" diff) + ;(logs "Send" shadow-hash "->" hash) + ))) From f087309c0f08697d36e7b6f01d76f554af9c034b Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 30 Aug 2014 15:40:17 +0300 Subject: [PATCH 12/40] Possibly fully working differential sync --- src/clj/grub/state.clj | 74 ++++++++++++++++++--------------- src/cljs/grub/core.cljs | 2 +- src/cljs/grub/state.cljs | 52 ++++++++++++++++------- src/cljx/grub/common_state.cljx | 17 ++++---- 4 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index d210363..d8ee3d3 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -13,9 +13,11 @@ (def state-history (atom [])) (defn save-history-state [history new-state] - (when-not (= (last history) new-state) + (when-not (= (hasch/uuid (last history)) (hasch/uuid new-state)) (println "Adding state to history: " (hasch/uuid new-state)) - (println "History size:" (inc (count history))) + (println "History:") + (doseq [s (conj history new-state)] + (println (hasch/uuid s))) (conj history new-state))) (defn get-history-state [hash] @@ -34,53 +36,57 @@ (defn sync-new-client! [to from] (let [client-id (java.util.UUID/randomUUID) - server-shadow (atom cs/empty-state)] + client-state (atom cs/empty-state) + log (fn [& args] + (apply println client-id args))] (add-watch state client-id (fn [_ _ _ current-state] - (when-let [msg (cs/diff-states @server-shadow current-state)] + (when-let [msg (cs/diff-states @client-state current-state)] (a/put! to msg) + ;; send ACK even if nothing changes ;; TODO: reset only if send succeeds? - (reset! server-shadow current-state)))) + (reset! client-state current-state)))) (a/go-loop [] (if-let [{:keys [type diff hash shadow-hash] :as msg} (! @to-db diff)) - (do (println "Applying diff failed --> full sync") + (do (log "Applying diff failed --> full sync") (let [sync-state @state] - (reset! server-shadow sync-state) + (reset! client-state sync-state) (a/put! to (cs/complete-sync-response sync-state)))))) - ;; we have something different than they thought - ;; check history - (if-let [history-state (get-history-state shadow-hash)] - ;; Found what they thought in history, - ;; reset client state to this - ;; and continue as normal - (do - (println "Hash check failed --> Reset from history") - (reset! server-shadow history-state) - (let [new-shadow (swap! server-shadow sync/patch-state diff)] - (if (= (hasch/uuid new-shadow) hash) - (let [new-state (swap! state sync/patch-state diff)] - (>! @to-db diff)) - (do (println "Applying diff failed --> full sync") - (let [sync-state @state] - (reset! server-shadow sync-state) - (a/put! to (cs/complete-sync-response sync-state))))))) - ;; No history found, do complete sync - (do (println "Hash check failed, not in history --> full sync") - (let [sync-state @state] - (reset! server-shadow sync-state) - (a/put! to (cs/complete-sync-response sync-state)))))) - :complete (let [new-state (reset! server-shadow @state)] + ;; We have something different than they thought + ;; Check history + (do + (log "Hash check failed --> Reset from history") + (if-let [history-state (get-history-state shadow-hash)] + ;; Found what they thought we had in history, + ;; reset client state to this and continue as normal + (do + (reset! client-state history-state) + (let [new-shadow (swap! client-state sync/patch-state diff)] + (if (= (hasch/uuid new-shadow) hash) + (let [new-state (swap! state sync/patch-state diff)] + (>! @to-db diff)) + (do (log "Applying diff failed --> full sync") + (let [sync-state @state] + (reset! client-state sync-state) + (a/put! to (cs/complete-sync-response sync-state))))))) + ;; Not found in history, do complete sync + (do (log "Hash check failed, not in history --> full sync") + (let [sync-state @state] + (reset! client-state sync-state) + (a/put! to (cs/complete-sync-response sync-state))))))) + :complete (let [new-state (reset! client-state @state)] + (log "full sync") (a/put! to (cs/complete-sync-response new-state))) - (println "Invalid msg:" msg)) + (log "Invalid msg:" msg)) (recur)) (remove-watch state client-id))))) diff --git a/src/cljs/grub/core.cljs b/src/cljs/grub/core.cljs index 329485d..f61b084 100644 --- a/src/cljs/grub/core.cljs +++ b/src/cljs/grub/core.cljs @@ -12,7 +12,7 @@ (state/sync-state! from-remote to-remote reset?))) (defn init-app [] - (view/render-app state/app-state) + (view/render-app state/state) (connect-to-server true)) (init-app) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 85b2825..a87db33 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -6,26 +6,48 @@ (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go go-loop]])) -(def app-state (atom cs/empty-state)) +(def state (atom cs/empty-state)) + +(def unacked-history (atom {})) + +(defn get-unacked-state [hash] + (logs "Look for history state:" hash) + (get @unacked-history hash)) (defn sync-state! [to from reset?] - (let [client-shadow (atom cs/empty-state)] - (add-watch app-state :app-state (fn [_ _ _ current-state] - (when-let [msg (cs/diff-states @client-shadow current-state)] - (a/put! from msg) - ;; TODO: reset only if send succeeds - (reset! client-shadow current-state)))) + (let [server-state (atom cs/empty-state)] + (add-watch state :state (fn [_ _ _ current-state] + (when-not (= @server-state current-state) + (let [msg (cs/diff-states @server-state current-state)] + (when-not (get @unacked-history (hasch/uuid current-state)) + (logs "state change! msg: " msg) + (swap! unacked-history assoc (hasch/uuid current-state) current-state) + (logs "History:" (keys @unacked-history)) + (a/put! from msg)) + )))) (go-loop [] (if-let [{:keys [type diff hash shadow-hash] :as msg} ( complete sync") - (a/put! from cs/complete-sync-request)))) - :complete (do (reset! client-shadow (:state msg)) - (reset! app-state (:state msg))) + :diff (do + (logs "Received diff:" msg) + (when (not (= (hasch/uuid @server-state) shadow-hash)) + (reset! server-state (get-unacked-state shadow-hash))) + (reset! unacked-history {}) + (let [ ;; what they now think we have (after updating) + new-shadow (swap! server-state #(sync/patch-state % diff))] + ;; should match hash + (if (= (hasch/uuid new-shadow) hash) + ;; apply same changes locally + ;; if there are differences, they will be sent back + (swap! state sync/patch-state diff) + (do (log "Hash check failed --> complete sync") + (a/put! from cs/complete-sync-request))))) + :complete (do + (logs "Complete sync:" (hasch/uuid (:state msg))) + (reset! unacked-history {}) + (reset! server-state (:state msg)) + (reset! state (:state msg))) (logs "Invalid msg:" msg)) (recur)) - (remove-watch app-state :app-state))) + (remove-watch state :state))) (when reset? (a/put! from cs/complete-sync-request)))) diff --git a/src/cljx/grub/common_state.cljx b/src/cljx/grub/common_state.cljx index 396459d..8a174af 100644 --- a/src/cljx/grub/common_state.cljx +++ b/src/cljx/grub/common_state.cljx @@ -16,15 +16,18 @@ :shadow-hash shadow-hash}) (defn diff-states [shadow state] - (when (not= state shadow) - (let [diff (sync/diff-states shadow state) - hash (hasch/uuid state) - shadow-hash (hasch/uuid shadow) - msg (diff-msg diff hash shadow-hash)] - msg + (let [diff (sync/diff-states shadow state) + ;; what we now have + hash (hasch/uuid state) + + ;; what we had/what you used to have + ;; should match what they think we have + shadow-hash (hasch/uuid shadow) + msg (diff-msg diff hash shadow-hash)] + msg ;(logs "Sync because:") ;(logs "Local = " state) ;(logs "Remote = " shadow) ;(logs "Diff:" diff) ;(logs "Send" shadow-hash "->" hash) - ))) + )) From 4320401a4e989d84a9ba325d04d89738acd7e980 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 30 Aug 2014 16:43:58 +0300 Subject: [PATCH 13/40] Better - uses tx-listen --- src/clj/grub/state.clj | 16 ++----- src/cljs/grub/core.cljs | 9 ++-- src/cljs/grub/state.cljs | 80 ++++++++++++++++----------------- src/cljs/grub/view/app.cljs | 6 ++- src/cljx/grub/common_state.cljx | 12 +---- 5 files changed, 52 insertions(+), 71 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index d8ee3d3..ed50836 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -14,17 +14,9 @@ (defn save-history-state [history new-state] (when-not (= (hasch/uuid (last history)) (hasch/uuid new-state)) - (println "Adding state to history: " (hasch/uuid new-state)) - (println "History:") - (doseq [s (conj history new-state)] - (println (hasch/uuid s))) (conj history new-state))) (defn get-history-state [hash] - (println "Look for history state:" hash) - (println "History:") - (doseq [s @state-history] - (println (hasch/uuid s))) (first (filter #(= (hasch/uuid %) hash) @state-history))) (add-watch state :history (fn [_ _ _ new-state] @@ -40,18 +32,16 @@ log (fn [& args] (apply println client-id args))] (add-watch state client-id (fn [_ _ _ current-state] - (when-let [msg (cs/diff-states @client-state current-state)] + (let [msg (cs/diff-states @client-state current-state)] (a/put! to msg) - ;; send ACK even if nothing changes - ;; TODO: reset only if send succeeds? (reset! client-state current-state)))) (a/go-loop [] (if-let [{:keys [type diff hash shadow-hash] :as msg} (! chan]]) (:require-macros [grub.macros :refer [log logs]])) -(defn connect-to-server [reset?] +(defn connect-to-server [reset? state-changes] (let [to-remote (chan) from-remote (chan)] (ws/connect-client! to-remote from-remote) - (state/sync-state! from-remote to-remote reset?))) + (state/sync-state! from-remote to-remote reset? state-changes))) (defn init-app [] - (view/render-app state/state) - (connect-to-server true)) + (let [state-changes (chan)]d + (view/render-app state/state state-changes) + (connect-to-server true state-changes))) (init-app) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index a87db33..7e8410e 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -7,47 +7,45 @@ [cljs.core.async.macros :refer [go go-loop]])) (def state (atom cs/empty-state)) +(def server-state (atom cs/empty-state)) -(def unacked-history (atom {})) +(def unacked-states (atom {})) -(defn get-unacked-state [hash] - (logs "Look for history state:" hash) - (get @unacked-history hash)) +(defn get-server-state [hash] + (if (= (hasch/uuid @server-state) hash) + @server-state + (get @unacked-states hash))) -(defn sync-state! [to from reset?] - (let [server-state (atom cs/empty-state)] - (add-watch state :state (fn [_ _ _ current-state] - (when-not (= @server-state current-state) - (let [msg (cs/diff-states @server-state current-state)] - (when-not (get @unacked-history (hasch/uuid current-state)) - (logs "state change! msg: " msg) - (swap! unacked-history assoc (hasch/uuid current-state) current-state) - (logs "History:" (keys @unacked-history)) - (a/put! from msg)) - )))) - (go-loop [] - (if-let [{:keys [type diff hash shadow-hash] :as msg} ( complete sync") - (a/put! from cs/complete-sync-request))))) - :complete (do - (logs "Complete sync:" (hasch/uuid (:state msg))) - (reset! unacked-history {}) - (reset! server-state (:state msg)) - (reset! state (:state msg))) - (logs "Invalid msg:" msg)) - (recur)) - (remove-watch state :state))) - (when reset? (a/put! from cs/complete-sync-request)))) +(defn sync-state! [to from reset? state-changes] + (go-loop [] + (when-let [current-state ( complete sync") + (a/put! from cs/complete-sync-request))))) + (do (log "Could not find server state locally --> complete sync") + (a/put! from cs/complete-sync-request))) + :complete (do + (logs "Complete sync") + (reset! unacked-states {}) + (reset! server-state (:state msg)) + (reset! state (:state msg))) + (logs "Invalid msg:" msg)) + (recur)) + (remove-watch state :state))) + (if reset? + (a/put! from cs/complete-sync-request) + (a/put! from (cs/diff-states @server-state @state)))) diff --git a/src/cljs/grub/view/app.cljs b/src/cljs/grub/view/app.cljs index 99db695..3b49fc5 100644 --- a/src/cljs/grub/view/app.cljs +++ b/src/cljs/grub/view/app.cljs @@ -25,7 +25,7 @@ (dom/on-document-mousedown #(put! >events {:type :body-mousedown :event %})) (dom/on-window-scroll #(put! >events {:type :body-scroll :event %})))))) -(defn render-app [state] +(defn render-app [state state-changes] (let [>events (chan) events :type) add-grubs-ch (chan)] @@ -34,4 +34,6 @@ {:target (.getElementById js/document "container") :shared {:>events >events :" hash) - )) + msg)) From 49aa9c784d987792c2f4c28820539cb270dbe480 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Wed, 3 Sep 2014 20:18:04 +0300 Subject: [PATCH 14/40] Minor refactor --- src/cljs/grub/core.cljs | 3 +-- src/cljs/grub/view/app.cljs | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cljs/grub/core.cljs b/src/cljs/grub/core.cljs index 1bd1e76..7850910 100644 --- a/src/cljs/grub/core.cljs +++ b/src/cljs/grub/core.cljs @@ -12,8 +12,7 @@ (state/sync-state! from-remote to-remote reset? state-changes))) (defn init-app [] - (let [state-changes (chan)]d - (view/render-app state/state state-changes) + (let [state-changes (view/render-app state/state state-changes)] (connect-to-server true state-changes))) (init-app) diff --git a/src/cljs/grub/view/app.cljs b/src/cljs/grub/view/app.cljs index 3b49fc5..0fe469b 100644 --- a/src/cljs/grub/view/app.cljs +++ b/src/cljs/grub/view/app.cljs @@ -25,10 +25,11 @@ (dom/on-document-mousedown #(put! >events {:type :body-mousedown :event %})) (dom/on-window-scroll #(put! >events {:type :body-scroll :event %})))))) -(defn render-app [state state-changes] +(defn render-app [state] (let [>events (chan) events :type) - add-grubs-ch (chan)] + add-grubs-ch (chan) + state-changes (chan)] (om/root app-view state {:target (.getElementById js/document "container") @@ -36,4 +37,5 @@ : Date: Sun, 7 Sep 2014 09:29:06 +0300 Subject: [PATCH 15/40] Try breaking apart sync algorithm --- src/cljs/grub/core.cljs | 2 +- src/cljs/grub/state.cljs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cljs/grub/core.cljs b/src/cljs/grub/core.cljs index 7850910..14489d2 100644 --- a/src/cljs/grub/core.cljs +++ b/src/cljs/grub/core.cljs @@ -12,7 +12,7 @@ (state/sync-state! from-remote to-remote reset? state-changes))) (defn init-app [] - (let [state-changes (view/render-app state/state state-changes)] + (let [state-changes (view/render-app state/state)] (connect-to-server true state-changes))) (init-app) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 7e8410e..34f5c6c 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -16,14 +16,16 @@ @server-state (get @unacked-states hash))) -(defn sync-state! [to from reset? state-changes] +(defn send-state-changes-to-server! [state-changes from] (go-loop [] (when-let [current-state ( Date: Sun, 14 Sep 2014 21:03:51 +0300 Subject: [PATCH 16/40] Make sync tests more explicit --- src/test/grub/test/unit/sync.clj | 81 +++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj index c11197e..581c2f0 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/sync.clj @@ -2,7 +2,74 @@ (:require [grub.sync :as sync] [clojure.test :refer :all])) -(def server-state + +(def empty-diff {:grubs {:deleted #{} :updated nil} + :recipes {:deleted #{} :updated nil}}) + +(deftest diff-empty-states + (let [empty-state {:grubs {} :recipes {}}] + (is (= empty-diff + (sync/diff-states empty-state empty-state))))) + +(deftest diff-equal-states + (is (= empty-diff + (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + {:grubs {"id" {:text "asdf" :completed false}} :recipes {}})))) + +(deftest diff-added-grub + (is (= {:grubs {:deleted #{} + :updated {"id" {:completed false, :text "asdf"}}} + :recipes {:deleted #{} :updated nil}} + (sync/diff-states {:grubs {} :recipes {}} + {:grubs {"id" {:text "asdf" :completed false}} :recipes {}})))) + +(deftest diff-deleted-grub + (is (= {:grubs {:deleted #{"id"} + :updated nil} + :recipes {:deleted #{} :updated nil}} + (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + {:grubs {} :recipes {}})))) + +(deftest diff-edited-grub + (is (= {:grubs {:deleted #{} + :updated {"id" {:text "asdf2"}}} + :recipes {:deleted #{} :updated nil}} + (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + {:grubs {"id" {:text "asdf2" :completed false}} :recipes {}})))) + +(deftest diff-completed-grub + (is (= {:grubs {:deleted #{} + :updated {"id" {:completed true}}} + :recipes {:deleted #{} :updated nil}} + (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + {:grubs {"id" {:text "asdf" :completed true}} :recipes {}})))) + +(deftest diff-added-recipe + (is (= {:grubs {:deleted #{} + :updated nil} + :recipes {:deleted #{} :updated {"id" {:name "Blue Cheese Soup" + :grubs "Some grubs"}}}} + (sync/diff-states {:grubs {} :recipes {}} + {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" + :grubs "Some grubs"}}})))) + +(deftest diff-edited-recipe + (is (= {:grubs {:deleted #{} + :updated nil} + :recipes {:deleted #{} :updated {"id" {:name "Bleu Cheese Soup" }}}} + (sync/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" + :grubs "Some grubs"}}} + {:grubs {} :recipes {"id" {:name "Bleu Cheese Soup" + :grubs "Some grubs"}}})))) + +(deftest diff-deleted-recipe + (is (= {:grubs {:deleted #{} :updated nil} + :recipes {:deleted #{"id"} :updated nil}} + (sync/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" + :grubs "Some grubs"}}} + {:grubs {} :recipes {}})))) + +(def before-state {:grubs {"grub-same" {:completed false :text "3 garlic cloves"} @@ -20,7 +87,7 @@ "recipe-deleted" {:grubs "8 slices rye bread\n400 g chicken breast\nBBQ sauce\nketchup\nmustard\nbutter\n1 package rocket\n4 tomatoes\n2 red onions\n1 bottle Coca Cola" :name "Chickenburgers"}}}) -(def client-state +(def after-state {:grubs {"grub-same" {:completed false, :text "3 garlic cloves"} @@ -57,10 +124,10 @@ "grub-added" {:completed false :text "Toothpaste"}}}}) -(deftest diffing - (is (= expected-diff (sync/diff-states server-state client-state)))) +(deftest diff-many-changes + (is (= expected-diff (sync/diff-states before-state after-state)))) -(deftest patching +(deftest patch-returns-original-state (is - (let [diff (sync/diff-states server-state client-state)] - (= client-state (sync/patch-state server-state diff))))) + (let [diff (sync/diff-states before-state after-state)] + (= after-state (sync/patch-state before-state diff))))) From 5a66361746a6ee4c29a5a5eee72d22f9820d8575 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sun, 14 Sep 2014 21:30:19 +0300 Subject: [PATCH 17/40] Rename sync -> diff --- src/clj/grub/state.clj | 10 +++---- src/cljs/grub/state.cljs | 6 ++-- src/cljs/grub/websocket.cljs | 4 +-- src/cljx/grub/common_state.cljx | 4 +-- src/cljx/grub/{sync.cljx => diff.cljx} | 2 +- .../grub/test/unit/{sync.clj => diff.clj} | 28 +++++++++---------- 6 files changed, 26 insertions(+), 28 deletions(-) rename src/cljx/grub/{sync.cljx => diff.cljx} (98%) rename src/test/grub/test/unit/{sync.clj => diff.clj} (88%) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index ed50836..3b19142 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -1,5 +1,5 @@ (ns grub.state - (:require [grub.sync :as sync] + (:require [grub.diff :as diff] [grub.util :as util] [grub.common-state :as cs] [clojure.core.async :as a :refer [! chan go]] @@ -42,10 +42,10 @@ (if (= (hasch/uuid @client-state) shadow-hash) ;; We have what they thought we had ;; Apply changes normally - (let [new-shadow (swap! client-state sync/patch-state diff)] + (let [new-shadow (swap! client-state diff/patch-state diff)] (log "Hash matched state, apply changes") (if (= (hasch/uuid new-shadow) hash) - (let [new-state (swap! state sync/patch-state diff)] + (let [new-state (swap! state diff/patch-state diff)] (>! @to-db diff)) (do (log "Applying diff failed --> full sync") (let [sync-state @state] @@ -60,9 +60,9 @@ ;; reset client state to this and continue as normal (do (reset! client-state history-state) - (let [new-shadow (swap! client-state sync/patch-state diff)] + (let [new-shadow (swap! client-state diff/patch-state diff)] (if (= (hasch/uuid new-shadow) hash) - (let [new-state (swap! state sync/patch-state diff)] + (let [new-state (swap! state diff/patch-state diff)] (>! @to-db diff)) (do (log "Applying diff failed --> full sync") (let [sync-state @state] diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 34f5c6c..3c451c5 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -1,5 +1,5 @@ (ns grub.state - (:require [grub.sync :as sync] + (:require [grub.diff :as diff] [grub.common-state :as cs] [cljs.core.async :as a :refer [! chan]] [hasch.core :as hasch]) @@ -33,9 +33,9 @@ (if-let [acked-server-state (get-server-state shadow-hash)] (do (reset! server-state acked-server-state) (reset! unacked-states {}) - (let [new-server (swap! server-state #(sync/patch-state % diff))] + (let [new-server (swap! server-state #(diff/patch-state % diff))] (if (= (hasch/uuid new-server) hash) - (swap! state sync/patch-state diff) + (swap! state diff/patch-state diff) (do (log "State update failure --> complete sync") (a/put! from cs/complete-sync-request))))) (do (log "Could not find server state locally --> complete sync") diff --git a/src/cljs/grub/websocket.cljs b/src/cljs/grub/websocket.cljs index f64bd8a..1fb0968 100644 --- a/src/cljs/grub/websocket.cljs +++ b/src/cljs/grub/websocket.cljs @@ -1,7 +1,5 @@ (ns grub.websocket - (:require [grub.state :as state] - [grub.sync :as sync] - [cljs.core.async :as a :refer [! chan]] + (:require [cljs.core.async :as a :refer [! chan]] [cljs.reader] goog.net.WebSocket goog.events.EventHandler diff --git a/src/cljx/grub/common_state.cljx b/src/cljx/grub/common_state.cljx index 0e8d8d5..aaa2537 100644 --- a/src/cljx/grub/common_state.cljx +++ b/src/cljx/grub/common_state.cljx @@ -1,5 +1,5 @@ (ns grub.common-state - (:require [grub.sync :as sync] + (:require [grub.diff :as diff] [hasch.core :as hasch])) (def empty-state {:grubs {} :recipes {}}) @@ -16,7 +16,7 @@ :shadow-hash shadow-hash}) (defn diff-states [shadow state] - (let [diff (sync/diff-states shadow state) + (let [diff (diff/diff-states shadow state) hash (hasch/uuid state) shadow-hash (hasch/uuid shadow) msg (diff-msg diff hash shadow-hash)] diff --git a/src/cljx/grub/sync.cljx b/src/cljx/grub/diff.cljx similarity index 98% rename from src/cljx/grub/sync.cljx rename to src/cljx/grub/diff.cljx index f18f26c..879d367 100644 --- a/src/cljx/grub/sync.cljx +++ b/src/cljx/grub/diff.cljx @@ -1,4 +1,4 @@ -(ns grub.sync +(ns grub.diff (:require [clojure.data :as data] [clojure.set :as set])) diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/diff.clj similarity index 88% rename from src/test/grub/test/unit/sync.clj rename to src/test/grub/test/unit/diff.clj index 581c2f0..e71e755 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/diff.clj @@ -1,5 +1,5 @@ -(ns grub.test.unit.sync - (:require [grub.sync :as sync] +(ns grub.test.unit.diff + (:require [grub.diff :as diff] [clojure.test :refer :all])) @@ -9,39 +9,39 @@ (deftest diff-empty-states (let [empty-state {:grubs {} :recipes {}}] (is (= empty-diff - (sync/diff-states empty-state empty-state))))) + (diff/diff-states empty-state empty-state))))) (deftest diff-equal-states (is (= empty-diff - (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + (diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} {:grubs {"id" {:text "asdf" :completed false}} :recipes {}})))) (deftest diff-added-grub (is (= {:grubs {:deleted #{} :updated {"id" {:completed false, :text "asdf"}}} :recipes {:deleted #{} :updated nil}} - (sync/diff-states {:grubs {} :recipes {}} + (diff/diff-states {:grubs {} :recipes {}} {:grubs {"id" {:text "asdf" :completed false}} :recipes {}})))) (deftest diff-deleted-grub (is (= {:grubs {:deleted #{"id"} :updated nil} :recipes {:deleted #{} :updated nil}} - (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + (diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} {:grubs {} :recipes {}})))) (deftest diff-edited-grub (is (= {:grubs {:deleted #{} :updated {"id" {:text "asdf2"}}} :recipes {:deleted #{} :updated nil}} - (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + (diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} {:grubs {"id" {:text "asdf2" :completed false}} :recipes {}})))) (deftest diff-completed-grub (is (= {:grubs {:deleted #{} :updated {"id" {:completed true}}} :recipes {:deleted #{} :updated nil}} - (sync/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} + (diff/diff-states {:grubs {"id" {:text "asdf" :completed false}} :recipes {}} {:grubs {"id" {:text "asdf" :completed true}} :recipes {}})))) (deftest diff-added-recipe @@ -49,7 +49,7 @@ :updated nil} :recipes {:deleted #{} :updated {"id" {:name "Blue Cheese Soup" :grubs "Some grubs"}}}} - (sync/diff-states {:grubs {} :recipes {}} + (diff/diff-states {:grubs {} :recipes {}} {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" :grubs "Some grubs"}}})))) @@ -57,7 +57,7 @@ (is (= {:grubs {:deleted #{} :updated nil} :recipes {:deleted #{} :updated {"id" {:name "Bleu Cheese Soup" }}}} - (sync/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" + (diff/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" :grubs "Some grubs"}}} {:grubs {} :recipes {"id" {:name "Bleu Cheese Soup" :grubs "Some grubs"}}})))) @@ -65,7 +65,7 @@ (deftest diff-deleted-recipe (is (= {:grubs {:deleted #{} :updated nil} :recipes {:deleted #{"id"} :updated nil}} - (sync/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" + (diff/diff-states {:grubs {} :recipes {"id" {:name "Blue Cheese Soup" :grubs "Some grubs"}}} {:grubs {} :recipes {}})))) @@ -125,9 +125,9 @@ {:completed false :text "Toothpaste"}}}}) (deftest diff-many-changes - (is (= expected-diff (sync/diff-states before-state after-state)))) + (is (= expected-diff (diff/diff-states before-state after-state)))) (deftest patch-returns-original-state (is - (let [diff (sync/diff-states before-state after-state)] - (= after-state (sync/patch-state before-state diff))))) + (let [diff (diff/diff-states before-state after-state)] + (= after-state (diff/patch-state before-state diff))))) From fe5bd004272e5bdf6a0a909f92d7fd47e00d60e9 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Tue, 16 Sep 2014 23:27:10 +0300 Subject: [PATCH 18/40] Refactor - wip --- src/clj/grub/state.clj | 28 ++++++++++++++++ src/test/grub/test/unit/state.clj | 56 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/test/grub/test/unit/state.clj diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 3b19142..df433dc 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -5,6 +5,33 @@ [clojure.core.async :as a :refer [! chan go]] [hasch.core :as hasch])) +(def initial-state {:hash (hasch/uuid cs/empty-state) + :state cs/empty-state}) +(def states (atom [initial-state])) + +(defn get-history-state [states hash] + (:state (first (filter #(= (:hash %) hash) states)))) + +(defn add-history-state [current new-state] + (let [{:keys [state hash]} (first current) + new-hash (hasch/uuid new-state)] + (if (= hash new-hash) + current + (conj current {:hash hash :state state})))) + +(defn receive-diff [states diff shadow-hash] + (let [state (:state (first states)) + shadow (get-history-state states shadow-hash)] + (if shadow + {:new-state (diff/patch-state state diff) + :new-shadow (diff/patch-state shadow diff) + :full-sync? false} + {:new-state state + :new-shadow state + :full-sync? true}))) + + + (def state (atom cs/empty-state)) (def to-db (atom nil)) (def to-all (chan)) @@ -26,6 +53,7 @@ {:grubs (util/map-by-key :id grubs) :recipes (util/map-by-key :id recipes)}) + (defn sync-new-client! [to from] (let [client-id (java.util.UUID/randomUUID) client-state (atom cs/empty-state) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj new file mode 100644 index 0000000..c30a04d --- /dev/null +++ b/src/test/grub/test/unit/state.clj @@ -0,0 +1,56 @@ +(ns grub.test.unit.state + (:require [grub.state :as s] + [grub.common-state :as cs] + [clojure.test :refer :all] + [hasch.core :as hasch])) + +(deftest apply-diff-normally + ;; Apply changes and return ACK for in sync client/server + (let [state {:grubs {"1" {:text "2 apples" :completed false}} + :recipes {}} + hash (hasch/uuid state) + states [{:hash hash :state state}] + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + shadow-hash hash + {:keys [new-state new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + (do + (is (= {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}} + new-state)) + (is (= {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}} + new-shadow)) + (is (not full-sync?))))) + +(deftest server-state-changed + ;; Send differences back if server state changed + (let [state {:grubs {"1" {:text "3 apples" :completed false}} :recipes {}} + prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + states [{:hash (hasch/uuid state) :state state} + {:hash (hasch/uuid prev) :state prev}] + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + shadow-hash (hasch/uuid prev) + {:keys [new-state new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + (do + (is (= {:grubs {"1" {:text "3 apples" :completed true}} + :recipes {}} + new-state)) + (is (= {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}} + new-shadow)) + (is (not full-sync?))))) + +(deftest full-sync-if-client-too-far-out-of-sync + ;; Shadow hash not in history means client has fallen too far + ;; out of sync. Send a full sync + (let [state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + states [{:hash (hasch/uuid state) :state state} + {:hash (hasch/uuid prev) :state prev}] + shadow-hash (hasch/uuid {:grubs {} :recipes {}}) + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + {:keys [new-state new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + (do + (is (= state new-state)) + (is (= state new-shadow)) + (is full-sync?)))) From 1b8339dec006f9e82453e9378569a8d23a5859ed Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Mon, 22 Sep 2014 22:47:36 +0300 Subject: [PATCH 19/40] Mid-changes: organize state changes --- src/clj/grub/state.clj | 193 ++++++++++++++++++++++++----------------- 1 file changed, 113 insertions(+), 80 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index df433dc..0901cb3 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -5,19 +5,21 @@ [clojure.core.async :as a :refer [! chan go]] [hasch.core :as hasch])) -(def initial-state {:hash (hasch/uuid cs/empty-state) - :state cs/empty-state}) -(def states (atom [initial-state])) +(defn initial-state [grubs recipes] + [{:grubs (util/map-by-key :id grubs) + :recipes (util/map-by-key :id recipes)}]) + +(def states (ref [])) (defn get-history-state [states hash] (:state (first (filter #(= (:hash %) hash) states)))) -(defn add-history-state [current new-state] - (let [{:keys [state hash]} (first current) +(defn add-history-state [states new-state] + (let [{:keys [state hash]} (first states) new-hash (hasch/uuid new-state)] (if (= hash new-hash) - current - (conj current {:hash hash :state state})))) + states + (conj states {:hash hash :state state})))) (defn receive-diff [states diff shadow-hash] (let [state (:state (first states)) @@ -30,84 +32,115 @@ :new-shadow state :full-sync? true}))) +(defn apply-diff? [states diff shadow-hash] + (get-history-state states shadow-hash)) - -(def state (atom cs/empty-state)) (def to-db (atom nil)) -(def to-all (chan)) -(def from-all (a/mult to-all)) -(def state-history (atom [])) +(defn make-client [in client-state states] + (let [out (chan) + full-sync! (fn [] (let [new-client (dosync (ref-set client-state @states))] + (a/put! out (cs/complete-sync-response new-client))))] + (a/go-loop + [] + (when-let [{:keys [type diff shadow-hash state]} (client ! @to-db diff)) - (do (log "Applying diff failed --> full sync") - (let [sync-state @state] - (reset! client-state sync-state) - (a/put! to (cs/complete-sync-response sync-state)))))) - ;; We have something different than they thought - ;; Check history - (do - (log "Hash check failed --> Reset from history") - (if-let [history-state (get-history-state shadow-hash)] - ;; Found what they thought we had in history, - ;; reset client state to this and continue as normal - (do - (reset! client-state history-state) - (let [new-shadow (swap! client-state diff/patch-state diff)] - (if (= (hasch/uuid new-shadow) hash) - (let [new-state (swap! state diff/patch-state diff)] - (>! @to-db diff)) - (do (log "Applying diff failed --> full sync") - (let [sync-state @state] - (reset! client-state sync-state) - (a/put! to (cs/complete-sync-response sync-state))))))) - ;; Not found in history, do complete sync - (do (log "Hash check failed, not in history --> full sync") - (let [sync-state @state] - (reset! client-state sync-state) - (a/put! to (cs/complete-sync-response sync-state))))))) - :complete (let [new-state (reset! client-state @state)] - (log "full sync") - (a/put! to (cs/complete-sync-response new-state))) - (log "Invalid msg:" msg)) - (recur)) - (remove-watch state client-id))))) + state-changes (chan) + events (chan) + client-state (ref cs/empty-state)] + (add-watch states client-id (fn [_ _ _ [state _]] (a/put! state-changes state))) + (a/pipe (a/merge ! >client v) + (recur)) + (remove-watch states client-id)))) + + ;; (let [full-sync! (fn [] (let [new-client (dosync (ref-set client-state @states))] + ;; (a/put! >client (cs/complete-sync-response new-client))))] + ;; (a/go-loop + ;; [] + ;; (if-let [{:keys [type diff shadow-hash state]} (client (diff/diff-states @client-state (first @states))) + ;; :diff (dosync + ;; (let [state (:state (first @states)) + ;; shadow (get-history-state states shadow-hash)] + ;; (if shadow + ;; (do (alter states add-history-state (diff/patch-state state diff)) + ;; (alter client-state diff/patch-state shadow diff)) + ;; (full-sync!)))) + ;; :full-sync (full-sync!)) + ;; (remove-watch states client-id)))) + )) + +;; (defn sync-new-client! [to from] +;; (let [client-id (java.util.UUID/randomUUID) +;; state-changes (chan)] +;; (add-watch states client-id (fn [_ _ _ [current-state _]] +;; (put! state-changes current-state))) +;; (a/go-loop [client-state cs/empty-state] +;; (if-let [[{:keys [type] :as msg} c] (! @to-db diff)) +;; (do (log "Applying diff failed --> full sync") +;; (let [sync-state @state] +;; (reset! client-state sync-state) +;; (a/put! to (cs/complete-sync-response sync-state)))))) +;; ;; We have something different than they thought +;; ;; Check history +;; (do +;; (log "Hash check failed --> Reset from history") +;; (if-let [history-state (get-history-state shadow-hash)] +;; ;; Found what they thought we had in history, +;; ;; reset client state to this and continue as normal +;; (do +;; (reset! client-state history-state) +;; (let [new-shadow (swap! client-state diff/patch-state diff)] +;; (if (= (hasch/uuid new-shadow) hash) +;; (let [new-state (swap! state diff/patch-state diff)] +;; (>! @to-db diff)) +;; (do (log "Applying diff failed --> full sync") +;; (let [sync-state @state] +;; (reset! client-state sync-state) +;; (a/put! to (cs/complete-sync-response sync-state))))))) +;; ;; Not found in history, do complete sync +;; (do (log "Hash check failed, not in history --> full sync") +;; (let [sync-state @state] +;; (reset! client-state sync-state) +;; (a/put! to (cs/complete-sync-response sync-state))))))) +;; :complete (let [new-state (reset! client-state @state)] +;; (log "full sync") +;; (a/put! to (cs/complete-sync-response new-state))) +;; (log "Invalid msg:" msg)) +;; (recur)) +;; (remove-watch state client-id))))) (defn init [_to-db grubs recipes] - (reset! state (initial-state grubs recipes)) + (reset! states (initial-state grubs recipes)) (reset! to-db _to-db)) From dc355eb6ec2eb407479a8d237240d1fb0d225f14 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Tue, 23 Sep 2014 19:17:26 +0300 Subject: [PATCH 20/40] Separate state handle, sync algorithm --- src/clj/grub/message.clj | 12 +++ src/clj/grub/state.clj | 159 +++++++------------------------ src/clj/grub/sync.clj | 40 ++++++++ src/test/grub/test/unit/sync.clj | 55 +++++++++++ 4 files changed, 142 insertions(+), 124 deletions(-) create mode 100644 src/clj/grub/message.clj create mode 100644 src/clj/grub/sync.clj create mode 100644 src/test/grub/test/unit/sync.clj diff --git a/src/clj/grub/message.clj b/src/clj/grub/message.clj new file mode 100644 index 0000000..47af02f --- /dev/null +++ b/src/clj/grub/message.clj @@ -0,0 +1,12 @@ +(ns grub.message) + +(def full-sync-request {:type :complete}) + +(defn full-sync [state] + {:type :complete + :state state}) + +(defn diff-msg [diff shadow-hash] + {:type :diff + :diff diff + :shadow-hash shadow-hash}) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 0901cb3..c2e805a 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -1,146 +1,57 @@ (ns grub.state (:require [grub.diff :as diff] - [grub.util :as util] - [grub.common-state :as cs] - [clojure.core.async :as a :refer [! chan go]] - [hasch.core :as hasch])) - -(defn initial-state [grubs recipes] - [{:grubs (util/map-by-key :id grubs) - :recipes (util/map-by-key :id recipes)}]) + [grub.message :as message] + [grub.sync :as sync] + [clojure.core.async :as a :refer [! chan go]])) +;; Server state (def states (ref [])) -(defn get-history-state [states hash] - (:state (first (filter #(= (:hash %) hash) states)))) - -(defn add-history-state [states new-state] - (let [{:keys [state hash]} (first states) - new-hash (hasch/uuid new-state)] - (if (= hash new-hash) - states - (conj states {:hash hash :state state})))) - -(defn receive-diff [states diff shadow-hash] - (let [state (:state (first states)) - shadow (get-history-state states shadow-hash)] - (if shadow - {:new-state (diff/patch-state state diff) - :new-shadow (diff/patch-state shadow diff) - :full-sync? false} - {:new-state state - :new-shadow state - :full-sync? true}))) - -(defn apply-diff? [states diff shadow-hash] - (get-history-state states shadow-hash)) - (def to-db (atom nil)) (defn make-client [in client-state states] (let [out (chan) - full-sync! (fn [] (let [new-client (dosync (ref-set client-state @states))] - (a/put! out (cs/complete-sync-response new-client))))] + full-sync! (fn [] + (let [new-client (dosync (let [state (sync/get-current-state @states)] + (ref-set client-state state)))] + (println "full-sync!") + (a/put! out (message/full-sync new-client))))] (a/go-loop - [] - (when-let [{:keys [type diff shadow-hash state]} (client ! >client v) (recur)) - (remove-watch states client-id)))) - - ;; (let [full-sync! (fn [] (let [new-client (dosync (ref-set client-state @states))] - ;; (a/put! >client (cs/complete-sync-response new-client))))] - ;; (a/go-loop - ;; [] - ;; (if-let [{:keys [type diff shadow-hash state]} (client (diff/diff-states @client-state (first @states))) - ;; :diff (dosync - ;; (let [state (:state (first @states)) - ;; shadow (get-history-state states shadow-hash)] - ;; (if shadow - ;; (do (alter states add-history-state (diff/patch-state state diff)) - ;; (alter client-state diff/patch-state shadow diff)) - ;; (full-sync!)))) - ;; :full-sync (full-sync!)) - ;; (remove-watch states client-id)))) - )) - -;; (defn sync-new-client! [to from] -;; (let [client-id (java.util.UUID/randomUUID) -;; state-changes (chan)] -;; (add-watch states client-id (fn [_ _ _ [current-state _]] -;; (put! state-changes current-state))) -;; (a/go-loop [client-state cs/empty-state] -;; (if-let [[{:keys [type] :as msg} c] (! @to-db diff)) -;; (do (log "Applying diff failed --> full sync") -;; (let [sync-state @state] -;; (reset! client-state sync-state) -;; (a/put! to (cs/complete-sync-response sync-state)))))) -;; ;; We have something different than they thought -;; ;; Check history -;; (do -;; (log "Hash check failed --> Reset from history") -;; (if-let [history-state (get-history-state shadow-hash)] -;; ;; Found what they thought we had in history, -;; ;; reset client state to this and continue as normal -;; (do -;; (reset! client-state history-state) -;; (let [new-shadow (swap! client-state diff/patch-state diff)] -;; (if (= (hasch/uuid new-shadow) hash) -;; (let [new-state (swap! state diff/patch-state diff)] -;; (>! @to-db diff)) -;; (do (log "Applying diff failed --> full sync") -;; (let [sync-state @state] -;; (reset! client-state sync-state) -;; (a/put! to (cs/complete-sync-response sync-state))))))) -;; ;; Not found in history, do complete sync -;; (do (log "Hash check failed, not in history --> full sync") -;; (let [sync-state @state] -;; (reset! client-state sync-state) -;; (a/put! to (cs/complete-sync-response sync-state))))))) -;; :complete (let [new-state (reset! client-state @state)] -;; (log "full sync") -;; (a/put! to (cs/complete-sync-response new-state))) -;; (log "Invalid msg:" msg)) -;; (recur)) -;; (remove-watch state client-id))))) + (remove-watch states client-id)))))) (defn init [_to-db grubs recipes] - (reset! states (initial-state grubs recipes)) + (dosync (ref-set states (sync/initial-state grubs recipes))) (reset! to-db _to-db)) diff --git a/src/clj/grub/sync.clj b/src/clj/grub/sync.clj new file mode 100644 index 0000000..0d805dc --- /dev/null +++ b/src/clj/grub/sync.clj @@ -0,0 +1,40 @@ +(ns grub.sync + (:require [grub.diff :as diff] + [grub.util :as util] + [hasch.core :as hasch])) + +(def empty-state {:grubs {} :recipes {}}) + +(defn initial-state [grubs recipes] + (let [state {:grubs (util/map-by-key :id grubs) + :recipes (util/map-by-key :id recipes)}] + [{:state state :hash (hasch/uuid state)}])) + +(defn get-current-state [states] + (:state (last states))) + +(defn get-history-state [states hash] + (:state (first (filter #(= (:hash %) hash) states)))) + +(defn add-history-state [states new-state] + (let [last-hash (:hash (last states)) + new-hash (hasch/uuid new-state)] + (if (= last-hash new-hash) + states + (conj states {:hash new-hash :state new-state})))) + +(defn diff-states [states shadow] + (let [state (get-current-state states)] + {:shadow-hash (hasch/uuid shadow) + :diff (diff/diff-states shadow state)})) + +(defn apply-diff [states diff shadow-hash] + (let [state (:state (first states)) + shadow (get-history-state states shadow-hash)] + (if shadow + {:new-states (add-history-state states (diff/patch-state state diff)) + :new-shadow (diff/patch-state shadow diff) + :full-sync? false} + {:new-states states + :new-shadow state + :full-sync? true}))) diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj new file mode 100644 index 0000000..1fb46cd --- /dev/null +++ b/src/test/grub/test/unit/sync.clj @@ -0,0 +1,55 @@ +(ns grub.test.unit.sync + (:require [grub.sync :as s] + [clojure.test :refer :all] + [hasch.core :as hasch])) + +(deftest apply-diff-normally + ;; Apply changes and return ACK for in sync client/server + (let [state {:grubs {"1" {:text "2 apples" :completed false}} + :recipes {}} + hash (hasch/uuid state) + states [{:hash hash :state state}] + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + shadow-hash hash + {:keys [new-states new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + (do + (is (= {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}} + (:state (last new-states)))) + (is (= {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}} + new-shadow)) + (is (not full-sync?))))) + +(deftest server-state-changed + ;; Send differences back if server state changed + (let [state {:grubs {"1" {:text "3 apples" :completed false}} :recipes {}} + prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + states [{:hash (hasch/uuid state) :state state} + {:hash (hasch/uuid prev) :state prev}] + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + shadow-hash (hasch/uuid prev) + {:keys [new-states new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + (do + (is (= {:grubs {"1" {:text "3 apples" :completed true}} + :recipes {}} + (:state (last new-states)))) + (is (= {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}} + new-shadow)) + (is (not full-sync?))))) + +(deftest full-sync-if-client-too-far-out-of-sync + ;; Shadow hash not in history means client has fallen too far + ;; out of sync. Send a full sync + (let [state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + states [{:hash (hasch/uuid state) :state state} + {:hash (hasch/uuid prev) :state prev}] + shadow-hash (hasch/uuid {:grubs {} :recipes {}}) + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + {:keys [new-states new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + (do + (is (= state (:state (last new-states)))) + (is (= state new-shadow)) + (is full-sync?)))) From 36a0e3d419dd2f2ca6b6a6bee675061eb1776d11 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Tue, 23 Sep 2014 19:51:31 +0300 Subject: [PATCH 21/40] Move sync code to shared --- .../grub/sync.clj => cljx/grub/sync.cljx} | 0 src/test/grub/test/unit/state.clj | 56 ------------------- src/test/grub/test/unit/sync.clj | 35 ++++++++++-- 3 files changed, 29 insertions(+), 62 deletions(-) rename src/{clj/grub/sync.clj => cljx/grub/sync.cljx} (100%) delete mode 100644 src/test/grub/test/unit/state.clj diff --git a/src/clj/grub/sync.clj b/src/cljx/grub/sync.cljx similarity index 100% rename from src/clj/grub/sync.clj rename to src/cljx/grub/sync.cljx diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj deleted file mode 100644 index c30a04d..0000000 --- a/src/test/grub/test/unit/state.clj +++ /dev/null @@ -1,56 +0,0 @@ -(ns grub.test.unit.state - (:require [grub.state :as s] - [grub.common-state :as cs] - [clojure.test :refer :all] - [hasch.core :as hasch])) - -(deftest apply-diff-normally - ;; Apply changes and return ACK for in sync client/server - (let [state {:grubs {"1" {:text "2 apples" :completed false}} - :recipes {}} - hash (hasch/uuid state) - states [{:hash hash :state state}] - diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - shadow-hash hash - {:keys [new-state new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] - (do - (is (= {:grubs {"1" {:text "2 apples" :completed true}} - :recipes {}} - new-state)) - (is (= {:grubs {"1" {:text "2 apples" :completed true}} - :recipes {}} - new-shadow)) - (is (not full-sync?))))) - -(deftest server-state-changed - ;; Send differences back if server state changed - (let [state {:grubs {"1" {:text "3 apples" :completed false}} :recipes {}} - prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - states [{:hash (hasch/uuid state) :state state} - {:hash (hasch/uuid prev) :state prev}] - diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - shadow-hash (hasch/uuid prev) - {:keys [new-state new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] - (do - (is (= {:grubs {"1" {:text "3 apples" :completed true}} - :recipes {}} - new-state)) - (is (= {:grubs {"1" {:text "2 apples" :completed true}} - :recipes {}} - new-shadow)) - (is (not full-sync?))))) - -(deftest full-sync-if-client-too-far-out-of-sync - ;; Shadow hash not in history means client has fallen too far - ;; out of sync. Send a full sync - (let [state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - states [{:hash (hasch/uuid state) :state state} - {:hash (hasch/uuid prev) :state prev}] - shadow-hash (hasch/uuid {:grubs {} :recipes {}}) - diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - {:keys [new-state new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] - (do - (is (= state new-state)) - (is (= state new-shadow)) - (is full-sync?)))) diff --git a/src/test/grub/test/unit/sync.clj b/src/test/grub/test/unit/sync.clj index 1fb46cd..c074486 100644 --- a/src/test/grub/test/unit/sync.clj +++ b/src/test/grub/test/unit/sync.clj @@ -3,7 +3,30 @@ [clojure.test :refer :all] [hasch.core :as hasch])) -(deftest apply-diff-normally +(deftest initial-state + (let [grubs [{:id "1" :text "2 bananas" :completed false} + {:id "2" :text "3 onions" :completed false}] + recipes [] + expected-state {:grubs {"1" {:id "1" :text "2 bananas" :completed false} + "2" {:id "2" :text "3 onions" :completed false}} + :recipes {}} + expected-hash (hasch/uuid expected-state)] + (is (= [{:state expected-state :hash expected-hash}] (s/initial-state grubs recipes))))) + +(deftest get-current-state-returns-last-state + (let [states [{:hash "asdf" :state {:a :b}} + {:hash "fdsa" :state {:c :d}}]] + (is (= {:c :d} (s/get-current-state states))))) + +(deftest get-history-state-returns-state-with-hash + (let [states [{:hash "hash1" :state {:a :b}} + {:hash "hash2" :state {:c :d}} + {:hash "hash3" :state {:e :f}}]] + (is (= {:a :b} (s/get-history-state states "hash1"))) + (is (= {:c :d} (s/get-history-state states "hash2"))) + (is (= {:e :f} (s/get-history-state states "hash3"))))) + +(deftest apply-diff-no-changes ;; Apply changes and return ACK for in sync client/server (let [state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} @@ -11,7 +34,7 @@ states [{:hash hash :state state}] diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} shadow-hash hash - {:keys [new-states new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + {:keys [new-states new-shadow full-sync?]} (s/apply-diff states diff shadow-hash)] (do (is (= {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}} @@ -21,7 +44,7 @@ new-shadow)) (is (not full-sync?))))) -(deftest server-state-changed +(deftest apply-diff-server-state-changed ;; Send differences back if server state changed (let [state {:grubs {"1" {:text "3 apples" :completed false}} :recipes {}} prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} @@ -29,7 +52,7 @@ {:hash (hasch/uuid prev) :state prev}] diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} shadow-hash (hasch/uuid prev) - {:keys [new-states new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + {:keys [new-states new-shadow full-sync?]} (s/apply-diff states diff shadow-hash)] (do (is (= {:grubs {"1" {:text "3 apples" :completed true}} :recipes {}} @@ -39,7 +62,7 @@ new-shadow)) (is (not full-sync?))))) -(deftest full-sync-if-client-too-far-out-of-sync +(deftest apply-diff-client-out-of-sync ;; Shadow hash not in history means client has fallen too far ;; out of sync. Send a full sync (let [state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} @@ -48,7 +71,7 @@ {:hash (hasch/uuid prev) :state prev}] shadow-hash (hasch/uuid {:grubs {} :recipes {}}) diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - {:keys [new-states new-shadow full-sync?]} (s/receive-diff states diff shadow-hash)] + {:keys [new-states new-shadow full-sync?]} (s/apply-diff states diff shadow-hash)] (do (is (= state (:state (last new-states)))) (is (= state new-shadow)) From f632fabdb2c8bfe475cbe77e8dcf0d711c3ca7be Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Tue, 23 Sep 2014 19:52:47 +0300 Subject: [PATCH 22/40] Move message to shared --- src/{clj/grub/message.clj => cljx/grub/message.cljx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{clj/grub/message.clj => cljx/grub/message.cljx} (100%) diff --git a/src/clj/grub/message.clj b/src/cljx/grub/message.cljx similarity index 100% rename from src/clj/grub/message.clj rename to src/cljx/grub/message.cljx From 0d2d619a1b99a5cde0684dc4159b9097d6e40230 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Tue, 23 Sep 2014 20:01:26 +0300 Subject: [PATCH 23/40] Remove cljx hooks --- project.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/project.clj b/project.clj index ff7d88a..20f14f6 100644 --- a/project.clj +++ b/project.clj @@ -41,7 +41,6 @@ {:source-paths ["src/cljx"] :output-path "target/generated/cljs" :rules :cljs}]} - :hooks [cljx.hooks] :source-paths ["src/clj" "src/test"] :test-paths ["spec/clj"] :ring {:handler grub.core/app} From 31b6d40aef87246e608f934a773cc3204443b9f8 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Wed, 24 Sep 2014 06:20:19 +0300 Subject: [PATCH 24/40] wip --- src/clj/grub/state.clj | 70 +++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index c2e805a..47fcdad 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -6,36 +6,44 @@ ;; Server state (def states (ref [])) - (def to-db (atom nil)) -(defn make-client [in client-state states] - (let [out (chan) - full-sync! (fn [] - (let [new-client (dosync (let [state (sync/get-current-state @states)] +(defmulti handle-message (fn [msg states client-state] (:type msg))) + +(defn full-sync! [msg states client-state] + (let [new-client (dosync (let [state (sync/get-current-state @states)] (ref-set client-state state)))] (println "full-sync!") - (a/put! out (message/full-sync new-client))))] - (a/go-loop - [] - (when-let [msg (! >client response)) + (recur)) + (remove-watch states client-id))) out)) (defn sync-new-client! [>client ! >client v) - (recur)) - (remove-watch states client-id)))))) + (a/go-loop [] (if-let [msg (! >client response)) + (recur)) + (remove-watch states client-id))))) (defn init [_to-db grubs recipes] (dosync (ref-set states (sync/initial-state grubs recipes))) From 3760fc805990f037bce7ce3f23b9ab4e86980114 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Wed, 24 Sep 2014 08:25:42 +0300 Subject: [PATCH 25/40] Possible changes - wip --- src/clj/grub/state.clj | 81 ++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 47fcdad..1c149a6 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -5,61 +5,42 @@ [clojure.core.async :as a :refer [! chan go]])) ;; Server state -(def states (ref [])) -(def to-db (atom nil)) +(def states (atom [])) -(defmulti handle-message (fn [msg states client-state] (:type msg))) - -(defn full-sync! [msg states client-state] - (let [new-client (dosync (let [state (sync/get-current-state @states)] - (ref-set client-state state)))] - (println "full-sync!") - (message/full-sync new-client))) - -(defmethod handle-message :full-sync [msg states client-state] - (full-sync! msg states client-state)) - -(defmethod handle-message :new-state [msg states client-state] - (let [diff-result (sync/diff-states (:new-states msg) @client-state) - {:keys [diff shadow-hash]} diff-result] - (println "new-state!") - (message/diff-msg diff shadow-hash))) - -(defmethod handle-message :diff [msg states client-state] - (dosync - (println "diff!") - (let [{:keys [diff shadow-hash]} msg - apply-result (sync/apply-diff @states diff shadow-hash) - {:keys [new-states new-shadow full-sync?]} apply-result] - (ref-set states new-states) - (ref-set client-state new-shadow) - (when full-sync? (full-sync! msg states client-state))))) - -(defn make-client-agent [in initial-states] - (let [out (chan)] - (a/go-loop [client-state sync/empty-state - states initial-states] - (if-let [msg (! >client response)) - (recur)) - (remove-watch states client-id))) - out)) +(defn make-client-agent [in >client] + (a/go-loop [client-state sync/empty-state] + (when-let [msg (! >client message/full-sync state) + (recur state)) + (do (reset! states new-states) + (recur new-shadow)))) + :full-sync + (let [state (get-current-state @states)] + (>! >client message/full-sync state) + (recur state)) + :new-state + (let [{:keys [diff shadow-hash]} (sync/diff-states (:new-states msg) @client-state)] + (println "new-state!") + (>! >client (message/diff-msg diff shadow-hash))) + (do (println "Unknown event") + (recur client-state)))))) +;; TODO: Remove watch, close up channels properly (defn sync-new-client! [>client ! >client response)) - (recur)) - (remove-watch states client-id))))) + (a/pipe (a/merge [client))) -(defn init [_to-db grubs recipes] - (dosync (ref-set states (sync/initial-state grubs recipes))) - (reset! to-db _to-db)) +(defn init [to-db grubs recipes] + (reset! states (sync/initial-state grubs recipes)) + (add-watch states :to-db (fn [_ _ old-states new-states] + (a/put! to-db (sync/get-current-state new-states))))) From edab2ad684eef3c232b5f14695f5c53a01d9c9f6 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Thu, 25 Sep 2014 21:43:31 +0300 Subject: [PATCH 26/40] Add sync test for single diff --- src/clj/grub/state.clj | 50 +++++++++++++++++-------------- src/cljs/grub/state.cljs | 26 ++++++++-------- src/cljx/grub/message.cljx | 8 ++--- src/cljx/grub/sync.cljx | 2 +- src/test/grub/test/unit/state.clj | 32 ++++++++++++++++++++ 5 files changed, 79 insertions(+), 39 deletions(-) create mode 100644 src/test/grub/test/unit/state.clj diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 1c149a6..7a6bc3f 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -7,28 +7,34 @@ ;; Server state (def states (atom [])) -(defn make-client-agent [in >client] +(defn make-server-agent [in >client states] (a/go-loop [client-state sync/empty-state] - (when-let [msg (! >client message/full-sync state) - (recur state)) - (do (reset! states new-states) - (recur new-shadow)))) - :full-sync - (let [state (get-current-state @states)] - (>! >client message/full-sync state) - (recur state)) - :new-state - (let [{:keys [diff shadow-hash]} (sync/diff-states (:new-states msg) @client-state)] - (println "new-state!") - (>! >client (message/diff-msg diff shadow-hash))) - (do (println "Unknown event") - (recur client-state)))))) + (when-let [msg (! >client (message/full-sync state)) + (recur state)) + (do (println "state found, just send changes") + (let [{:keys [diff hash]} (sync/diff-states new-states new-shadow)] + (reset! states new-states) + (>! >client (message/diff-msg diff hash)) + (recur new-shadow))))) + :full-sync + (let [state (sync/get-current-state @states)] + (println "got full sync, send full sync") + (>! >client (message/full-sync state)) + (recur state)) + :new-state + (let [{:keys [diff shadow-hash]} (sync/diff-states (:new-states msg) client-state)] + (println "new-state!") + (>! >client (message/diff-msg diff shadow-hash))) + (do (println "Unknown message:" msg) + (recur client-state)))))) ;; TODO: Remove watch, close up channels properly (defn sync-new-client! [>client client))) + (make-server-agent client-events >client states))) (defn init [to-db grubs recipes] (reset! states (sync/initial-state grubs recipes)) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs index 3c451c5..ef33769 100644 --- a/src/cljs/grub/state.cljs +++ b/src/cljs/grub/state.cljs @@ -1,13 +1,15 @@ (ns grub.state (:require [grub.diff :as diff] [grub.common-state :as cs] + [grub.message :as message] + [grub.sync :as sync] [cljs.core.async :as a :refer [! chan]] [hasch.core :as hasch]) (:require-macros [grub.macros :refer [log logs]] [cljs.core.async.macros :refer [go go-loop]])) -(def state (atom cs/empty-state)) -(def server-state (atom cs/empty-state)) +(def state (atom sync/empty-state)) +(def server-state (atom sync/empty-state)) (def unacked-states (atom {})) @@ -36,15 +38,15 @@ (let [new-server (swap! server-state #(diff/patch-state % diff))] (if (= (hasch/uuid new-server) hash) (swap! state diff/patch-state diff) - (do (log "State update failure --> complete sync") - (a/put! from cs/complete-sync-request))))) - (do (log "Could not find server state locally --> complete sync") - (a/put! from cs/complete-sync-request))) - :complete (do - (logs "Complete sync") - (reset! unacked-states {}) - (reset! server-state (:state msg)) - (reset! state (:state msg))) + (do (log "State update failure --> full sync") + (a/put! from message/full-sync-request))))) + (do (log "Could not find server state locally --> full sync") + (a/put! from message/full-sync-request))) + :full-sync (do + (logs "Full sync") + (reset! unacked-states {}) + (reset! server-state (:state msg)) + (reset! state (:state msg))) (logs "Invalid msg:" msg)) (recur)) (remove-watch state :state)))) @@ -52,4 +54,4 @@ (defn sync-state! [to from reset? state-changes] (send-state-changes-to-server! state-changes from) (handle-received-changes! to from) - (a/put! from (if reset? cs/complete-sync-request (cs/diff-states @server-state @state)))) + (a/put! from (if reset? message/full-sync-request (cs/diff-states @server-state @state)))) diff --git a/src/cljx/grub/message.cljx b/src/cljx/grub/message.cljx index 47af02f..09b94c1 100644 --- a/src/cljx/grub/message.cljx +++ b/src/cljx/grub/message.cljx @@ -1,12 +1,12 @@ (ns grub.message) -(def full-sync-request {:type :complete}) +(def full-sync-request {:type :full-sync}) (defn full-sync [state] - {:type :complete + {:type :full-sync :state state}) -(defn diff-msg [diff shadow-hash] +(defn diff-msg [diff hash] {:type :diff :diff diff - :shadow-hash shadow-hash}) + :hash hash}) diff --git a/src/cljx/grub/sync.cljx b/src/cljx/grub/sync.cljx index 0d805dc..eee253a 100644 --- a/src/cljx/grub/sync.cljx +++ b/src/cljx/grub/sync.cljx @@ -25,7 +25,7 @@ (defn diff-states [states shadow] (let [state (get-current-state states)] - {:shadow-hash (hasch/uuid shadow) + {:hash (hasch/uuid shadow) :diff (diff/diff-states shadow state)})) (defn apply-diff [states diff shadow-hash] diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj new file mode 100644 index 0000000..970e81d --- /dev/null +++ b/src/test/grub/test/unit/state.clj @@ -0,0 +1,32 @@ +(ns grub.test.unit.state + (:require [grub.state :as state] + [clojure.test :refer :all] + [hasch.core :as hasch] + [clojure.core.async :as a :refer [!! chan go]])) + +(deftest single-diff + ;; Returns empty ACK diff + (let [in (chan 1) + >client (chan 1) + state {:grubs {"1" {:text "2 apples" :completed false}} + :recipes {}} + hash (hasch/uuid state) + states* [{:hash hash :state state}] + states (atom states*) + diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + diff-msg {:type :diff + :diff diff + :hash hash} + server-agent (state/make-server-agent in >client states)] + (>!! in diff-msg) + (let [diff-response (client)] + (is (= @states + [{:hash #uuid "0cb7ae13-2523-52fa-aa79-4a6f2489cafd" + :state {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}} + {:hash #uuid "166d7e23-5a7b-5101-8364-0d2c06b8d554" + :state {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}}])) + (is (= diff-response + {:type :diff + :diff {:grubs {:deleted #{}, :updated nil} + :recipes {:deleted #{}, :updated nil}} + :hash #uuid "166d7e23-5a7b-5101-8364-0d2c06b8d554"}))))) From 39307f5a738e2b8d172e239f162f5d1ec048842f Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Thu, 25 Sep 2014 21:58:18 +0300 Subject: [PATCH 27/40] Test at "server agent" level --- src/clj/grub/state.clj | 58 +++++----- src/cljx/grub/sync.cljx | 14 +-- src/test/grub/test/unit/state.clj | 179 ++++++++++++++++++++++++++---- src/test/grub/test/unit/sync.clj | 51 --------- 4 files changed, 189 insertions(+), 113 deletions(-) diff --git a/src/clj/grub/state.clj b/src/clj/grub/state.clj index 7a6bc3f..6fb1348 100644 --- a/src/clj/grub/state.clj +++ b/src/clj/grub/state.clj @@ -7,34 +7,36 @@ ;; Server state (def states (atom [])) -(defn make-server-agent [in >client states] - (a/go-loop [client-state sync/empty-state] - (when-let [msg (! >client (message/full-sync state)) - (recur state)) - (do (println "state found, just send changes") - (let [{:keys [diff hash]} (sync/diff-states new-states new-shadow)] - (reset! states new-states) - (>! >client (message/diff-msg diff hash)) - (recur new-shadow))))) - :full-sync - (let [state (sync/get-current-state @states)] - (println "got full sync, send full sync") - (>! >client (message/full-sync state)) - (recur state)) - :new-state - (let [{:keys [diff shadow-hash]} (sync/diff-states (:new-states msg) client-state)] - (println "new-state!") - (>! >client (message/diff-msg diff shadow-hash))) - (do (println "Unknown message:" msg) - (recur client-state)))))) +(defn make-server-agent + ([in out states] (make-server-agent in out states sync/empty-state)) + ([in out states initial-client-state] + (a/go-loop [client-state initial-client-state] + (when-let [msg (! out (message/diff-msg diff hash)) + (recur new-shadow)) + (let [state (sync/get-current-state @states)] + (>! out (message/full-sync state)) + (recur state)))) + + :full-sync + (let [state (sync/get-current-state @states)] + (>! out (message/full-sync state)) + (recur state)) + + :new-state + (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) client-state)] + (>! out (message/diff-msg diff hash))) + (do (println "Unknown message:" msg) + (recur client-state))))))) ;; TODO: Remove watch, close up channels properly (defn sync-new-client! [>client !! chan go]])) -(deftest single-diff - ;; Returns empty ACK diff - (let [in (chan 1) - >client (chan 1) - state {:grubs {"1" {:text "2 apples" :completed false}} - :recipes {}} - hash (hasch/uuid state) - states* [{:hash hash :state state}] - states (atom states*) - diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - diff-msg {:type :diff - :diff diff - :hash hash} - server-agent (state/make-server-agent in >client states)] - (>!! in diff-msg) - (let [diff-response (client)] - (is (= @states - [{:hash #uuid "0cb7ae13-2523-52fa-aa79-4a6f2489cafd" - :state {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}} - {:hash #uuid "166d7e23-5a7b-5101-8364-0d2c06b8d554" - :state {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}}])) - (is (= diff-response - {:type :diff +(defn hashed-states [& states] + (->> states + (map (fn [s] {:hash (hasch/uuid s) + :state s})) + (into []))) + +(deftest diff-no-server-changes + ;; Returns empty ACK diff with hash of current state + ;; when no server changes + (let [states (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}) + states* (atom states) + msg {:type :diff + :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + :hash (:hash (first states))} + in (chan 1) + out (chan 1)] + (state/make-server-agent in out states*) + (>!! in msg) + (let [diff-response (!! in msg) + (let [diff-response (!! in msg) + (let [diff-response (!! in msg) + (let [diff-response (!! in msg) + (let [diff-response ( Date: Thu, 25 Sep 2014 23:16:57 +0300 Subject: [PATCH 28/40] Move state handling to shared code --- src/{clj/grub/state.clj => cljx/grub/state.cljx} | 9 ++++----- src/test/grub/test/unit/state.clj | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) rename src/{clj/grub/state.clj => cljx/grub/state.cljx} (89%) diff --git a/src/clj/grub/state.clj b/src/cljx/grub/state.cljx similarity index 89% rename from src/clj/grub/state.clj rename to src/cljx/grub/state.cljx index 6fb1348..9a980b5 100644 --- a/src/clj/grub/state.clj +++ b/src/cljx/grub/state.cljx @@ -7,8 +7,8 @@ ;; Server state (def states (atom [])) -(defn make-server-agent - ([in out states] (make-server-agent in out states sync/empty-state)) +(defn make-agent + ([in out states] (make-agent in out states sync/empty-state)) ([in out states initial-client-state] (a/go-loop [client-state initial-client-state] (when-let [msg (! out (message/diff-msg diff hash))) - (do (println "Unknown message:" msg) - (recur client-state))))))) + (recur client-state)))))) ;; TODO: Remove watch, close up channels properly (defn sync-new-client! [>client client states))) + (make-agent client-events >client states))) (defn init [to-db grubs recipes] (reset! states (sync/initial-state grubs recipes)) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 186229f..c124d14 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -21,7 +21,7 @@ :hash (:hash (first states))} in (chan 1) out (chan 1)] - (state/make-server-agent in out states*) + (state/make-agent in out states*) (>!! in msg) (let [diff-response (!! in msg) (let [diff-response (!! in msg) (let [diff-response (!! in msg) (let [diff-response (!! in msg) (let [diff-response ( Date: Thu, 25 Sep 2014 23:23:29 +0300 Subject: [PATCH 29/40] Merge client and server state handling -- wip --- src/cljx/grub/state.cljx | 35 ++++++++++++++++++++++++++++--- src/test/grub/test/unit/state.clj | 10 ++++----- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/cljx/grub/state.cljx b/src/cljx/grub/state.cljx index 9a980b5..f4921e7 100644 --- a/src/cljx/grub/state.cljx +++ b/src/cljx/grub/state.cljx @@ -7,8 +7,8 @@ ;; Server state (def states (atom [])) -(defn make-agent - ([in out states] (make-agent in out states sync/empty-state)) +(defn make-server-agent + ([in out states] (make-server-agent in out states sync/empty-state)) ([in out states initial-client-state] (a/go-loop [client-state initial-client-state] (when-let [msg (! out (message/diff-msg diff hash))) (recur client-state)))))) +(defn make-client-agent + ([in out states] (make-client-agent in out states sync/empty-state)) + ([in out states initial-server-state] + (a/go-loop [server-state initial-server-state] + (when-let [msg (! out (message/full-sync state)) + (recur state)))) + + :full-sync + (let [state (:state msg)] + (reset! states [state]) + (recur state)) + + :new-state + (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) server-state)] + (>! out (message/diff-msg diff hash))) + (recur server-state)))))) + ;; TODO: Remove watch, close up channels properly (defn sync-new-client! [>client client states))) + (make-server-agent client-events >client states))) (defn init [to-db grubs recipes] (reset! states (sync/initial-state grubs recipes)) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index c124d14..186229f 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -21,7 +21,7 @@ :hash (:hash (first states))} in (chan 1) out (chan 1)] - (state/make-agent in out states*) + (state/make-server-agent in out states*) (>!! in msg) (let [diff-response (!! in msg) (let [diff-response (!! in msg) (let [diff-response (!! in msg) (let [diff-response (!! in msg) (let [diff-response ( Date: Fri, 26 Sep 2014 11:53:19 +0300 Subject: [PATCH 30/40] Move to shared state --- src/clj/grub/core.clj | 2 +- src/cljx/grub/shared_state.cljx | 85 +++++++++++++++++++++++++++++++ src/cljx/grub/state.cljx | 82 ----------------------------- src/test/grub/test/unit/state.clj | 2 +- 4 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 src/cljx/grub/shared_state.cljx delete mode 100644 src/cljx/grub/state.cljx diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index d892f09..5030d0f 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -2,7 +2,7 @@ (:require [grub.websocket :as ws] [grub.db :as db] [grub.test.integration.core :as integration-test] - [grub.state :as state] + [grub.shared-state :as state] [ring.middleware.file :as file] [ring.util.response :as resp] [compojure.core :refer [defroutes GET POST]] diff --git a/src/cljx/grub/shared_state.cljx b/src/cljx/grub/shared_state.cljx new file mode 100644 index 0000000..157e125 --- /dev/null +++ b/src/cljx/grub/shared_state.cljx @@ -0,0 +1,85 @@ +(ns grub.shared-state + (:require [grub.diff :as diff] + [grub.message :as message] + [grub.sync :as sync] + #+clj [clojure.core.async :as a :refer [! chan go]] + #+cljs [cljs.core.async :as a :refer [! chan]]) + #+cljs (:require-macros [cljs.core.async.macros :refer [go]])) + +;; Server state +(def states (atom [])) + +(defn make-server-agent + ([in out states] (make-server-agent in out states sync/empty-state)) + ([in out states initial-client-state] + (go (loop [client-state initial-client-state] + (when-let [msg (! out (message/diff-msg diff hash)) + (recur new-shadow)) + (let [state (sync/get-current-state @states)] + (>! out (message/full-sync state)) + (recur state)))) + + :full-sync + (let [state (sync/get-current-state @states)] + (>! out (message/full-sync state)) + (recur state)) + + :new-state + (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) client-state)] + (>! out (message/diff-msg diff hash)) + (recur client-state)) + (recur client-state))))))) + +;; (defn make-client-agent +;; ([in out states] (make-client-agent in out states sync/empty-state)) +;; ([in out states initial-server-state] +;; (a/go-loop [server-state initial-server-state] +;; (when-let [msg (! out (message/full-sync state)) +;; (recur state)))) + +;; :full-sync +;; (let [state (:state msg)] +;; (reset! states [state]) +;; (recur state)) + +;; :new-state +;; (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) server-state)] +;; (>! out (message/diff-msg diff hash))) +;; (recur server-state)))))) + +;; TODO: Remove watch, close up channels properly +(defn sync-new-client! [>client client states))) + +(defn init [to-db grubs recipes] + (reset! states (sync/initial-state grubs recipes)) + (add-watch states :to-db (fn [_ _ old-states new-states] + (a/put! to-db (sync/get-current-state new-states))))) diff --git a/src/cljx/grub/state.cljx b/src/cljx/grub/state.cljx deleted file mode 100644 index f4921e7..0000000 --- a/src/cljx/grub/state.cljx +++ /dev/null @@ -1,82 +0,0 @@ -(ns grub.state - (:require [grub.diff :as diff] - [grub.message :as message] - [grub.sync :as sync] - [clojure.core.async :as a :refer [! chan go]])) - -;; Server state -(def states (atom [])) - -(defn make-server-agent - ([in out states] (make-server-agent in out states sync/empty-state)) - ([in out states initial-client-state] - (a/go-loop [client-state initial-client-state] - (when-let [msg (! out (message/diff-msg diff hash)) - (recur new-shadow)) - (let [state (sync/get-current-state @states)] - (>! out (message/full-sync state)) - (recur state)))) - - :full-sync - (let [state (sync/get-current-state @states)] - (>! out (message/full-sync state)) - (recur state)) - - :new-state - (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) client-state)] - (>! out (message/diff-msg diff hash))) - (recur client-state)))))) - -(defn make-client-agent - ([in out states] (make-client-agent in out states sync/empty-state)) - ([in out states initial-server-state] - (a/go-loop [server-state initial-server-state] - (when-let [msg (! out (message/full-sync state)) - (recur state)))) - - :full-sync - (let [state (:state msg)] - (reset! states [state]) - (recur state)) - - :new-state - (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) server-state)] - (>! out (message/diff-msg diff hash))) - (recur server-state)))))) - -;; TODO: Remove watch, close up channels properly -(defn sync-new-client! [>client client states))) - -(defn init [to-db grubs recipes] - (reset! states (sync/initial-state grubs recipes)) - (add-watch states :to-db (fn [_ _ old-states new-states] - (a/put! to-db (sync/get-current-state new-states))))) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 186229f..8078d0e 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -1,5 +1,5 @@ (ns grub.test.unit.state - (:require [grub.state :as state] + (:require [grub.shared-state :as state] [clojure.test :refer :all] [hasch.core :as hasch] [clojure.core.async :as a :refer [!! chan go]])) From 15b9ec8927ce3730aa368170f9bcba0e303f6bbb Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Fri, 26 Sep 2014 20:51:23 +0300 Subject: [PATCH 31/40] Convert tests to Midje --- project.clj | 5 +- src/test/grub/test/unit/state.clj | 163 +++++++++++++----------------- 2 files changed, 76 insertions(+), 92 deletions(-) diff --git a/project.clj b/project.clj index 20f14f6..b19b011 100644 --- a/project.clj +++ b/project.clj @@ -19,7 +19,8 @@ [sablono "0.2.17"] [cljs-uuid "0.0.4"] [net.polyc0l0r/hasch "0.2.3"]] - :profiles {:uberjar {:aot :all}} + :profiles {:uberjar {:aot :all} + :dev {:dependencies [[midje "1.6.3"]]}} :min-lein-version "2.1.2" :plugins [[lein-cljsbuild "1.0.3"] [lein-ring "0.8.6"] @@ -42,7 +43,7 @@ :output-path "target/generated/cljs" :rules :cljs}]} :source-paths ["src/clj" "src/test"] - :test-paths ["spec/clj"] + :test-paths ["src/test"] :ring {:handler grub.core/app} :uberjar-name "grub-standalone.jar" :main grub.core) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 8078d0e..10ce5a3 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -1,6 +1,7 @@ (ns grub.test.unit.state (:require [grub.shared-state :as state] [clojure.test :refer :all] + [midje.sweet :refer :all] [hasch.core :as hasch] [clojure.core.async :as a :refer [!! chan go]])) @@ -10,33 +11,27 @@ :state s})) (into []))) -(deftest diff-no-server-changes - ;; Returns empty ACK diff with hash of current state - ;; when no server changes +(fact "Applies diff and returns empty diff when no server changes" (let [states (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}) + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}) states* (atom states) msg {:type :diff - :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - :hash (:hash (first states))} + :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + :hash (:hash (first states))} in (chan 1) out (chan 1)] (state/make-server-agent in out states*) (>!! in msg) - (let [diff-response ( (hashed-states + {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} + {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}) + response => {:type :diff + :diff {:grubs {:deleted #{}, :updated nil} + :recipes {:deleted #{}, :updated nil}} + :hash (:hash (last @states*))}))) -(deftest diff-server-changes - ;; Returns diff with changes when server has changed - ;; Client state fetched from history +(fact "Applies diff and returns changes when server has changed" (let [states (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -44,33 +39,29 @@ :recipes {}}) states* (atom states) msg {:type :diff - :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - :hash (:hash (first states))} + :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} + :hash (:hash (first states))} in (chan 1) out (chan 1)] (state/make-server-agent in out states*) (>!! in msg) - (let [diff-response ( (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}} + {:grubs {"1" {:text "2 apples" :completed true} + "2" {:text "3 onions" :completed false}} + :recipes {}}) + response => {:type :diff + :diff {:grubs {:deleted #{} + :updated {"2" {:completed false, :text "3 onions"}}} + :recipes {:deleted #{}, :updated nil}} + :hash (hasch/uuid {:grubs {"1" {:text "2 apples" :completed true}} + :recipes {}})}))) -(deftest diff-client-out-of-sync - ;; Returns full sync if client state not found - ;; in history +(fact "Force full sync if client is out of sync" (let [states (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -78,28 +69,25 @@ :recipes {}}) states* (atom states) msg {:type :diff - :diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}} - :hash (:hash {:grubs {"0" {:text "milk" :completed false}} - :recipes {}})} + :diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}} + :hash (:hash {:grubs {"0" {:text "milk" :completed false}} + :recipes {}})} in (chan 1) out (chan 1)] (state/make-server-agent in out states*) (>!! in msg) - (let [diff-response ( (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}}) + response => {:type :full-sync + :state {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}}}))) -(deftest full-sync-request - ;; Returns full sync if client requests it +(fact "Full sync if client requests it" (let [states (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -111,21 +99,18 @@ out (chan 1)] (state/make-server-agent in out states*) (>!! in msg) - (let [diff-response ( (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}}) + response => {:type :full-sync + :state {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}}}))) -(deftest new-state - ;; Passes diff with new state to client +(fact "Passes diffs of new states to client" (let [states (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -143,21 +128,19 @@ out (chan 1)] (state/make-server-agent in out states* client-state) (>!! in msg) - (let [diff-response ( (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false} + "3" {:text "milk" :completed false}} + :recipes {}}) + response => {:type :diff + :diff {:grubs {:deleted #{} + :updated {"2" {:text "3 onions" :completed false} + "3" {:text "milk" :completed false}}} + :recipes {:deleted #{}, :updated nil}} + :hash (hasch/uuid client-state)}))) From e3e763469af78507f2ff537f3c3f9153c45f39ca Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Fri, 26 Sep 2014 21:04:17 +0300 Subject: [PATCH 32/40] Clean up tests slightly --- src/test/grub/test/unit/state.clj | 96 +++++++++++++++---------------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 10ce5a3..416ae69 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -11,49 +11,50 @@ :state s})) (into []))) +(defn states-atom [& states] + (atom (apply hashed-states states))) + (fact "Applies diff and returns empty diff when no server changes" - (let [states (hashed-states + (let [states (states-atom {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}) - states* (atom states) msg {:type :diff :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - :hash (:hash (first states))} + :hash (:hash (first @states))} in (chan 1) out (chan 1)] - (state/make-server-agent in out states*) + (state/make-server-agent in out states) (>!! in msg) (let [response ( (hashed-states - {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} - {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}) + @states => (hashed-states + {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} + {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}) response => {:type :diff :diff {:grubs {:deleted #{}, :updated nil} :recipes {:deleted #{}, :updated nil}} - :hash (:hash (last @states*))}))) + :hash (:hash (last @states))}))) (fact "Applies diff and returns changes when server has changed" - (let [states (hashed-states + (let [states (states-atom {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} "2" {:text "3 onions" :completed false}} :recipes {}}) - states* (atom states) msg {:type :diff :diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}} - :hash (:hash (first states))} + :hash (:hash (first @states))} in (chan 1) out (chan 1)] - (state/make-server-agent in out states*) + (state/make-server-agent in out states) (>!! in msg) (let [response ( (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - {:grubs {"1" {:text "2 apples" :completed false} - "2" {:text "3 onions" :completed false}} - :recipes {}} - {:grubs {"1" {:text "2 apples" :completed true} - "2" {:text "3 onions" :completed false}} - :recipes {}}) + @states => (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}} + {:grubs {"1" {:text "2 apples" :completed true} + "2" {:text "3 onions" :completed false}} + :recipes {}}) response => {:type :diff :diff {:grubs {:deleted #{} :updated {"2" {:completed false, :text "3 onions"}}} @@ -62,56 +63,54 @@ :recipes {}})}))) (fact "Force full sync if client is out of sync" - (let [states (hashed-states + (let [states (states-atom {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} "2" {:text "3 onions" :completed false}} :recipes {}}) - states* (atom states) msg {:type :diff :diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}} :hash (:hash {:grubs {"0" {:text "milk" :completed false}} :recipes {}})} in (chan 1) out (chan 1)] - (state/make-server-agent in out states*) + (state/make-server-agent in out states) (>!! in msg) (let [response ( (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - {:grubs {"1" {:text "2 apples" :completed false} - "2" {:text "3 onions" :completed false}} - :recipes {}}) + @states => (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}}) response => {:type :full-sync :state {:grubs {"1" {:text "2 apples" :completed false} "2" {:text "3 onions" :completed false}} :recipes {}}}))) (fact "Full sync if client requests it" - (let [states (hashed-states + (let [states (states-atom {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} "2" {:text "3 onions" :completed false}} :recipes {}}) - states* (atom states) msg {:type :full-sync} in (chan 1) out (chan 1)] - (state/make-server-agent in out states*) + (state/make-server-agent in out states) (>!! in msg) (let [response ( (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - {:grubs {"1" {:text "2 apples" :completed false} - "2" {:text "3 onions" :completed false}} - :recipes {}}) + @states => (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}}) response => {:type :full-sync :state {:grubs {"1" {:text "2 apples" :completed false} "2" {:text "3 onions" :completed false}} :recipes {}}}))) (fact "Passes diffs of new states to client" - (let [states (hashed-states + (let [states (states-atom {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} "2" {:text "3 onions" :completed false}} @@ -120,24 +119,23 @@ "2" {:text "3 onions" :completed false} "3" {:text "milk" :completed false}} :recipes {}}) - states* (atom states) client-state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} msg {:type :new-state - :new-states states} + :new-states @states} in (chan 1) out (chan 1)] - (state/make-server-agent in out states* client-state) + (state/make-server-agent in out states client-state) (>!! in msg) (let [response ( (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - {:grubs {"1" {:text "2 apples" :completed false} - "2" {:text "3 onions" :completed false}} - :recipes {}} - {:grubs {"1" {:text "2 apples" :completed false} - "2" {:text "3 onions" :completed false} - "3" {:text "milk" :completed false}} - :recipes {}}) + @states => (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false}} + :recipes {}} + {:grubs {"1" {:text "2 apples" :completed false} + "2" {:text "3 onions" :completed false} + "3" {:text "milk" :completed false}} + :recipes {}}) response => {:type :diff :diff {:grubs {:deleted #{} :updated {"2" {:text "3 onions" :completed false} From d174b2236e6507a521eed4975801abb3523825c4 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sun, 28 Sep 2014 09:35:20 +0300 Subject: [PATCH 33/40] Integration test for client server sync --- src/cljx/grub/shared_state.cljx | 87 ++++++++++++++++--------------- src/test/grub/test/unit/state.clj | 62 ++++++++++++++++++++++ 2 files changed, 107 insertions(+), 42 deletions(-) diff --git a/src/cljx/grub/shared_state.cljx b/src/cljx/grub/shared_state.cljx index 157e125..37a5f5c 100644 --- a/src/cljx/grub/shared_state.cljx +++ b/src/cljx/grub/shared_state.cljx @@ -7,12 +7,12 @@ #+cljs (:require-macros [cljs.core.async.macros :refer [go]])) ;; Server state -(def states (atom [])) +;; (def states (atom [])) (defn make-server-agent ([in out states] (make-server-agent in out states sync/empty-state)) - ([in out states initial-client-state] - (go (loop [client-state initial-client-state] + ([in out states initial-client-shadow] + (go (loop [client-shadow initial-client-shadow] (when-let [msg (! out (message/diff-msg diff hash)) - (recur client-state)) - (recur client-state))))))) + (recur client-shadow)) + (recur client-shadow))))))) -;; (defn make-client-agent -;; ([in out states] (make-client-agent in out states sync/empty-state)) -;; ([in out states initial-server-state] -;; (a/go-loop [server-state initial-server-state] -;; (when-let [msg (! out (message/full-sync state)) -;; (recur state)))) +(defn make-client-agent + ([in out states] (make-client-agent in out states sync/empty-state)) + ([in out states initial-server-shadow] + (a/go-loop [server-shadow initial-server-shadow] + (when-let [msg (! out (message/full-sync state)) + (recur state)))) -;; :full-sync -;; (let [state (:state msg)] -;; (reset! states [state]) -;; (recur state)) + :full-sync + (let [state (:state msg)] + (reset! states [state]) + (recur state)) -;; :new-state -;; (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) server-state)] -;; (>! out (message/diff-msg diff hash))) -;; (recur server-state)))))) + :new-state + (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) server-shadow)] + (>! out (message/diff-msg diff hash)) + (recur server-shadow)) + (recur server-shadow)))))) ;; TODO: Remove watch, close up channels properly (defn sync-new-client! [>client client states))) + nil) + ;; (let [client-id (java.util.UUID/randomUUID) + ;; state-changes (chan) + ;; state-change-events (a/map< (fn [s] {:type :new-state :new-states s}) state-changes) + ;; client-events (chan)] + ;; (add-watch states client-id (fn [_ _ _ new-states] (a/put! state-changes new-states))) + ;; (a/pipe (a/merge [client states))) (defn init [to-db grubs recipes] - (reset! states (sync/initial-state grubs recipes)) - (add-watch states :to-db (fn [_ _ old-states new-states] - (a/put! to-db (sync/get-current-state new-states))))) +nil) +;; (reset! states (sync/initial-state grubs recipes)) +;; (add-watch states :to-db (fn [_ _ old-states new-states] +;; (a/put! to-db (sync/get-current-state new-states))))) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 416ae69..c7e7994 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -142,3 +142,65 @@ "3" {:text "milk" :completed false}}} :recipes {:deleted #{}, :updated nil}} :hash (hasch/uuid client-state)}))) + +(fact "Client-only changes synced with server" + (let [client-shadow {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}} + client-states (states-atom + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}) + server-shadow {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + server-states (states-atom server-shadow) + client-in (chan) + client-out (chan) + server-in (chan) + server-out (chan) + client-state-changes (chan 1) + msg {:type :new-state + :new-states (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}})}] + (a/pipe client-out server-in) + (a/pipe server-out client-in) + (state/make-client-agent client-in client-out client-states server-shadow) + (state/make-server-agent server-in server-out server-states client-shadow) + (add-watch client-states :test (fn [_ _ _ new-states] (a/put! client-state-changes new-states))) + (>!! client-in msg) + ( {:grubs {"1" {:completed true, :text "2 apples"}} + :recipes {}} + (:state (last @server-states)) => {:grubs {"1" {:completed true, :text "2 apples"}} + :recipes {}})) + +(fact "Client and server changes synced" + (let [client-shadow {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + client-states (states-atom + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}) + server-shadow {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + server-states (states-atom + server-shadow + {:grubs {"1" {:text "4 apples" :completed false}} :recipes {}}) + client-in (chan) + client-out (chan) + server-in (chan) + server-out (chan) + msg {:type :new-state + :new-states (hashed-states + {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} + {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}})} + client-state-changes (chan 1)] + (a/pipe client-out server-in) + (a/pipe server-out client-in) + (state/make-client-agent client-in client-out client-states server-shadow) + (state/make-server-agent server-in server-out server-states client-shadow) + (add-watch client-states :test (fn [_ _ _ new-states] (a/put! client-state-changes new-states))) + (>!! client-in msg) + ( (hashed-states + {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} + {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}} + {:grubs {"1" {:completed true, :text "4 apples"}}, :recipes {}}) + @server-states => (hashed-states + {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} + {:grubs {"1" {:completed false, :text "4 apples"}}, :recipes {}} + {:grubs {"1" {:completed true, :text "4 apples"}}, :recipes {}}))) From 48ba2c5449d4ceee81646a8d864cbada9a63973b Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 4 Oct 2014 22:38:32 +0300 Subject: [PATCH 34/40] Possibly fully working (poorly tested) --- project.clj | 2 +- src/clj/grub/core.clj | 6 +- src/cljs/grub/core.cljs | 12 ++- src/cljs/grub/state.cljs | 57 ------------- src/cljs/grub/websocket.cljs | 1 + src/cljx/grub/common_state.cljx | 23 ------ src/cljx/grub/shared_state.cljx | 88 -------------------- src/cljx/grub/state.cljx | 103 ++++++++++++++++++++++++ src/cljx/grub/sync.cljx | 9 ++- src/test/grub/test/integration/core.clj | 2 +- src/test/grub/test/unit/state.clj | 12 +-- 11 files changed, 126 insertions(+), 189 deletions(-) delete mode 100644 src/cljs/grub/state.cljs delete mode 100644 src/cljx/grub/common_state.cljx delete mode 100644 src/cljx/grub/shared_state.cljx create mode 100644 src/cljx/grub/state.cljx diff --git a/project.clj b/project.clj index b19b011..2d08acc 100644 --- a/project.clj +++ b/project.clj @@ -42,7 +42,7 @@ {:source-paths ["src/cljx"] :output-path "target/generated/cljs" :rules :cljs}]} - :source-paths ["src/clj" "src/test"] + :source-paths ["src/clj" "src/test" "target/classes"] :test-paths ["src/test"] :ring {:handler grub.core/app} :uberjar-name "grub-standalone.jar" diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index 5030d0f..fedd41c 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -2,7 +2,7 @@ (:require [grub.websocket :as ws] [grub.db :as db] [grub.test.integration.core :as integration-test] - [grub.shared-state :as state] + [grub.state :as state] [ring.middleware.file :as file] [ring.util.response :as resp] [compojure.core :refer [defroutes GET POST]] @@ -73,14 +73,14 @@ (reset! index-page prod-index-page) (let [to-db (chan)] (db/connect-production-database to-db mongo-url) - (state/init to-db (db/get-current-grubs) (db/get-current-recipes)) + (state/init-server to-db (db/get-current-grubs) (db/get-current-recipes)) (println "Starting production server on localhost:" port) (start-server port))) (defn start-development-server [{:keys [port]}] (let [to-db (chan)] (db/connect-development-database to-db) - (state/init to-db (db/get-current-grubs) (db/get-current-recipes)) + (state/init-server to-db (db/get-current-grubs) (db/get-current-recipes)) (println "Starting development server on localhost:" port) (start-server port))) diff --git a/src/cljs/grub/core.cljs b/src/cljs/grub/core.cljs index 14489d2..f6e73f4 100644 --- a/src/cljs/grub/core.cljs +++ b/src/cljs/grub/core.cljs @@ -5,14 +5,12 @@ [cljs.core.async :as a :refer [! chan]]) (:require-macros [grub.macros :refer [log logs]])) -(defn connect-to-server [reset? state-changes] - (let [to-remote (chan) +(defn init-app [] + (let [current-state (atom state/empty-state) + state-changes (view/render-app current-state) + to-remote (chan) from-remote (chan)] (ws/connect-client! to-remote from-remote) - (state/sync-state! from-remote to-remote reset? state-changes))) - -(defn init-app [] - (let [state-changes (view/render-app state/state)] - (connect-to-server true state-changes))) + (state/init-client from-remote to-remote state-changes current-state))) (init-app) diff --git a/src/cljs/grub/state.cljs b/src/cljs/grub/state.cljs deleted file mode 100644 index ef33769..0000000 --- a/src/cljs/grub/state.cljs +++ /dev/null @@ -1,57 +0,0 @@ -(ns grub.state - (:require [grub.diff :as diff] - [grub.common-state :as cs] - [grub.message :as message] - [grub.sync :as sync] - [cljs.core.async :as a :refer [! chan]] - [hasch.core :as hasch]) - (:require-macros [grub.macros :refer [log logs]] - [cljs.core.async.macros :refer [go go-loop]])) - -(def state (atom sync/empty-state)) -(def server-state (atom sync/empty-state)) - -(def unacked-states (atom {})) - -(defn get-server-state [hash] - (if (= (hasch/uuid @server-state) hash) - @server-state - (get @unacked-states hash))) - -(defn send-state-changes-to-server! [state-changes from] - (go-loop [] - (when-let [current-state ( full sync") - (a/put! from message/full-sync-request))))) - (do (log "Could not find server state locally --> full sync") - (a/put! from message/full-sync-request))) - :full-sync (do - (logs "Full sync") - (reset! unacked-states {}) - (reset! server-state (:state msg)) - (reset! state (:state msg))) - (logs "Invalid msg:" msg)) - (recur)) - (remove-watch state :state)))) - -(defn sync-state! [to from reset? state-changes] - (send-state-changes-to-server! state-changes from) - (handle-received-changes! to from) - (a/put! from (if reset? message/full-sync-request (cs/diff-states @server-state @state)))) diff --git a/src/cljs/grub/websocket.cljs b/src/cljs/grub/websocket.cljs index 1fb0968..2fb857e 100644 --- a/src/cljs/grub/websocket.cljs +++ b/src/cljs/grub/websocket.cljs @@ -14,6 +14,7 @@ (defn send-pending-msg [websocket] (when (and (.isOpen websocket) (not (nil? @pending-msg))) + (logs "Send message:" @pending-msg) (.send websocket (pr-str @pending-msg)) (reset! pending-msg nil))) diff --git a/src/cljx/grub/common_state.cljx b/src/cljx/grub/common_state.cljx deleted file mode 100644 index aaa2537..0000000 --- a/src/cljx/grub/common_state.cljx +++ /dev/null @@ -1,23 +0,0 @@ -(ns grub.common-state - (:require [grub.diff :as diff] - [hasch.core :as hasch])) - -(def empty-state {:grubs {} :recipes {}}) - -(def complete-sync-request {:type :complete}) -(defn complete-sync-response [state] - {:type :complete - :state state}) - -(defn diff-msg [diff hash shadow-hash] - {:type :diff - :diff diff - :hash hash - :shadow-hash shadow-hash}) - -(defn diff-states [shadow state] - (let [diff (diff/diff-states shadow state) - hash (hasch/uuid state) - shadow-hash (hasch/uuid shadow) - msg (diff-msg diff hash shadow-hash)] - msg)) diff --git a/src/cljx/grub/shared_state.cljx b/src/cljx/grub/shared_state.cljx deleted file mode 100644 index 37a5f5c..0000000 --- a/src/cljx/grub/shared_state.cljx +++ /dev/null @@ -1,88 +0,0 @@ -(ns grub.shared-state - (:require [grub.diff :as diff] - [grub.message :as message] - [grub.sync :as sync] - #+clj [clojure.core.async :as a :refer [! chan go]] - #+cljs [cljs.core.async :as a :refer [! chan]]) - #+cljs (:require-macros [cljs.core.async.macros :refer [go]])) - -;; Server state -;; (def states (atom [])) - -(defn make-server-agent - ([in out states] (make-server-agent in out states sync/empty-state)) - ([in out states initial-client-shadow] - (go (loop [client-shadow initial-client-shadow] - (when-let [msg (! out (message/diff-msg diff hash)) - (recur new-shadow)) - (let [state (sync/get-current-state @states)] - (>! out (message/full-sync state)) - (recur state)))) - - :full-sync - (let [state (sync/get-current-state @states)] - (>! out (message/full-sync state)) - (recur state)) - - :new-state - (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) client-shadow)] - (>! out (message/diff-msg diff hash)) - (recur client-shadow)) - (recur client-shadow))))))) - -(defn make-client-agent - ([in out states] (make-client-agent in out states sync/empty-state)) - ([in out states initial-server-shadow] - (a/go-loop [server-shadow initial-server-shadow] - (when-let [msg (! out (message/full-sync state)) - (recur state)))) - - :full-sync - (let [state (:state msg)] - (reset! states [state]) - (recur state)) - - :new-state - (let [{:keys [diff hash]} (sync/diff-states (:new-states msg) server-shadow)] - (>! out (message/diff-msg diff hash)) - (recur server-shadow)) - (recur server-shadow)))))) - -;; TODO: Remove watch, close up channels properly -(defn sync-new-client! [>client client states))) - -(defn init [to-db grubs recipes] -nil) -;; (reset! states (sync/initial-state grubs recipes)) -;; (add-watch states :to-db (fn [_ _ old-states new-states] -;; (a/put! to-db (sync/get-current-state new-states))))) diff --git a/src/cljx/grub/state.cljx b/src/cljx/grub/state.cljx new file mode 100644 index 0000000..671f875 --- /dev/null +++ b/src/cljx/grub/state.cljx @@ -0,0 +1,103 @@ +(ns grub.state + (:require [grub.diff :as diff] + [grub.message :as message] + [grub.sync :as sync] + #+clj [clojure.core.async :as a :refer [! chan go]] + #+cljs [cljs.core.async :as a :refer [! chan]]) + #+cljs (:require-macros [grub.macros :refer [log logs]] + [cljs.core.async.macros :refer [go]])) + +(defn make-agent + ([client? in out states] (make-agent client? in out states sync/empty-state)) + ([client? in out states initial-shadow] + (go (loop [shadow initial-shadow] + (when-let [msg (! out (message/diff-msg diff hash))) ;; HERE + (recur new-shadow)) + (if client? + (do (>! out message/full-sync-request) + (recur shadow)) + (let [state (sync/get-current-state states*)] + (>! out (message/full-sync state)) + (recur state))))) + + :full-sync + (if client? + (let [state (:state msg)] + #+cljs (logs "received full sync") + (reset! states (sync/new-state state)) + (recur state)) + (let [state (sync/get-current-state @states)] + #+clj (println "sending full sync") + (>! out (message/full-sync state)) ;; HERE + (recur state))) + + :new-state + (let [{:keys [diff hash]} (sync/diff-states (:state msg) shadow)] + #+cljs (logs "new state") + #+clj (println "new state") + (if client? + (when-not (sync/empty-diff? diff) + (>! out (message/diff-msg diff hash))) + (>! out (message/diff-msg diff hash))) + (recur shadow)) + (recur shadow))))))) + +(defn make-server-agent + ([in out states] (make-agent false in out states)) + ([in out states initial-shadow] (make-agent false in out states initial-shadow))) + +(defn make-client-agent + ([in out states] (make-agent true in out states)) + ([in out states initial-shadow] (make-agent true in out states initial-shadow))) + +(def states (atom [])) +(def empty-state sync/empty-state) + +;; TODO: Remove watch, close up channels properly +#+clj +(defn sync-new-client! [>client ! client-events val) + (recur)) + (do #+clj (println "client disconnected, clean up") + (remove-watch states client-id) + (a/close! client states))) + +(defn init-server [to-db grubs recipes] + (reset! states (sync/initial-state grubs recipes)) + (add-watch states :to-db (fn [_ _ old-states new-states] + (a/put! to-db (sync/get-current-state new-states))))) + +#+cljs +(defn init-client [in out state-changes current-state] + (reset! states (sync/initial-state {} {})) + (add-watch states :render (fn [_ _ _ new-states] + (let [new-state (sync/get-current-state new-states)] + (reset! current-state new-state)))) + (a/pipe (a/map< (fn [s] + (swap! states sync/add-history-state s) + {:type :new-state :state s}) state-changes) in) + (make-client-agent in out states) + (a/put! out message/full-sync-request)) diff --git a/src/cljx/grub/sync.cljx b/src/cljx/grub/sync.cljx index 9c646df..033449b 100644 --- a/src/cljx/grub/sync.cljx +++ b/src/cljx/grub/sync.cljx @@ -10,6 +10,10 @@ :recipes (util/map-by-key :id recipes)}] [{:state state :hash (hasch/uuid state)}])) +(defn new-state [state] + [{:hash (hasch/uuid state) + :state state}]) + (defn get-current-state [states] (:state (last states))) @@ -24,7 +28,8 @@ (conj states {:hash new-hash :state new-state})))) (defn diff-states [states shadow] - (let [state (get-current-state states)] + (let [state states;(get-current-state states) + ] {:hash (hasch/uuid shadow) :diff (diff/diff-states shadow state)})) @@ -32,3 +37,5 @@ (let [new-state (diff/patch-state (get-current-state states) diff)] (add-history-state states new-state))) +(defn empty-diff? [diff] + (= diff {:recipes {:deleted #{}, :updated nil}, :grubs {:deleted #{}, :updated nil}})) diff --git a/src/test/grub/test/integration/core.clj b/src/test/grub/test/integration/core.clj index af172f3..cc12d05 100644 --- a/src/test/grub/test/integration/core.clj +++ b/src/test/grub/test/integration/core.clj @@ -75,7 +75,7 @@ (defn start-db-and-websocket-server! [] (let [to-db (chan)] (db/connect-and-handle-events to-db "grub-integration-test") - (state/init to-db (db/get-current-grubs) (db/get-current-recipes)))) + (state/init-server to-db (db/get-current-grubs) (db/get-current-recipes)))) (defn run [] (println "Starting integration test") diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index c7e7994..302ec8f 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -1,5 +1,5 @@ (ns grub.test.unit.state - (:require [grub.shared-state :as state] + (:require [grub.state :as state] [clojure.test :refer :all] [midje.sweet :refer :all] [hasch.core :as hasch] @@ -121,7 +121,7 @@ :recipes {}}) client-state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} msg {:type :new-state - :new-states @states} + :state (:state (last @states))} in (chan 1) out (chan 1)] (state/make-server-agent in out states client-state) @@ -156,9 +156,7 @@ server-out (chan) client-state-changes (chan 1) msg {:type :new-state - :new-states (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}})}] + :state {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}}] (a/pipe client-out server-in) (a/pipe server-out client-in) (state/make-client-agent client-in client-out client-states server-shadow) @@ -185,9 +183,7 @@ server-in (chan) server-out (chan) msg {:type :new-state - :new-states (hashed-states - {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} - {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}})} + :state {:grubs {"1" {:text "2 apples" :completed true}} :recipes {}}} client-state-changes (chan 1)] (a/pipe client-out server-in) (a/pipe server-out client-in) From 86bfdf80e4b60dd3792c3e9a59b16cdf6153be4c Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 4 Oct 2014 23:20:55 +0300 Subject: [PATCH 35/40] Simplify MongoDB code by just storing whole state - Every time it receives a state, it blows away the existing one and puts in the new one. - There's a non-zero probability of losing the entire state if the server fails after blowing away the previous state and before inserting the new. --- src/clj/grub/core.clj | 4 +- src/clj/grub/db.clj | 58 ++++++------------------- src/cljx/grub/state.cljx | 6 +-- src/test/grub/test/integration/core.clj | 4 +- 4 files changed, 21 insertions(+), 51 deletions(-) diff --git a/src/clj/grub/core.clj b/src/clj/grub/core.clj index fedd41c..f6a9665 100644 --- a/src/clj/grub/core.clj +++ b/src/clj/grub/core.clj @@ -73,14 +73,14 @@ (reset! index-page prod-index-page) (let [to-db (chan)] (db/connect-production-database to-db mongo-url) - (state/init-server to-db (db/get-current-grubs) (db/get-current-recipes)) + (state/init-server to-db (db/get-current-state)) (println "Starting production server on localhost:" port) (start-server port))) (defn start-development-server [{:keys [port]}] (let [to-db (chan)] (db/connect-development-database to-db) - (state/init-server to-db (db/get-current-grubs) (db/get-current-recipes)) + (state/init-server to-db (db/get-current-state)) (println "Starting development server on localhost:" port) (start-server port))) diff --git a/src/clj/grub/db.clj b/src/clj/grub/db.clj index 90cdac5..30765d5 100644 --- a/src/clj/grub/db.clj +++ b/src/clj/grub/db.clj @@ -1,5 +1,6 @@ (ns grub.db (:require [grub.util :as util] + [grub.sync :as sync] [monger.core :as m] [monger.collection :as mc] [monger.operators :as mo] @@ -7,54 +8,22 @@ (def conn (atom nil)) (def db (atom nil)) -(def grub-collection "grubs") -(def recipe-collection "recipes") +(def collection "grub-lists") (def production-db "grub") (def development-db "grub-dev") -(defn clear-grubs [] - (mc/drop @db grub-collection)) - -(defn clear-recipes [] - (mc/drop @db recipe-collection)) - (defn clear-all [] - (clear-grubs) - (clear-recipes)) + (mc/drop @db collection)) -(defn update-db! [{:keys [grubs recipes]}] - (let [deleted-grubs (:deleted grubs) - updated-grubs (->> (:updated grubs) - (seq) - (map (fn [[k v]] - (-> v - (dissoc :id) - (assoc :_id k))))) - deleted-recipes (:deleted recipes) - updated-recipes (->> (:updated recipes) - (seq) - (map (fn [[k v]] - (-> v - (dissoc :id) - (assoc :_id k)))))] - (doseq [g deleted-grubs] - (mc/remove-by-id @db grub-collection g)) - (doseq [g updated-grubs] - (mc/update-by-id @db grub-collection (:_id g) g {:upsert true})) - (doseq [r deleted-recipes] - (mc/remove-by-id @db recipe-collection r)) - (doseq [r updated-recipes] - (mc/update-by-id @db recipe-collection (:_id r) r {:upsert true})))) +(defn update-db! [state] + (mc/drop @db collection) + (mc/insert @db collection state)) -(defn get-current-grubs [] - (->> (mc/find-maps @db grub-collection) - (sort-by :_id) - (map #(clojure.set/rename-keys % {:_id :id})))) - -(defn get-current-recipes [] - (->> (mc/find-maps @db recipe-collection) - (sort-by :_id) - (map #(clojure.set/rename-keys % {:_id :id})))) +(defn get-current-state [] + (let [state (first (mc/find-maps @db collection))] + (if state + (dissoc state :_id) + sync/empty-state))) (defn connect! [db-name mongo-url] (if mongo-url @@ -65,8 +34,9 @@ (defn connect-and-handle-events [to-db db-name & [mongo-url]] (a/go-loop [] - (if-let [diff (client client states))) -(defn init-server [to-db grubs recipes] - (reset! states (sync/initial-state grubs recipes)) +#+clj +(defn init-server [to-db initial-state] + (reset! states (sync/new-state initial-state)) (add-watch states :to-db (fn [_ _ old-states new-states] (a/put! to-db (sync/get-current-state new-states))))) diff --git a/src/test/grub/test/integration/core.clj b/src/test/grub/test/integration/core.clj index cc12d05..3916f34 100644 --- a/src/test/grub/test/integration/core.clj +++ b/src/test/grub/test/integration/core.clj @@ -35,7 +35,7 @@ (doseq [grub grubs] (test/is (taxi/find-element driver {:value grub}) "Previously added grubs should be loaded on refresh"))) - (db/clear-grubs)) + (db/clear-all)) (defn test-added-grubs-sync [url driver1 driver2] (taxi/to driver1 url) @@ -75,7 +75,7 @@ (defn start-db-and-websocket-server! [] (let [to-db (chan)] (db/connect-and-handle-events to-db "grub-integration-test") - (state/init-server to-db (db/get-current-grubs) (db/get-current-recipes)))) + (state/init-server to-db (db/get-current-state)))) (defn run [] (println "Starting integration test") From 76074e5b5c740c210a45b7d4dc210e7c7736400b Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sat, 4 Oct 2014 23:40:57 +0300 Subject: [PATCH 36/40] Optimization: don't reset grub/recipe edit state if unchanged --- src/cljs/grub/view/grub.cljs | 2 +- src/cljs/grub/view/recipe.cljs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cljs/grub/view/grub.cljs b/src/cljs/grub/view/grub.cljs index f51ee8c..d0822f3 100644 --- a/src/cljs/grub/view/grub.cljs +++ b/src/cljs/grub/view/grub.cljs @@ -36,7 +36,7 @@ [:editing :waiting] (let [grub (om/get-props owner)] (om/transact! grub #(assoc % :text (om/get-state owner :grub-text)))) nil) - (om/set-state! owner :edit-state next))) + (when-not (= current next) (om/set-state! owner :edit-state next)))) (defn view [{:keys [id text completed] :as grub} owner {:keys [remove-ch]}] (reify diff --git a/src/cljs/grub/view/recipe.cljs b/src/cljs/grub/view/recipe.cljs index 63c2bd5..2f0bab8 100644 --- a/src/cljs/grub/view/recipe.cljs +++ b/src/cljs/grub/view/recipe.cljs @@ -40,7 +40,7 @@ grubs (om/get-state owner :grubs)] (om/transact! recipe #(assoc % :name name :grubs grubs))) nil) - (om/set-state! owner :edit-state next))) + (when-not (= current next) (om/set-state! owner :edit-state next)))) (defn num-newlines [str] (count (re-seq #"\n" str))) From 8850552838b647b6a486b0abbb529bc4d92c8d9d Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Sun, 5 Oct 2014 00:11:26 +0300 Subject: [PATCH 37/40] Send empty diffs as ACKs on both client/server On receiving ACK, update "current server state". --- src/cljx/grub/state.cljx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cljx/grub/state.cljx b/src/cljx/grub/state.cljx index f3a8f8b..c2f6d31 100644 --- a/src/cljx/grub/state.cljx +++ b/src/cljx/grub/state.cljx @@ -22,8 +22,10 @@ (let [new-states (sync/apply-diff states* (:diff msg)) new-shadow (diff/patch-state shadow (:diff msg)) {:keys [diff hash]} (sync/diff-states (sync/get-current-state new-states) new-shadow)] - (reset! states new-states) - (when-not client? (>! out (message/diff-msg diff hash))) ;; HERE + (when-not (= states* new-states) + (reset! states new-states)) + (when-not (sync/empty-diff? diff) + (>! out (message/diff-msg diff hash))) (recur new-shadow)) (if client? (do (>! out message/full-sync-request) @@ -47,9 +49,7 @@ (let [{:keys [diff hash]} (sync/diff-states (:state msg) shadow)] #+cljs (logs "new state") #+clj (println "new state") - (if client? - (when-not (sync/empty-diff? diff) - (>! out (message/diff-msg diff hash))) + (when-not (sync/empty-diff? diff) (>! out (message/diff-msg diff hash))) (recur shadow)) (recur shadow))))))) From 821ba079e0fbb1fb7815a2d049fc691926bf4306 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Mon, 6 Oct 2014 17:58:46 +0300 Subject: [PATCH 38/40] Add timeout to make tests work even if channel pull blocks --- src/test/grub/test/unit/state.clj | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/test/grub/test/unit/state.clj b/src/test/grub/test/unit/state.clj index 302ec8f..e241c1b 100644 --- a/src/test/grub/test/unit/state.clj +++ b/src/test/grub/test/unit/state.clj @@ -14,7 +14,11 @@ (defn states-atom [& states] (atom (apply hashed-states states))) -(fact "Applies diff and returns empty diff when no server changes" +(defn !! in msg) - (let [response ( (hashed-states {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}) @@ -46,7 +50,7 @@ out (chan 1)] (state/make-server-agent in out states) (>!! in msg) - (let [response ( (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -76,7 +80,7 @@ out (chan 1)] (state/make-server-agent in out states) (>!! in msg) - (let [response ( (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -98,7 +102,7 @@ out (chan 1)] (state/make-server-agent in out states) (>!! in msg) - (let [response ( (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -126,7 +130,7 @@ out (chan 1)] (state/make-server-agent in out states client-state) (>!! in msg) - (let [response ( (hashed-states {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}} {:grubs {"1" {:text "2 apples" :completed false} @@ -163,7 +167,7 @@ (state/make-server-agent server-in server-out server-states client-shadow) (add-watch client-states :test (fn [_ _ _ new-states] (a/put! client-state-changes new-states))) (>!! client-in msg) - ( {:grubs {"1" {:completed true, :text "2 apples"}} :recipes {}} (:state (last @server-states)) => {:grubs {"1" {:completed true, :text "2 apples"}} @@ -191,7 +195,7 @@ (state/make-server-agent server-in server-out server-states client-shadow) (add-watch client-states :test (fn [_ _ _ new-states] (a/put! client-state-changes new-states))) (>!! client-in msg) - ( (hashed-states {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}} {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}} From 7126a6b7b3562cf10b57e7c2b967bed97231d743 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Mon, 6 Oct 2014 19:18:08 +0300 Subject: [PATCH 39/40] Server assumes client gets messages sent --- src/cljx/grub/state.cljx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cljx/grub/state.cljx b/src/cljx/grub/state.cljx index c2f6d31..a5dabd3 100644 --- a/src/cljx/grub/state.cljx +++ b/src/cljx/grub/state.cljx @@ -24,9 +24,11 @@ {:keys [diff hash]} (sync/diff-states (sync/get-current-state new-states) new-shadow)] (when-not (= states* new-states) (reset! states new-states)) - (when-not (sync/empty-diff? diff) + (when-not (or client? (sync/empty-diff? (:diff msg))) (>! out (message/diff-msg diff hash))) - (recur new-shadow)) + (if client? + (recur new-shadow) + (recur (sync/get-current-state new-states)))) (if client? (do (>! out message/full-sync-request) (recur shadow)) From bd47ec10994b110142f675990f03dbeb3217dc31 Mon Sep 17 00:00:00 2001 From: Nicholas Kariniemi Date: Mon, 6 Oct 2014 19:21:06 +0300 Subject: [PATCH 40/40] Remove log statements --- src/cljx/grub/state.cljx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/cljx/grub/state.cljx b/src/cljx/grub/state.cljx index a5dabd3..265abe3 100644 --- a/src/cljx/grub/state.cljx +++ b/src/cljx/grub/state.cljx @@ -16,8 +16,6 @@ :diff (let [states* @states shadow (sync/get-history-state states* (:hash msg))] - #+cljs (logs "diff msg:" msg) - #+clj (println "diff msg:" msg) (if shadow (let [new-states (sync/apply-diff states* (:diff msg)) new-shadow (diff/patch-state shadow (:diff msg)) @@ -39,18 +37,14 @@ :full-sync (if client? (let [state (:state msg)] - #+cljs (logs "received full sync") (reset! states (sync/new-state state)) (recur state)) (let [state (sync/get-current-state @states)] - #+clj (println "sending full sync") (>! out (message/full-sync state)) ;; HERE (recur state))) :new-state (let [{:keys [diff hash]} (sync/diff-states (:state msg) shadow)] - #+cljs (logs "new state") - #+clj (println "new state") (when-not (sync/empty-diff? diff) (>! out (message/diff-msg diff hash))) (recur shadow)) @@ -78,12 +72,11 @@ (a/go-loop [] (let [[val _] (a/alts! [! client-events val) - (recur)) - (do #+clj (println "client disconnected, clean up") - (remove-watch states client-id) - (a/close! ! client-events val) + (recur)) + (do (remove-watch states client-id) + (a/close! client states))) #+clj