Liking cljdoc? Tell your friends :D

Don’t use this library - the code is not ready.

ci

Easy and Robust HTTP Requests

A re-frame library for performing HTTP requests using an effect with key :http

It is an improved version of the original re-frame-http-fx library

Overview

HTTP requests are simple, right?

You send off a request, you get back a response, and you store it in app-db. Done.

Except requests are anything but simple. There is a happy path, yes, but it winds through a deceptively dense briar thicket of fiddly issues. Accessing unreliable servers across unreliable networks using an async flow of control is hard enough, what with the multiple failure paths and possible retries but, on top of that, there’s also the cross-cutting issues of managing UI updates, user-initiated cancellations, error recovery, logging, statistics gathering, etc.

Many programmers instinctively shy away from the tentacles of this complexity, pragmatically preferring naive but simple solutions. And yet, even after accepting that trade-off, they can find themselves with an uncomfortable amount of repetition and boilerplate.

This library has two goals:

  1. proper treatment for failure paths (robustness and a better user experience)

  2. you write less code for each request (no repetition or fragile boilerplate)

These two goals might typically pull in opposite directions, but this library saves you from the horns of this dilemma and seeks to let you have your cake and eat it too. In fact, maybe even eat it twice. But no chocolate sprinkles. We’re not monsters.

An Indicative Code Fragment

Here’s a re-frame event handler returning an effect keyed :http (see the last three lines):

(ref-event-fx
  :switch-to-articles-panel
  (fn [{:keys [db]} _]
    ;; Note the following :http effect
    {:http {:action :GET
            :url    "http://api.something.com/articles/"
            :path    [:a :path :within :app-db]}}))

That :http effect will do an HTTP GET and place any response data into app-db at the given :path.

But wait, there’s more.

This request will be retried on timeouts, and 503 failure, etc. Logs will be written, errors will be reported, and interesting statistics will be captured. The user will see a busy-twirly and be able to cancel the request by hitting a button. The response data can be processed from JSON and transit into EDN, before being put into right place within app-db. Etc.

So, by using just three lines of code - simple ones at that - you’ll get a robust HTTP request process. However, as you’ll soon see, there’s no magic happening to achieve the outcome. You’ll need to compose the right defaults.

The cost? Some upfront learning so you can fashion these right defaults for your application. Let us begin that learning now …​

An Overview

I have some good news and some bad news — first, the good news.

This library models an HTTP request using a Finite State Machine (hereafter FSM) - one which captures the various failure paths and retry paths, etc. That leads to a robust and understandable request process.

Also, this library wraps fetch, making it central to the FSM, and that delivers a browser-modern way to do the network layer.

But, that’s the end of the good news. The bad news is that you’ll be doing most of the work.

Most parts of the FSM are left unimplemented, and you will need to fill in these blanks by writing a number of Logical State Handlers.

A Logical State Handler provides the behaviour for a single Logical State in the FSM. To help you, this library will provide "a recipe" for each of these handlers, describing what your implementation might reasonably do. So, for example, in the Failed state, the recipe might say that you "perform error logging and error reporting to the user" and "recover the application into a stable state, in light of the failure".

Of course, only you, the application programmer, know how to implement "a recipe" correctly for your application. And that’s precisely why the FSM’s implementation is left incomplete and why you are asked to fill in the blanks.

With a bit of squinting and head tilting, you can see a Logical State Handler as something of a callback. For example, writing a Logical State Handler for the Failed State is very similar to supplying an on-failure callback. Except, of course, because we’re in re-frame land, the mechanism will be more dispatch-back than callback.

Each Logical State Handler you write has to be something of a "good citizen" within the overall composition (follow the task recipe). If your Logical State handler fails to do the right thing, any FSM you compose using it will be, to some extent, dysfunctional. I have every confidence in you.

Later, once you have written the necessary set of Logical State Handlers, you compose them to form a functioning FSM. And, because this is a ClojureScript library, this composition happens in a data oriented way.

