Developing (with) Keechma

Developing (with) Keechma


Mihael Konjević

September 02, 2017


  1. Developing (with) Keechma Mihael Konjević |

  2. A bit about me • ~ 15 years of software

    development experience • ~ 7 of those (full time) on frontend • Keechma is the 2nd framework I’ve been working on • 5 years as a core contributor to CanJS / JMVC • Keechma is a solution to the problems I’ve seen so far • …and I’ve seen them a lot
  3. Keechma goals • Deterministic and predictable behavior • Unidirectional data

    flow • Enforced lifecycle (and memory safety)
  4. Keechma’s Secret Sauce • Router is built in • Router

    converts URLs to data and vice versa • Route data is stored in the AppDB • Router is the main driver of change
  5. Router !;; define routes (def routes [[":page", {:page "index"}] ":page/:id"

    ":page/:id/:action"]) (def expanded-routes (expand-routes routes)) (map!->url expanded-routes {:page "foo" :id 1}) !;; "foo/1" (map!->url expanded-routes {:page "foo" :id 1 :action "bar" :qux "baz"}) !;; "foo/1/bar?qux=baz" (url!->map expanded-routes "foo/1") !;; {:page "foo" :id 1} (url!->map expanded-routes "foo?bar=baz") !;; {:page "foo" :bar "baz"}
  6. None
  7. Controller API • Controllers are records • params (sync) -

    called on every route change • start (sync) - called when started • handler - called after the start fn • stop (sync) - called when stopped
  8. Controller lifecycle prev value new value stop start final state

    nil nil stopped nil “articles” ✓ started “articles” “blog” ✓ ✓ started “articles” nil ✓ stopped “articles” “articles” started (defmethod controller/params Controller [this route-params] (get-in route-params [:data :page]))
  9. Why Controllers? • Automatically run code when you enter or

    exit the route • Enforced lifecycle (through start and stop functions) • Place to put your “dirty” code • Non opinionated - full access to AppDB • They don’t force you to model your code in the “Keechma" way
  10. None
  11. Controller Handler • A place for the async functionality •

    Full access to app-db • Communicates through channels (out-chan, in-chan) (defmethod controller/handler Controller [this app-db-atom in-chan out-chan] (make-ajax-req "/articles" #(swap! app-db-atom :assoc :articles %)))
  12. Controller Handler (defmethod controller/start Controller [this params app-db] (controller/execute this

    :load-articles) app-db) (defmethod controller/handler Controller [this app-db-atom in-chan out-chan] (go-loop [] (let [[command args] (<! in-chan)] (case command :load-articles (make-ajax-req "/articles" #(swap! app-db-atom :assoc :articles %)) :upvote-article (make-ajax-req (str "/artcles/" args) :post #(update-upvote-count %)) nil) (when command (recur)))))
  13. Controller Handler (defmethod controller/params Controller [_ route] (when (= (get-in

    route [:data :page]) "order-history") true)) (defmethod controller/start Controller [this params app-db] (controller/execute this :load-order-history) app-db) (defmethod controller/stop Controller [this params app-db] (controller/execute this :disconnect) app-db) (defmethod controller/handler Controller [this app-db-atom in-chan out-chan] (let [disconnect (connect-socketio in-chan)] (go-loop [] (let [[command args] (<! in-chan)] (case command :load-order-history (load-order-history app-db-atom) :order-created (order-created app-db-atom args) :disconnect (disconnect) nil) (when command (recur))))))
  14. Problems • A lot of boilerplate code • State mutations,

    async calls and pure functions mixed together - hard to figure out what’s going on • A lot of actions have the same structure (and slightly different implementation) • Do a few things in succession • Some are pure, some call server, some mutate state
  15. Pipelines! • Possible because of controller’s non opinionated API •

    Nice abstraction for a list of steps that define an action • It is possible to understand the action flow (no event ping pong) • They handle promises • They can handle errors
  16. Pipeline Example (def load-restaurant (pipeline! [value app-db] (pp/commit! (mark-restaurant-as-loading app-db

    :current)) !;; sideffect (http/get (str "/restaurants/" value)) !;; AJAX request (unpack-req value) !;; pure function (pp/commit! (store-restaurant-data app-db :current value)) !;; sideffect (rescue! [error] (pp/commit! (store-restaurant-error app-db :current error)))))
  17. Pipeline Example {:begin [(fn [value app-db] (pp/commit! (mark-restaurant-as-loading app-db :current)))

    (fn [value app-db] (http/get (str "/restaurants/" value))) (fn [value app-db] (unpack-req value)) (fn [value app-db] (pp/commit! (store-restaurant-data app-db :current value)))] :rescue [(fn [value app-db error] (pp/commit! (store-restaurant-error app-db :current error)))]}
  18. Notification example (def notice-pipeline (pipeline! [value app-db] (pp/commit! (assoc app-db

    :notice value)) !;; store the notice in the app-db (delay-pipeline 5000) !;; wait 5 seconds (pp/commit! (dissoc app-db :notice)))) !;; remove the notice from app-db
  19. Live search example (def search-pipeline (pp/exclusive (pipeline! [value app-db] (when-not

    (empty? value) (pipeline! [value app-db] (delay-pipeline 300) (movie-search value) (println "SEARCH RESULTS:" value))))))
  20. Pipelines are great, but… • Data loading is still a

    problem - especially when there are dependencies in data • “God” controllers start to appear • How to handle “tectonic” changes - user logging in
  21. Ideas • Global notification system - allow controllers to broadcast

    commands • Make controllers able to “wait” on other controllers • Both ideas are bad because they create dependencies between controllers
  22. Dataloader • Built as a reusable controller • Can resolve

    a graph of datasources • Router + deps • Knows when to load data • Knows when to invalidate stale data
  23. Dataloader (def datasources {:jwt {:target [:jwt] :loader (map-loader #(get-item local-storage

    "jwt-token")) :params (fn [prev _ _] (when (:data prev) ignore-datasource-check))} :current-user {:target [:current-user] :deps [:jwt] :loader load-data-from-api :params (fn [prev _ deps] (when-let [jwt (:jwt deps)] {:url "current-user" :auth jwt}))} :articles {:target [:articles] :loader load-data-from-api :params (fn [prev route _] (when (= "articles" (:page route)) {:url "articles"}))}})
  24. Dataloader + GraphQL • Combine all requests happening at the

    same time to one • Unpack data when it comes back • • master/src/cljs/graphql_starwars/datasources.cljs
  25. Conclusion • Treat route as the minimal representation of state:

    • Lifecycles • Memory safety • Declarative data loading
  26. Thank you.