Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developing (with) Keechma

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Developing (with) Keechma

Avatar for Mihael Konjević

Mihael Konjević

September 02, 2017
Tweet

More Decks by Mihael Konjević

Other Decks in Programming

Transcript

  1. 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. 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
  3. 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"}
  4. 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
  5. 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]))
  6. 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
  7. 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 %)))
  8. 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)))))
  9. 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))))))
  10. 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
  11. 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
  12. 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)))))
  13. 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)))]}
  14. 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
  15. 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))))))
  16. 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
  17. 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
  18. 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
  19. 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"}))}})
  20. 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
  21. Conclusion • Treat route as the minimal representation of state:

    • Lifecycles • Memory safety • Declarative data loading