If your application’s needs are sufficiently complicated, you can create multiple FSM compositions within the one app, with each configuration designed for a different kind of request.

And, if you use one regularly, you can nominate it to be the default FSM. That three-line example I presented earlier owes its brevity to (implicitly) using a default FSM composition.

Finally, let’s talk about state.

XXX logical state

XXX extended state

Each FSM instance has some working state, which we call request-state. In addition, there will be some state stored within app-db which is also associated with a request - we call that path-state. Your Logical State handlers`are responsible for pushing/projecting parts of `request-state through into path-state, so that your UI can render the state of the request. Again, only you, the application programmer, know how you want this done, although we will certainly be suggesting some patterns. For example, you might write a :retrying? value of true into path-state which then causes the UI to render "Sorry about the delay. There was a problem with the network connection, so we’re trying again".

So, in summary:

  • you should get to know the FSM topology (in the next section)

  • you will implement the blank parts of the FSM but writing a set of Logical State handlers, following the recipe for each.

  • you will compose an ensemble of these Logical State handlers to form a FSM

  • your FSM will likely push/project aspects of request-state into path-state

  • you will write views in terms of what’s in path-state, to show the user what’s happening

  • when you make an HTTP request, you’ll be nominating which FSM to use (or you will implicitly be using your default FSM)

The FSM

An HTTP request is a multi-step process which plays out over time. This library models this process as a Machine.

By a Machine, I’m referring to the abstract concept of something which does something. This library uses a specific kind of Machine called a *Finte State Machine (FSM)*, which is one that has a fixed, finite number of States.

In each State, a Machine has discrete responsibilities, concerns and behaviours. And a FSM can only be in one State at a time.

This library formalises that process as a Machine. And, specifically, it uses a Finte State Machine (FSM), which has a fixed, finite number of States.

States allow us to reason about what a Machine is doing because: * * in each State the Machine has discrete responsibilities, concerns and behaviours

So, when an FSM changes State, it goes from doing one thing to doing another thing.

Over time, events occur and they can cause the Machine to changes from one State to another. Such events are called Triggers and a change in State is called a Transition. Sometimes there are certain Actions (behaviour/computation) associated with a Transition. But, to repeat, the significant thing about a State change is that the Machine goes from doing one thing to doing another thing.

What does a Machine do in a State?:
  • it can do nothing (waiting for a Trigger)

  • it can undertake "an activity" which takes finite time and comes to an end (this ending might causes a Trigger)

  • it can undertake "an activity" which does not naturally come to an end (until there is a Trigger)

  • it can compute the next Trigger. These are sometimes called PseudoStates.

This library’s FSM contains examples of all these kinds of State.

The Logical State Handlers you will be asked to write are about "doing a thing" when the Machine is in a particular State. And, as a result, they implement the behaviour for one part of this library’s FSM.

The FSM at the core of this library:

FSM

Notes:

  • to use this library, you’ll need to understand this FSM

  • the boxes in the diagram represent the FSM’s Logical States

  • the lines between the boxes show what Transitions are allowed between Logical States

  • the names on those lines are the Triggers (the event which causes the Transition to happen)

  • when you write a Logical State Handler you are implementing the behaviour for one of the boxes

  • the "happy path" for a request is shown in blue (both boxes and lines)

  • and, yes, there are variations on this FSM model of a request - this one is ours. We could, for example, have teased the "Problem" Logical State out into four distinct states: "Timed Out", "Connection Problem", "Recoverable Server Problem" and "Unrecoverable Server Problem". We decided not to do that because of, well, reasons. My point is that there isn’t a "right" model, just one that is fit for purpose.

NOTE:

Requesting

Earlier, we saw this code which uses an effect :http to initiate an HTTP GET request:

