Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Keechma goals • Deterministic and predictable behavior • Unidirectional data flow • Enforced lifecycle (and memory safety)

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Conclusion • Treat route as the minimal representation of state: • Lifecycles • Memory safety • Declarative data loading

Slide 26

Slide 26 text

Thank you. http://keechma.com