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

Developing (with) Keechma

Developing (with) Keechma

Mihael Konjević

September 02, 2017
Tweet

More Decks by Mihael Konjević

Other Decks in Programming

Transcript

  1. Developing (with) Keechma
    Mihael Konjević | twitter.com/mihaelkonjevic

    View Slide

  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

    View Slide

  3. Keechma goals
    • Deterministic and predictable behavior

    • Unidirectional data flow

    • Enforced lifecycle (and memory safety)

    View Slide

  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

    View Slide

  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"}

    View Slide

  6. View Slide

  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

    View Slide

  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]))

    View Slide

  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

    View Slide

  10. View Slide

  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 %)))

    View Slide

  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] ((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)))))

    View Slide

  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] ((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))))))

    View Slide

  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

    View Slide

  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

    View Slide

  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)))))

    View Slide

  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)))]}

    View Slide

  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

    View Slide

  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))))))

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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"}))}})

    View Slide

  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

    View Slide

  25. Conclusion
    • Treat route as the minimal representation of state:

    • Lifecycles

    • Memory safety

    • Declarative data loading

    View Slide

  26. Thank you.
    http://keechma.com

    View Slide