Developing (with) Keechma

Developing (with) Keechma

9976060bfb26220dd042340394d06b31?s=128

Mihael Konjević

September 02, 2017
Tweet

Transcript

  1. 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
  2. 3.
  3. 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
  4. 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"}
  5. 6.
  6. 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
  7. 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]))
  8. 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
  9. 10.
  10. 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 %)))
  11. 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)))))
  12. 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))))))
  13. 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
  14. 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
  15. 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)))))
  16. 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)))]}
  17. 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
  18. 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))))))
  19. 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
  20. 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
  21. 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
  22. 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"}))}})
  23. 24.

    Dataloader + GraphQL • Combine all requests happening at the

    same time to one • Unpack data when it comes back • http://github.com/retro/graphql-builder • https://github.com/keechma/example-dataloader-graphql/blob/ master/src/cljs/graphql_starwars/datasources.cljs
  24. 25.

    Conclusion • Treat route as the minimal representation of state:

    • Lifecycles • Memory safety • Declarative data loading