super-mithril-effects
TypeScript icon, indicating that this package has built-in type declarations

0.0.0-next.3 • Public • Published

super-mithril-effects

⚠️ This is very unstable, please do not use this in production. Contribution 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
	stop(): void
	context: EffectContext<U>
	on( cb: (x:U) => void ): (x:U) => void,
	off( cb: (x:U) => void  ): void,
	running: Promise<void>,
	addDependency(x: any): 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.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 passed in on initialization.

If you want to restart after the first emit you can do the lower level API: effectApi.restart() which requests a restart directly.

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 you to subscribe to specific lifecycle events of the effect and it gives you special utils like block and sleep.

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
	block: () => BlockEffect
}

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.block()

Will permanently block the generator from continuing execution. Useful if you want to wait for a dependency change to restart the effect.

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.

Readme

Keywords

none

Package Sidebar

Install

npm i super-mithril-effects

Weekly Downloads

1

Version

0.0.0-next.3

License

MIT

Unpacked Size

48.6 kB

Total Files

6

Last publish

Collaborators

  • jaforbes