gimgen is a javascript micro-library that uses generators to invert program flow and generate coroutines. This is especially useful when you need to make decisions based on a stream of events that come from multiple sources.
So its kind of like js-csp but smaller, and focused on control flow rather than on channels.
So its kind of like a limited form of functional reactive programming with baconjs but you can use javascript constructs for control flow
For examples check out the demo page
Interactive slide deck explaining many of the underlying concepts
I do not yet know exactly what I have here. It's interesting, it feels very useful, and it allows me to create very complex code that works correctly on the first attempt far more often than it has any right to.
However, I hesitate to promote it for use in production as it simply hasn't been very vetted by very many people yet. Please help and share your opinions on the issue tracker or by contacting me!
gimgen comes with a umd wrapper and is usable with amd, commonjs, or globals. While it is written with es2015, it is transpiled to regular es5. It should therefore work in any environment that supports or transpiles to javascript generators.
The key to understanding how this library works is to understand javascript generators.
While we won't go into this in depth, generators are fundamentally this
- A generator declaration (the
function *
thing) gives you a function that when invoked returns an iterator - That iterator has
.next()
method. - Every time
iterator.next()
is invoked, the function executes until it hits a yield keyword. If the yield keyword is passed a value on its right hand side (egyield 5
),iterator.next()
returns that value - The next time
iterator.next()
is called, the function runs starting with where it left off at the previous yield keyword.
If you've ever looked into how libraries like bluebird, co,
or the await
keyword work, you realize that that all they do is that if iterator.next()
returns a promise, the library/language will wait
until that promise resolves before invoking next()
again.
co(function * () {
consle.log(`starting`) //run this
const customers = yield $.get(`/customers`) //start query, yield promise. co waits for promise to resolve until resuming
const bestCustomer = customers[0] //this runs after the first next() call
const details = yield $.get(`/customer/${customers[0].id}/details`) //again start query, yield promise.
console.log(details) //co runs this after the second next() call
}) //done - returns a promise that resolves when execution finishes
gimgen simply builds on this concept by providing some structure around the returned promises. Rather than return a simple promise,
when wrapping a generator with gimgen you return signals where a signal is an object with a createPromise
method. Because signals are promise factories they can be yielded multiple times (enabling looping) and since they are objects, they
can maintain state and (varying what happens when createPromise
is called).
This has some very powerful consequences.
The core function to start using gimgen is
gimgen(generator) -> function
This takes a generator and returns a function. Invoking the function will start an instance of the generator and running through its steps. The generator should yield back signals. So the following will start two coroutines that will each log every 300 and 2000 milliseconds respectively
const { gimgen, timeoutSignal } = window.gimgen
const logTimes = gimgen(function * (name, frequency) {
const elapsed = timeoutSignal(frequency)
while(true) {
yield elapsed
console.log(`${name}: says hi at ${Date()}`)
}
})
logTimes("logger one", 300)
logTimes("logger b", 2000)
In the case where you would like to start the coroutine immediately, a function is provided that will simply start it running
runGimgen(generator) -> void
At its heart a signal is a simple object with a createPromise
method which returns any then-able object.
{ createPromise: () -> Promise }
gimgen comes bundled with many useful signals. The below are signal factory functions. Invoking these will return a signal object. This signal object can then be yielded back within your methods. See demos for usage examples of the below
timeoutSignal(milliseconds)
- emits X milliseconds after being yieldeddomEventToSignal(domNode, eventName)
- emits the next time the given event occurs after being yielded. The signal has agetLastEvent()->Event
method which is useful for accessing the dom event object.promiseToSignal(promise)
- when yielded will emit when the wrapped promise becomes resolved or immediately if the promise is already resolvedmanualSignal()
- returns a signal object with a.trigger(x)
method. When yielded emits the next time the object'strigger()
method is invoked. The first parameter to the trigger is returned by the yield.anySignal(...signals)
- when yielded, returns the ifrst of the passed in signals to occur in this structure:{signal, result}
wheresignal
is a reference to the signal that happened, andresult
is its last payload. Useful when one of several things might happen next. (See demos for usages)controlSignal(function * ({emit}))
- Takes a generator that is a gimgen coroutine. This will start executing immediately. When yielded, the signal will emit whenever the coroutine invokes the injectedemit
method. This is very useful for aggregating events. (see ping pong demo)
If you would like to create your own signal factories gimgen provides a convenience method createSignalFactory
which takes one of two forms
createSignalFactory(name, ({state, setState}, ...args)-> Promise) -> (() -> signal)
This takes a name (used for a toString
implementation) and a createPromise
function. It returns a function that when invoked will create your signal
export const timeoutSignal = createSignalFactory('timeoutSignal', (_, ms) =>
new Promise(resolve => setTimeout(resolve, ms) )
)
The other form is an en extension of this where you pass in a configuration object containing a createPromise
function
createSignalFactory(name, {
createPromise: ({state, setState}, ...args)-> Promise,
getInitialState?: ({}) -> state,
...any other functions..
}) -> (() -> signal)
The first parameter into any functions on the configuration object will always be an object with a state
and setState
property to read the current state of the signal and to adjust it respectively.
An additional getInitialState
function can be provided to set the state when a signal instance is created.
Any other functions or fields on the configuration object will be copied to each signal instance with all functions being re-bound to receive the state object as the first parameter before any others
export const manualSignal = createSignalFactory('manualSignal', {
getInitialState: () => [],
createPromise: ({state: toNotify, setState}) =>
new Promise(resolve => setState([resolve, ...toNotify])),
trigger: ({state: toNotify, setState}, ...args) => {
if(args.length > 1)
setState([])
toNotify.forEach(fn => fn(...args))
}
})
A primary motivation for gimgen was as an experiment to find a generator-based way of implementing common functional helpers
such as throttle
and debounce
. In order for this to be possible, we must be able to generate both a signal and a regular javascript function the invoking of
which will cause a signal to emit.
To assist with this we have
invokableGimgen(({invokedSignal}) => generator) -> function
invokeableGimgen
itself returns a function. Unlike other gimgen runners, it doesn't take a generator directly but rather a closure that
returns a generator ({invokedSignal}) => generator
. Within the generator you are free to yield invokedSignal
which will be a signal that emits
whenever the returned function runs.
For sample usages see function decoration demo and gimgen.implementations file.