(ref-event-fx
  :switch-to-articles-panel
  (fn [{:keys [db]} _]
    ;; Note the following :http effect
    {:http {:action :GET
            :url    "http://api.something.com/articles/"
            :path   [:put :response :data :at :this :path :in :app-db]}}))

Who doesn’t love terse? But, as a learning exercise, let’s now pendulum to the opposite extreme and show you the most verbose use of the :http effect:

(reg-event-fx
  :request-articles
  (fn [_ _]
    {:http  {:action :GET     ;; can be :PUT :POST  :HEAD etc

             :url    "http://api.something.com/articles/"

             ;; Optional. The path within `app-db` to which request related data should be written
             ;; The map at this location is known as `path-state`
             :path [:a :path :within :app-db]

             ;; Compose the FSM by nominating the `Logical State handlers`.
             ;; Look back at the FSM diagram and at the boxes which represent
             ;; Logical States.
             ;; When the FSM transitions to a new Logical State, it will `dispatch`
             ;; the event you nominate below, and the associated event handler is expected
             ;; to perform "the behaviour" required of that Logical State.
             :fsm {:in-setup      [:my-setup]
                   :in-process    [:my-processor]
                   :in-problem    [:deep-think :where-did-I-go-wrong]
                   :in-failed     [:call-mum]
                   :in-cancelled  [:generic-cancelled]
                   :in-succeeded  [:yah! "fist-pump" :twice]
                   :in-teardown   [:so-tired-now]}

             ;; a map of query params
             :params     {:user     "Fred"
                          :customer "big one"}

             ;; a map of HTTP headers
             :headers    {"Authorization"  "Bearer QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
                          "Cache-Control"  "no-cache"}

             ;; Where there is a body to the response, fetch will automatically
             ;; process that body according to mime type provided.
             ;; XXX Isaac we have to explain this
             ;; XXX are there sensible defaults? What if I forget to provide?
             :content-type {#"application/.*json" :json
                            #"application/edn"    :text}

             ;; Optional - by default a request will run as long as the browser implementation allows
             :timeout       5000

             ;; Note: GET or HEAD cannot have body.
             ;; Can be one of: String | js/ArrayBuffer | js/Blob | js/FormData | js/BufferSource | js/ReadableStream
             :body    "a string"

             ;; how many times should occurances like timeouts or HTTP status 503 be retried before failing
             :max-retries  5

             ;; Optional: an area to put application-specific data
             ;; If data is supplied here, it will probably be used later within the
             ;; implementation of a "Logical State Handler". For example "description"
             ;; might be a useful string for displaying to the users in the UI or
             ;; to put in errors or logs.
             :context {:description  "Loading articles"
                       :dispatch-on-success  [:another event]
                       :recover-to {[:where :I :store :the :panel :id] old-value}}

             ;; The following are optional and more obscure.
             ;; See https://developer.mozilla.org/en-US/docs/Web/API/Request#Properties
             :credentials   "omit"
             :redirect      "manual"
             :mode          "cors"
             :cache         "no-store"
             :referrer      "no-referrer"

             ;; See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
             :integrity     "sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE="}))

While all this specification offers useful flexibility, we clearly don’t want to repeat this much every time. Particularly because we’ll often want the same headers, params and Logical State handers.

How do we avoid boilerplate and repertition?

Profiles

A Profile associates an id with a fragment of :http specification.

You "register" one or more Profiles, typically on application startup.

Because an :http specification is just data (a map), a fragment is also just data (again, a map). And if you think that sounds pretty simple, you’d be right.

Registering A Profile

The code below shows how to register a profile with id :xyz, and associate it with certain specification values:

(reg-event-fx
   :register-my-http-profile
   (fn [_ _]

      {:http   {;; The `:action` is no longer a verb
                ;; Instead it indicates we are registering a profile
                :action  :reg-profile

                ;; This identifier will be used later
                :id      :xyz

                ;; Optional. Set this profile as the 'default' one?
                :default? true

                ;; This is the important bit
                ;; This map captures the values associated with this profile.
                :values {:url "http:/api.some.com/v2"
                         :fsm {:in-process    [:my-processor]
                               :in-problem    [:generic-problem :extra "whatever"]
                               :in-failed     [:my-special-failed]
                               :in-cancelled  [:generic-cancelled]
                               :in-teardown   [:generic-teardown]}
                          :timeout       3000
                          :max-retries   2
                          :context  {...}}}}))

Using A Profile

Here’s an example of using the Profile with id :xyz which we registered above:

{:http {:action :GET
        :url    "http://api.endpoint.com/articles/"
        :path   [:somewhere :in :app-db]}

Wait! Is this a trick? That’s the same three lines as before!

Indeed. If you look back, you’ll see the Profile :xyz was registered with :default? true which means it will be used by default, but only if no Profile is explicitly provided.

Here’s how to explicitly nominate a Profile:

{:http {:action :GET
        :url    "http://api.endpoint.com/articles/"
        :path   [:somewhere :in :app-db]
        :profiles [:xyz]}}     ;;  <--- NEW: THIS IS HOW WE SAY WHAT PROFILE(S) TO USE

That new key :profiles allows you to nominate a vector of previously registered Profile ids. The map of :values from those Profiles will be added into the :http specification.

Here’s another example, but this time with multiple profile ids:

{:http {:action :GET
        :url    "http://api.endpoint.com/articles/"
        :path   [:somewhere :in :app-db]
        :profiles [:jwt-token :standard-parms :xyz]}}     ;;  <---- MULTIPLE

The map of :values in all nominated profiles will be composed into the the :http specification.

explicitly using :profiles [] would mean no profiles. This is the way to NOT even use the default.

Composing Profiles

How are multiple profiles combined?

As a first approximation, imagine the process as a clojure.core/reduce across a collection of maps, using clojure.core/merge:

(reduce merge [map1, map2, map3])

This will accumulate the key/value pairs in the maps, into one final map.

An example:

(def map1 {:a 1})
(def map2 {:b 11})

(reduce merge [map1, map2])

the result is {:a 1 :b 11}.

Instead of map1, map2, imagine that we combine profile1, profile2, like this:

(def profile1 {:action :GET})
(def profile2 {:url "http://some.com/"})

(reduce merge [profile1, profile2])

with the result:

{
 :action :GET
 :url "http://some.com/"
}

While ever the profiles have disjoint keys, this is straightforward. But, when there are duplicate keys, we need a strategy to "combine" the coresponding values.

Here are the rules:
  • if both values satisfy str?, then they will be combined with str

  • if both values satisfy set?, then they will be combined with clojure.set/union

  • if both values satisfy map?, then they will be combined with merge (remember merge is shallow).

  • if both values satisfy sequential?, then conj is used

  • otherwise, last value wins (no combining)

Imagine we have a special version of merge which implements these rules, called say special-merge.

(def profile1 {:url "http://some.com/"})
(def profile2 {:url "blah"})

(reduce special-merge [profile1, profile2])

the result would be:

{:url "http://some.com/blah"}

because the values for the duplicate :url keys are strings, they will be combined with str to form one string.

Similarly:

(def profile1 {:params {:Cache-Control "no-cach"}})
(def profile2 {:params {:Authorization "Basic YWxhZGRpbjpvcGVuc2VzYW1l"}})

(reduce special-merge [profile1, profile2])

the result would be:

{:params {:Cache-Control "no-cach"
          :Authorization "Basic YWxhZGRpbjpvcGVuc2VzYW1l"}}

because the values for the duplicate :params keys are maps and will be combined with merge.

So, when you nominate multiple profiles:

{:http {:action :GET
        ...
        :profiles [:jwt-token :standard-parms :xyz]}}     ;;  <---- MULTIPLE PROFILES

the final :http spec will be a map. And it will be as if it was formed using special-merge on all the :values maps from all the nominated profiles, plus the map supplied for the :http itself as the last one.

Advanced Profile Combining

Where you need to take detailed control of the "combining" process you can use this library’s API function merge-profiles

{:http (-> (merge-profiles [:xyz :another])         ;; combines these two profiles and returns a map
           (assoc-in [:fsm :in-setup]  [:special])  ;; now manipulate the map in the way you want
           (update-in [:url] str "/path"))

The function call (http/merge-profiles [:xyz]) would just return :values map for that one profile.

About State

There are two kinds of State:

  • request-state is data for a single request and it maintained by this library. It only exists for the lifetime of a request. This state is stored internally in the library and, although it is provided in the event vector of Logical State Handlers, it is effectively read-only. It includes the request id, the current logical state of the FSM, the original request, a trace history through the FSM including timings, etc.

  • path-state - this state is a map of values which exists at a particular path within app-db. It is the application’s "materialised view" of the request-state. The contents of this map is up to you, the writer of the application. It will be created and maintained by the Logical State Handlers you write.

Typically, the in-setup LogicalStateHandler initialises path-state, and it is then maintained across the request handling process by the various FSM handlers. Ultimately, it will contain the response data or an error. Your views will be subscribed to this map and will render it appropriately for the user to view.

An example of the path-state map.

{
  :request-id  123456
  :loading?    true

  :result      nil
  :retries     0
  :cancelled?  false
  :description "Loading filtered thingos"

  :error   {
     :title            "Error loading thingos"
     :what-happened    "Couldn't load thingos from the server because it returned a status 500 error"
     :consequence      "This application can't display your thingos"
     :users-next-action "Please report this error to your help desk via email, with a screenshot. Perhaps try again later"}
}

Remember, you design this map. You initialise it in in-setup. You update it to reflect the state of the ongoing request. You create the subscriptions which deliver it to a view, and that view will render it.

XXX :context is put where?

Note: none of this precludes you, for example, writing errors to a different place within app-db. You write the LogicalStatehandlers. Your choice about how data flows into app-db. The proposal above is just one way to do it.

XXX To avoid race conditions, should the booleans be false in absence via subscriptions? Eg: use completed? instead of loading? because "absence" (a nil) correctly matches the predicate’s negative value.

XXX consider what else needs to happen to work well with re-frame-async-flow

So, I’d like to stress two points already made: - lifetime: path-state exists for as long as your application code says it should - it persists. Whereas request-state is created and destroyed by this library - it is a means to an ends - it is transitory. - during the request process, request-state tends to be authoritative. : path-state is something of a projection or materialised view of request-state. (Not entirely true but a useful mental model at this early stage in explanation)

While path-state …​. there might need to be a :loading? value set to true to indicate that the busy twirly should be kept up. Or perhaps a :retrying? flag might need to be "projected" from the reguest-state so that, again, the UI can show the user what is happening.

Ultimately, the most important part of this path-state is the (processed) response data itself. But there will be other information alongside it. For this reason, presentation-state is normally a map of values with a key for response, but it has other values.

The path-state is managed by your Logical State Handlers. You control what data is projected from the request-state across into the presentation-state. Because you, the application programmer, knows what you want to set within app-db. You know how you want the UI to render the state of the request process.

For example: - it is the job of the in-setup to initially create the XXX-state assumed to be a map. And it might initially establish within this map a :loading? flag as true. - it is then the job of the in-teardown handler to set the :loading? flag back to false (thus taking down the twirly).

Logical State Handler Recipes

To use this library, you’ll:
  • design path-state and the views which render it (or simply use the default design suggested)

  • implement your Logical State Handlers (or simply use the default Handlers provided)

The Logical State Handlers you write are about "executing the behaviour" associated with being in a particular state within the FSM. They implement behaviour for one part of "the machine".

Recipes for each of the Logical State Handlers …​

in-setup

Overview: prepare the application for the pending HTTP request.

Recipe:
  • establish initial path-state at the nominated :path

  • optionally, if the application is to allow the user to cancel the request (e.g., via a button) then capture the :request-id of the request and assoc it into path-state for access within the view (which will dispatch a cancel request event with this id supplied).

  • optionally, put up a twirly-busy-thing, perhaps with a description of the request: "Loading all the blah things", perhaps with a cancel button

  • optionally, cause the application to change panel or view to be ready for the incoming response data.

  • trigger :send to cause the transition to waiting state. The transition will cause the fetch action which actually initiates the request.

Views subscribed to this path-state will then render the UI, probably locking it up and allowing the user to see that a request is in-flight.

XXX a panel might change …​. perhaps the user clicked a button to "View Inappropriate", so the application will change panels to the inappropriate one (via a change in app-db state), AND also kickoff a server request to get the "inappropriates".

Example implementation:

(fn [{:keys [db] :as cofx} [_ {:keys [request-id context] :as request-state}]]
  (let [path (:path context)]
    ;; trigger for state transition
    {:http  {:trigger :send
             :request-id request-id}
     ;; Initialise app-db to reflect that a request is now inflight
     ;; This might mean updating some "global" place in app-db to get a twirly-busy-thing up
     ;; This might mean putting an "map" at the path provided in the request
     :db    (-> db
              (assoc-in (conj path :request-id) request-id)
              (assoc-in [:global :loading?] true)
              (assoc-in [:global :loading-text] (:loading-text context)))}))

XXX once preparation is complete, notice that your code is expected to trigger the transition.

in-waiting

This State Handler is unique because it is the only one you can’t write. It is provided by this library.

In this state, we are waiting for an HTTP response (after the fetch is launched) and then doing the first round of processing of the response body.

in-processing

Recipe:
  • Process the response: turn transit JSON into transit or XXX

  • store in app-db

  • FSM trigger :processed or :processing-error

Example implementation

(fn [{:keys [db] :as cofx} [_ {:keys [request-id response context] :as request-state}]]
  (let [path (:path context)
        reader (transit/reader :json)]
    (try
      (let [data (transit/read reader (:body response))]
        {:db (assoc-in db (conj path :data) data)
         :http {:trigger :processed
                :request-id request-id}}))
      (catch js/Error e
        {:db   (-> db
                 (assoc-in (conj path :error) (str e)))
         :http {:trigger :processing-error
                :request-id request-id}})))

XXX :processing-error causes a transition to failed. How and where does this state obtain the error details?

in-succeeded

The processing of the response has succeeded.

Recipe:
  • FSM trigger :done

Example implementation

(fn [{:keys [db] :as cofx} [_ {:keys [request-id] :as request-state}]]
  {:http {:trigger :done
          :request-id request-id}})

in-problem

Recipe:
  • decide what to do about the problem - retry or give up?

  • FSM trigger :fail or :retry

Example implementation:

(fn [{:keys [db] :as cofx} [_ {:keys [request-id context problem response] :as request-state}]]
  (let [path (:path context)
        temporary? (= :timeout problem)
        max-retries (:max-retries context)
        num-retries (get-in db (conj path :num-retries request-id) 0)
        try-again? (and (< num-retries max-retries) temporary?)]
    (if try-again?
      {:http {:trigger :retry
              :request-id request-id}
       :db (update-in db (conj path :num-retries request-id) inc)}
      {:http {:trigger :fail
              :request-id request-id}})))
Full taxonomy of problems:
  • network connection error - no response - retry-able (except that DNS issues take a long time, so retires are annoying)

    • cross-site scripting whereby access is denied; or

    • requesting a URI that is unreachable (typo, DNS issues, invalid hostname etc); or

    • request is interrupted after being sent (browser refresh or navigates away from the page); or

    • request is otherwise intercepted (check your ad blocker).

  • fetch API body processing error; e.g. JSON parse error.

  • timeout - no response - retry-able

  • non 200 HTTP status - returned from the server - MAY have a response

    • may have a response :body returned from server which will need to be processed. See https://tools.ietf.org/html/rfc7807 Imagine a 403 Forbidden response. XXX talk about how it might be EDN or a Blob etc.

  • some HTTP status are retry-able and some are not

in-failed

The request has failed and we must now adjust for that.

Ultimately, it doesn’t actually matter why we are in the failed state, but to help give context, here’s the sort of reasons we end up in this state: * no outright failure, but too many retries (see :history XXX for what happened) * some kind of networking error happened which means the request never even got to the target server (CORS, DNS error?) * the server failed in some way (didn’t return a 200) * a 200 response was received but an error occurred when processing that response

Recipe:
  • log the error

  • show the error to the user

  • put the application back into a sane state

  • FSM trigger :teardown

Example implementation:

(fn [{:keys [db] :as cofx} [_ {:keys [request-id context problem response] :as request-state}]]
  (let [path (:path context)]
    {:http {:trigger :teardown
            :request-id request-id}
     :db (-> db
             ...)}))

in-cancelled

This state follows user cancellation.

Recipe:
  • put the application into a state consistent with the cancellation. What does the user see? What can they do next?

  • update path-state, maybe.

  • FSM trigger :teardown

Example implementation:

(fn [{:keys [db] :as cofx} [_ {:keys [request-id context problem response] :as request-state}]]
  (let [path (:path context)]
    {:http {:trigger :teardown
            :request-id request-id}
     :db (-> db
             ...)}))

in-teardown

Irrespective of the outcome of the request (success, cancellation or failure), this state occurs immediately before it completes.

As a result, in this state we handle any actions which have to happen irrespective of the outcome.

Recipe:
  • take down the twirly

  • accumulate and log final stats

  • possible updates to path-state

  • change :loading? to false

  • possible updates to app-db

  • busy twirly removal

  • FSM trigger :destroy

Example implementation:

(fn [{:keys [db]} [_ {:keys [request-id context] :as request-state}]]
  (let [path (:path context)]
    {:http {:trigger :destroy
            :request-id request-id}
     :db (-> db
           (assoc-in [:global :loading?] false))}))

Notes

XXX:
  • split the recipies into their own docs in /docs

  • FAQ for file upload - reference example application

  • Talk about the two approaches to switching tabs

  • Nine states of UI

  • note somewhere you can supply multiple requests …​ a vector

  • Add note that fetch doesn’t work on IE. So you’ll need to provide a polyfil if you target IE.

  • add optional :cancel event handler ??

  • ??? add an interceptor to assert the correctness of the Transitions - Logical State Handlers

  • anything we should be doing around stubbing and testing?

  • add trace to FSM

FAQ

  1. Your FSM is wrong

  2. Why don’t you use gards?

Explaining State Machines

A control system determines its outputs depending on its inputs.

If the present input values are sufficient to determine the outputs the control system is a combinatorial system.

For example, a traffic light control system could be constructed like this: * the only input is the current time (the input changes every second) * strip the input time down to just the seconds part (a number between 0 and 59) * Apply the following rules: if the seconds is between 0 and 27 seconds then the output is "green" if the seconds is between 28 and 33 seconds, the output is "orange" ** if the seconds is between 34 and 59 seconds, the output is "red"

At any point in this system’s fucntioning, knowing the input (time) is sufficient to know the output (green,orange,red).

If, on the other hand, the control system needs to know the history of its inputs to determine its output the system is a sequential system.

To function, a sequential system must store a representation of its input history. And this representation is known as State.

Imagine a traffic lights control system which XXX

If the State was a 16 bit integer, the number of States would be 65536.

A state machine is the oldest known formal model for sequential behaviour i.e. behaviour that cannot be defined by the knowledge of inputs only, but depends on the history of the inputs.

Can you improve this documentation? These fine people already did:
Mike Thompson, Isaac Johnston & Denis Johnson
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close