super-effects
v0.0.0-next.9
Published
> ⚠️ This is very unstable, please do not use this in production. Contributions welcome!
Downloads
10
Readme
super-effects
⚠️ This is very unstable, please do not use this in production. Contributions welcome!
This is the core engine for a reactive effects system that makes use of generators.
It is not intended to be used directly as it is abstract enough to support different observable formats.
What does it do?
It works a bit like useEffect
in React, except instead of having a dependencies array you yield
things you want to depend on.
If any of your dependencies change the effect restarts.
There's various meta hooks you can use to listen to the different of the effect's lifecycle. You can also make anything a reactive dependency using a simple observable API.
Why is it so abstract?
The core principle could be applied in many different contexts, in different view frameworks, or even server-side. We wanted to know the basic logic of the effects system was rock solid so we've put it in its own repo and written a lot of tests to verify different corner cases work as expected.
Why generators?
Generators allow us to do alot of things that aren't possible otherwise. E.g.
- Synchronously tracking references to dependencies
- Early cancellation of an execution context (for e.g. restarts, component unmounts)
- Add custom handlers for custom data types that are yielded
API
fx.createEffect(...)
let api = createEffect<T>(options: Options): API<T, Initial>
type Options = {
handlers: Handler[]
, visitor: (ctx: Initial) => Generator<any, U, any>
, interceptNext?: (it: Iterator<any, U, any>, next: any) => any
}
Create an effect by providiing a visitor
GeneratorFunction and a list of handlers
.
Handlers let you control how yielded values are handled. E.g. if you want to add support for a particular stream library you might add a handler like so:
import * as fx from 'super-mithril-effects'
let effectApi = createEffect({
visitor: function * (){
let a = yield myStream
let b = yield otherStream
},
handlers: [streamHandler]
})
Where streamHandler
looks like this:
const streamHandler = (x) => {
if (!isStream(x)) {
return fx.SKIP
}
return (onchange) => {
let mapper = x.map(onchange)
return () => {
mapper.end(true)
}
}
}
interceptNext
allows you to control the execution of iterator.next(nextValue)
We provide this API so we can track creation and references of streams and stores, but you may also use it just for simple logging / debuging.
let effectApi = createEffect({
...,
interceptNext(it, next){
console.log('injecting', next)
const value = it.next(next)
console.log('got back', value)
return value
}
})
effectApi
export type API<U = any, Initial = U> = {
start(initial?: Initial): void
addDependency(x: any): void
stop(): void
resume(): void
context: EffectContext<U>
on(cb: (x: U) => void): (x: U) => void
off(cb: (x: U) => void): void
running: Promise<void>
onReplace(fn: onReplace): void
onReturn(fn: onReturn): void
onThrows(fn: onThrow): void
onFinally(fn: onFinally): void
}
This is what createEffect
returns.
effectApi.start(...)
Starts the generator function and handler. Calling this function when the generator is already started does nothing.
effectApi.stop(...)
Stops the generator function and handler and cleans up all listeners. Calling this function when the generator is already stopped does nothing.
effectApi.resume()
Will resume any blocking yield ctx.pause()
effects.
effectApi.on
Subscribe to the return value of a generator.
const cb = x => console.log(x)
effectApi.on( cb )
effectApi.off
Unsubscribe to the return value of a generator.
const cb = x => console.log(x)
effectApi.off( cb )
effectApi.running
A promise that resolves when the effect is no longer running.
effectApi.addDependency
Manually add a dependency that will restart the generator when it emits 2 times. Just like a normal yield
the first emit is assumed to be the current value and is valid in the current iterator session. The second emit invalidates the current session and triggers a restart.
addDependency
uses the same handler list that you pass in on initialization.
effectApi.context
The effect context is designed to be accessible to the framework user. You may want to pass it in as the initial
value to effectApi.start(...)
along with other framework or application specific context.
The context allows the user to subscribe to specific lifecycle events of the effect and use special utils like yield ctx.pause()
and yield ctx.sleep(5000)
.
export type EffectContext<U> = {
onFinally: (f: () => any) => void
onReplace: (f: () => any) => void
onReturn: (f: (data: U) => any) => void
onThrow: (f: (error: any) => any) => void
sleep: (ms: number) => SleepEffect
pause: () => PauseEffect
resume: () => void
encased<T>(fn: () => T): EncasedEffect<T>
}
EffectContext.onFinally
Called whenever the generator function is exiting. This could be due to any of the following:
- A restart of the generator due to a dependency changing
- An exception was thrown
- The generator completed and returned a value
- The effect is being teardown after
effectApi.stop()
was called
EffectContext.onReplace
Called whenever the generator is restarting and the current instance is being replaced.
This is called immediately before starting the new instance so you will still have access to the closure of the old instance.
EffectContext.onReturn
Called only after the generator has exited without error.
EffectContext.onThrow
Called only after the generator has exited via an exception being thrown.
EffectContext.sleep(ms)
Will pause the generator for the specified amount of ms
.
EffectContext.pause()
Will pause the generator from continuing execution. Useful if you want to wait for a dependency change to restart the effect.
Can be resumed via a dependency emit or calling api.resume()
Cancellation
When a dependency observable changes the current iterator is cancelled and restarted. The cancellation works by calling it.throw(new Cancellation())
If you have a try
/ catch
in your generator you can check if the error is a cancellation via err instanceof zed.Cancellation
. Note you shouldn't try to guard against cancellation, this library will stop iterating generators that have been cancelled, but you may want to rule out Cancellation
exceptions for logging / debugging purposes.
Typescript
Typescript doesn't really support generators very well. The good news is there are various open issues on the Typescript repo and it looks the core team takes the gap in support very seriously.
The best we can do in the meantime is manually cast our yields and pray for a better future.