Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs/interceptors #135

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 111 additions & 9 deletions packages/interceptors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ yarn add @thi.ng/interceptors
## Usage examples

```ts
import * as interceptors from "@thi.ng/interceptors";
importas interceptors from "@thi.ng/interceptors";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this edit intentional? Looks like a typo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, that's a typo... sorry

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha thanks. The way JavaScript moves these days I had to double check. Results were mostly in Spanish.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha

```

## Event bus, interceptors, side effects
## Event Bus, Interceptors, Side Effects


### [Interceptors](https://github.com/thi-ng/umbrella/blob/master/packages/interceptors/src/interceptors.ts) (Event and Effect primitives)
Expand All @@ -42,24 +42,126 @@ The idea of interceptors is quite similar to functional composition and AOP ([as
[UNDOABLE_EVENT]: [snapshot(), valueSetter("foo")]
```

### Event Handlers
### Defining Interceptors

Configuration of events and effects (collectively known as interceptors) can be provided upon initialization of the EventBus as objects with named keys (constants) and values that communicate with the bus.

The idea of **event** handlers is being responsible to assign parameters to side effects, rather than executing effects *themselves*, is again mainly to do with the DRY-principle, instrumentation potential and performance. Most composed event handler chains are setup so that your "actual" main handler is last in line in the pre processing phase. If e.g. your event handlers would directly update the state atom, then any attached watches [(derived views, cursors, other subscriptions)](https://github.com/thi-ng/umbrella/tree/master/packages/atom#about) would be re-run each time. By assigning the updated state to, e.g., an `FX_STATE` event, we can avoid these interim updates and only apply the new state once all events in the current frame have been processed. Furthermore, a post interceptor might cancel the event due to validation errors etc.
#### Signatures

#### Events vs Effects:
Interceptor values have the following valid signatures:

To briefly summarize the differences between event handlers & effects:
```js
{
I_FACTORY: (_, [__, args]) => ({ FX_KEY: [ ... ], FX_KEY2: (args) => {} }),
I_FACTORY2: (_, [__, args], bus, ctx) => { ... return { FX_KEY: ... } },
I_ARRAY: [ interceptor1(), () => ({ ...interceptors }) ],
I_ARRAY_VERBOSE: [ { pre: () => ({}), post: () => ({}) } ]
}
```

Event handlers are triggered by events, but each event handler is technically a chain of interceptors (even though many are just a single item). Even if you just specify a single function, it's internally translated into an array of interceptor objects like:
The reason for the `_` and `__` args is just a convention for saying "I don't need these arguments". You will see this often throughout the sourcecode, but here are the arguments that are passed to your interceptor factory [function signature](http://bit.ly/interceptor_signature) by the `EventBus`:

```js
{ INTERCEPTOR: (state, [event_key, event_args], bus, ctx) => ({ ... }) }
```
valueSetter("route") -> [{ pre: (...) => {[FX_STATE]: ...}, post: undefined }]

To sum up, interceptors can be defined as:

- factories
- an array of factories
- an array of explicitly defined interceptor objects (see Interceptor Object Syntax below)
- a nested array of any of the above

#### Commented Source:

- Interceptor [config options](http://bit.ly/evs_config)
- Example: [I_FACTORY2](http://bit.ly/ev_factory2)
- Example: [I_ARRAY](http://bit.ly/ev_array)

### Expanded Interceptor Syntax

If you'd like to have lower level control over ordering of interceptors, you can use this expanded syntax for such factories:

```js
{
I_EXPANDED: [ { pre: () => ({}), post: () => ({}) }, { pre: () => ({}), post: () => ({}) } ],
}
```

When processing an event, these interceptors are then executed first in ascending order for any pre functions and then backwards again for any post functions (only if there are any in the chain). So if you had defined an handler with this chain: `[{pre: f1, post: f2}, {pre: f3}, {pre: f4, post: f5}]`, then the functions would be called in this order: f1, f3, f4, f5, f2. The post phase is largely intended for state/effect validation & logging post-update. I.e., interceptors commonly need `pre` only.
Each event/effect handler is technically a chain of interceptors even though many are just a single item. I.e., they are automatically converted from their shorthand:

`I: () => ({})`

into:

`I: [{pre: () => ({}), post: undefined}]`

When processing an event/effect, these interceptors are then executed first in ascending order for any `pre` functions and then backwards again for any `post` functions defined. So if you had defined an handler with this chain:

`[{pre: f1, post: f2}, {pre: f3}, {pre: f4, post: f5}]`

then the functions would be called in this order:

`f1 -> f3 -> f4 -> f5 -> f2`

The post phase is largely intended for state/effect validation & logging post-update. I.e., interceptors commonly need `pre` only and so provide the unwrapped factory sugar. Event handlers should be pure functions (returning referentially transparent data immediately) and only side effects (see Effects below) execute any "real" work, which are triggered by events automatically when returned from their factories

Like with [`trace()`](https://github.com/thi-ng/umbrella/blob/master/packages/interceptors/src/interceptors.ts#L21) some interceptors DO have side effects, but they're really the exception to the rule. For example, `snapshot()` is idempotent since it only records a new snapshot if it's different from the last and `trace()`, but is typically used during development only - its side effect is outside the scope of your app (i.e. the console).


### Event Handlers

The idea of *event* handlers is being responsible to assign parameters to side *effect* handlers, rather than executing effects themselves, is again mainly to do with the DRY-principle, instrumentation potential and performance. Most composed event handler chains are setup so that your "actual" main handler is last in line in the `pre` processing phase. If e.g. your event handlers would directly update the state atom, then any attached watches [(derived views, cursors, other subscriptions)](https://github.com/thi-ng/umbrella/tree/master/packages/atom#about) would be re-run each time. By assigning the updated state to, e.g., an `FX_STATE` event, we can avoid these interim updates and only apply the new state once all events in the current frame have been processed. Furthermore, a `post` interceptor might cancel the event due to validation errors etc.

Events are always triggered and run before any side-effects are triggered.

#### Built-in Event Handlers

Built-ins for default (stateful) `EventBus`:

```js
[EV_SET_VALUE]: (state, [_, [path, val]]) => ({ [FX_STATE]: setIn(state, path, val) }),
[EV_UPDATE_VALUE]: (state, [_, [path, fn, ...args]]) => ({ [FX_STATE]: updateIn(state, path, fn, ...args) }),
[EV_TOGGLE_VALUE]: (state, [_, path]) => ({ [FX_STATE]: updateIn(state, path, (x) => !x) }),
[EV_UNDO]: undoHandler("undo"),
[EV_REDO]: undoHandler("redo")
```

### Effect Handlers

Effects are where the 'real work' happens. This is where you do your I/O, UI updates, etc..

#### Built-in Effect Handlers

```js
// Execute next frame:
[FX_DISPATCH]: [([ev_key, ev_val]) => bus.dispatch([ev_key, ev_val]), -999]
// Execute this frame (if triggered w/in bus context):
[FX_DISPATCH_NOW]: [([ev_key, ev_val]) => bus.dispatchNow([ev_key, ev_val])]
// Execute on frame following success:
[FX_DISPATCH_ASYNC]: [([ev_key, arg, success, err], bus, ctx) => {
const fx = bus.effects[ ev_key ] // calls effect handler for constant
if (fx) {
const p = fx(arg, bus, ctx);
if (isPromise(p)) {
p.then((res) => bus.dispatch([success, res])).catch((e) => bus.dispatch([err, e]));
} else {
LOGGER.warn("async effect did not return Promise");
}
} else {
LOGGER.warn(`skipping invalid async effect: ${ev_key}`);
}
},
-999
]
// Synchronous effects:
[FX_CANCEL] // -> toggles a boolean internal to the bus to cancel all queued interceptors
[FX_STATE] // -> takes an atom/history function (e.g., updateIn) and applies it to the state
// BUILT-IN PROMISES WHICH CAN BE USED AS FIRST ARGUMENT TO FX_DISPATCH_ASYNC:
[FX_DELAY]: [([x, body]) => new Promise((res) => setTimeout(() => res(body), x)), 1000]
[FX_FETCH]: [(req) => fetch(req).then((resp) => { if (!resp.ok) { throw new Error(resp.statusText) } return resp }), 1000]
```

![but why](http://www.reactiongifs.com/r/but-why.gif)

### Great, but why?
Expand Down