Corrie
Scared of side effects? Corrie is here to save the day!
When a function does not behave, all things could go wrong:
{ }
Corrie employs coroutines (generators) to make functions with side effects nicer:
{ }
sleep
, call
and print
here are effect factories, simple pure functions that return plain JavaScript objects called effects, like this one:
effect: 'sleep' duration: 1000
A coroutine yields such objects instead of calling effectful functions directly. These effect objects work as instructions that Corrie picks up and sends to a corresponding effect handler. The effect handler fulfills the instruction and then either resumes the coroutine or stops the execution.
Apart from yielding an effect object, a coroutine can also yield
, return
or throw
a non-effect value. These actions are considered a special kind of effects, and are also proccesed by effect handlers. There are a few effects built in Corrie, and you can register your own effect handlers both for regular and special effects.
Coroutines with declarative effects are easy to write and read, and especially to test: you can simply iterate over the yielded effects and check if they are what you expect them to be. This approach also gives you more control over the execution flow.
If you are coming from the React/redux world, Corrie can be described as redux-saga sans the redux parts.
Installation
npm install --save corrie
Usage
const corrie = const print = // This is a pure function { return `, `} // This is an effectful coroutine { // By default, yielding a non-effect value will resolve it let addressee = Promise // Pure functions can be used directly let fullMessage = // Since printing a string is a side effect, yield it } // Wrap the coroutine and use it like a regular functionlet talk = // Outputs "World, hello!" // Outputs "World, bye!"
Composition
yield*
Using To invoke a coroutine and yield its effects from another coroutine as if they were yielded directly, use yield*
:
{ } { // in this case it's the same as `yield print(2)` } // Outputs "1 2 3"
Middleware Pipelines
There is also a special way to compose coroutines that allows you to create pipelines of middleware similar to those from koa. To use it, pass more than one coroutine to Corrie, and yield a next
effect from them to pause the current coroutine and invoke the next one in the queue.
{ console let result = console return result} { let result = return result} { return string += '-' + string)} let pipeline = let result = console // hoottooting: 201.949ms// hoot-toot
The next
effect passes its arguments to the next handler in the queue and returns the result of its invocation. With this technique, the execution first goes downstream, then upstream, allowing every handler to both preprocess the arguments and postproccess the result of the execution.
This approach is very flexible and allows you to build middleware layers, method hooks, plugin systems and other awesome things.
Built-in Effects
Corrie has several effects built in it, which you can use by importing the needed effect factories like so:
const { call, sleep } = require('corrie/effects')
Effects marked as async or potentially async trigger an error in the sync
mode.
Effects marked as special are invoked by using JavaScript statements with values that are not effect objects.
yield
special
This effect is triggered when a non-effect value is yielded from a coroutine. By default, it is processed by the resolve
handler.
{ // These yields will be handled by the `yield` effect handler, // and by default work as `yield resolve(value)` 11 Promise // And these two won't: effect: 'sleep' duration: 100 }
return
special
This effect is triggered when a value is returned from a coroutine. By default, it marks the execution as complete, resolves the value and returns it.
{ return 322 // triggers the effect handler}
throw
special
This effect is triggered when a value is thrown from a coroutine. By default, it works as a regular throw.
{ throw 'ALARM!' // triggers the effect handler}
call(fn, ...args)
or call([context, fn], ...args)
potentially async
Invokes a function with the provided context (if given) and arguments. If the returned value is a promise, waits for it to resolve. Returns the result.
// This won't work in the `sync` mode:
fork(fn, ...args)
or fork(mode, fn, ...args)
async
Executes the given function as a new Corrie routine using the effect handlers, state and context of the current execution. If the mode parameter is not specified, it is set to "auto".
let promise = let result = // wait for the fork to complete // 1, 3, 2, 4
resolve(value)
potentially async
Resolves the provided value (e.g. a promise) and returns the result. This is the default effect for yielding a non-effect value.
// resolves to 1 Promise // resolves to 2 3 // resolves to 3
sleep(duration)
async
Pauses the invocation for duration
milliseconds, then resumes it.
getResume()
Returns a function that resumes the invocation. Although useless by itself, getResume
can be used together with suspend
to continue the invocation from outside.
{ console let rsm = let result = console console} // Here `rsm` is the resume functionlet rsm = // hoot!// hooting: 503.228ms
This technique can be used to build such advanced interfaces as the query builder.
suspend(value)
Pauses the invocation of the chain and returns the provided value to the outside. See getResume
for the example.
next(...args)
When executing a composition of middleware coroutines, pauses the current handler and invokes the next one in the queue passing the provided arguments. Once the next handler is finished, returns its result.
By default, next
may return undefined
both as the result value of the next handler and when there is no more handlers in the queue. To get a different value for the no-more-handlers case, use the or(value)
method of the effect:
Settings
Along with coroutines and theirs arguments, Corrie accepts a settings object. There are two ways to pass settings to Corrie:
- As the only argument to the Corrie function:
let newCorrie = corrie(settings)
. It will return a new Corrie function bound to the settings.
let syncCorrie = let asyncCorrie = { // using async effect } // throws an error // prints "Hey!"
- As the first argument along with coroutines:
corrie(settings, coroutine)
. It will instantly invoke the Corrie function with the provided settings and wrap the coroutines.
let coroutine =
Custom Effects
You can add your own effects by registering their handlers in a settings object with an effectHandlers
property. Effect factories don't need to be registered, they are merely a nicer user-land way to create effect objects.
Here is an example of using custom effects with Corrie:
const corrie = const sleep = const buildHandler destroyHandler = const build destroy = const settings = effectHandlers: build: buildHandler destroy: destroyHandler return: buildHandler // use the build handler for returned values const coroutine = { } // Reinstantiate corrie with the new settings for repeated useconst customCorrie = // or use them right away
For examples of effect handlers, see the built-in ones.
Execution Modes
Corrie supports different execution modes that affect how it treats promises. You can pass a mode as a setting (e.g. corrie({ mode: 'async'}, ...)
) or use the corresponding method of the main Corrie function.
auto
It is the default mode used when you invoke the main Corrie function. In this mode:
- The execution starts synchronously
- Promises and async effects are allowed
- Promises returned from effects are resolved, and the result is returned to the coroutine
- The return value of the execution can be both a promise and a regular value
// The result is a regular valuelet result = 1 // The result is a promise because `sleep` is an async effectlet promise = 2
You can also use the auto
mode explicitly: corrie.auto(...)
or corrie({ mode: 'auto' })
.
async
- The execution starts asynchronously
- Promises and async effects are allowed
- Promises returned from effects are resolved, and the result is returned to the coroutine
- The return value of the execution is always a promise
let double = corrie // prints "4"
sync
- The execution starts synchronously
- Resolving promises and using async effects is disallowed
- Unresolved promises returned from effects are allowed
- The return value of the execution must not be a promise
// This will throw an error
State
The state is a JavaScript object with arbitrary properties attached to a Corrie execution. Effect handlers can use this object to store some data. The built-in effects don't use the state, so it is only useful with custom effects.
You can pass a state using the "state" property in settings:
// Every coroutine wrapped using this function will have the same initial statelet corrieWithState = // In this case the state is used in one particular coroutine
Credits
yelouafi for redux-saga