Skip to content

Commit

Permalink
fix README example
Browse files Browse the repository at this point in the history
  • Loading branch information
mccraigmccraig committed May 22, 2023
1 parent daf15ae commit 1868a49
Showing 1 changed file with 82 additions and 43 deletions.
125 changes: 82 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@

TODO - much documentation expansion

A-frame is a port of the [re-frame](https://github.com/day8/re-frame)
event and effect handling machinery to the async domain. It offers a
straightforward separation of pure and side-effecting code for both
Clojure and ClojureScript.
A-frame was originally a port of the [re-frame](https://github.com/day8/re-frame)
event and effect handling machinery to the async domain. It has developed
to become even more data-driven than re-frame, making logging and debugging
extremely transparent.

## why?

Everyone tells you to keep your side-effecting and pure code apart. A-frame
helps you to do it.
Everyone says you should keep your side-effecting and pure code apart. A-frame
helps you to do it. it's not as posh as a freer monad based thing, but
it is easy to understand, and easy to observe.

## simple data

The term "simple data" is used below - it means data containing no opaque
objects - i.e. no functions or other opaque types. Roughly anything that can
easily be serialised and deserialised to/from EDN or JSON.

## how

Expand All @@ -26,91 +33,123 @@ are handled in a 3 stage process:
[gather coeffects] -> [handle event] -> [process effects]
```

Like re-frame, this process is implemented with an interceptor chain but, unlike
re-frame, the a-frame interceptor chain is asynchronous - the `:enter` or
`:leave` fns in any interceptor may return a promise of their result.
Like re-frame, this process is implemented with an interceptor chain. Unlike
re-frame, the a-frame interceptor chain is:
* asynchronous - the `:enter` or `:leave` fns in any interceptor may return a
promise of their result.
* data-driven - interceptor chains are themselves described by simple data.
Since `events`, `coeffects` and `effects` are also simple data, the state of
the interceptor chain at any point is fully serialisable.

* Events are handled by an interceptor chain
* The final interceptor in the chain is the `event-handler`
* an `event-handler` is always a pure function with no side effects
* Prior interceptors are `coeffect` or `effect` handlers
* `coeffect` and `effect` handlers may have side-effects and may return
a promise of their result
A typical event-handler interceptor chain looks like this:

``` text
-> [enter: ] -> [enter: coeffect-a] -> [enter: coeffect-b] -> [enter: handle-event] --|
<- [leave: handle-effects] <- [leave: ] <- [leave: ] <- [leave: ] <-|
```

## simple data

"simple" data means containing no opaque objects - i.e. no functions or
other opaque types.
The final interceptor in the chain contains the `event-handler`.
An `event-handler` is always a pure function with no side effects. Prior
interceptors contain `coeffect` or `effect` handlers, which may have
side-effects and may return a promise of their result.

## events

Events are simple maps describing something that happened. An event
map must have an `:a-frame/id` key, which describes the type of the
event and will be used to find a handler for processing the event.

Events can also be simple vectors of `[<id> ...]`, like they generally are
Events can also be simple vectors of `[<id> ...]`, as they generally are
in re-frame, but this is less preferred because it makes literal
paths referencing the events less easy to understand.
paths referencing data in the events less easy to understand.

Event handler functions have a signature:

`(fn [<coeffects> <event>])`

## coeffects

Coeffects are a simple data map representing (possibly side-effecting)
Coeffects are a simple data map representing
inputs gathered from the environment and required by the event-handler.
A particular event handler has a chain of coeffect handlers, each of
which identifies a particular coeffect handler by keyword.

Coeffect handler functions have a signature:

`(fn ([<app> <coeffects>]) ([<app> <coeffects> <data>]))`

## effects

Effects are a simple data structure (map) describing outputs from the event
handler. Effects are either a map of `{<effect-key> <effect-data>}` or
a vector of such maps. the `<effect-key>` keywords will be used to
Effects are a datastructure describing outputs from the event
handler. Effects are either a map of `{<effect-key> <effect-data>}`, indicating
concurrent processing of offects, or a vector of such maps - requiring
sequential processing. The `<effect-key>` keywords are used to
find a handler for a particular effect.

Effect handler functions have a signature:

`(fn [<app> <effect-data>] )`

## simple example

This example defines a `::get-foo` event, which uses a `::load-foo` cofx
to load the object and then returns the loaded object as an `:api/response`
effect.

``` clojure
(require '[a-frame.core :as af])
(require '[a-frame.std-interceptors :as af.stdintc])

(af/reg-cofx
::load-foo
(fn [app coeffects {id :id url :url}]
(assoc coeffects ::foo {:id (str url "/" id) :name "foo"})))

(af/reg-fx
:api/response
(fn [app data]
;; do nothing
))
(fn [;; app context
{api-client :api :as app}
;; already defined coeffects
_coeffects
;; data arg
{id :id url :url}]
{:id (str url "/" id) :name "foo" :client api-client}))

(af/reg-event-fx
::get-foo

[(af/inject-cofx
[(af/inject-validated-cofx
::load-foo
{:id #a-frame.event/path [::foo-id]
:url #a-frame.cofx/path [:config :api-url]})]
{:id #a-frame.cofx/event-path [::foo-id]
:url #a-frame.cofx/path [:config :api-url]}
[:map [:id :string] [:name :string]]
::the-foo)]

(fn [{foo ::foo
:as coeffects}
(fn [{foo ::the-foo :as coeffects}
event]
{:api/response {:foo foo}}))

(def router (af/create-router {:api nil}))
(def router (af/create-router
;; app context for opaque objects like network clients
{:api ::api}
;; global interceptors are prepended to every event chain
{:a-frame.router/global-interceptors
af.stdintc/minimal-global-interceptors}))

(def r (af/dispatch-sync
router
{:config {:api-url "http://foo.com/api"}} ;; initial coeffects

;; optional initial coeffects
{:config {:api-url "http://foo.com/api"}}

;; the event
{:a-frame/id ::get-foo
::foo-id "1000"} ;; the event
))
::foo-id "1000"}))

;; unpick deref'ing a promise only works on clj
(-> @r :a-frame/effects :api/response)
;; => {:foo {:id "http://foo.com/api/1000", :name "foo"}}

;; => {:foo {:id "http://foo.com/api/1000", :name "foo" :client :user/api}}
```

<!-- TODO -->
<!-- - dispatch-* coeffects - returning a result to a path in the coeffects -->
<!-- - do-fx interceptor should add an :a-frame/effects-results key to -->
<!-- the interceptor-context with all the effect results -->
<!-- - automatic test.check property based testing for event-handlers -->
<!-- from the accumulated schema of inject-validated-cofx -->

0 comments on commit 1868a49

Please sign in to comment.