Define business logic in cancellable, forkable, asynchronous javascript generator definitions and promises


Keywords
generator, task, sequence, cancellable, asynchronous, promise, process
License
MIT
Install
npm install iterffect@1.0.4

Documentation

iterffect

Short for "iterable with effects" is a tool that allows you to define logic using the power of iterables, promises and observables alike.

It solves the following common issues:

  • I want to check if an asynchronous task should keep going given external events have occurred, should I be checking for this constantly after every single call?
  • I want to be able to effortlessly stop asynchronous tasks from blocking other tasks.
  • I want to keep track of what is happening in my app without necessarily storing data in a global state.

Use case

You're given the Seq class to instantiate sequences, these will be where you define business logic using generator functions.

function* main() {
  // this call returns a promise, the sequence will wait for it to finish
  const user = yield getUser()
  while (true) {
    const data = yield fetchData()
    yield delay(1000)
  }
}

const seq = new Seq(main())

In this example two things stand out:

  • while(true): Wouldn't this cause an infinite loop that can't be stopped? No, well at least not necessarily, you can always cancel sequences, preventing them from going forever, this is something that you can't do using async/await.
  • delay(1000): This is an effect, the API provides a list of effects you can use to control the flow of the application, this one will block the sequence for 1000 ms.

API

new Seq(input: Sequenceable, ctx: object)

Creates a new sequence from the given input and starts running it immediately. The ctx is an object that can be accessed by the sequence and by all child sequences of itself as well.

  • Seq#promise: Promise: a promise that resolves when the sequence (and all its children) finishes succesfully (no errors emitted), it will resolve if the sequence is cancelled, but with no result value.
  • Seq#cancel(): cancels the sequence (and all of its children).
  • Seq#spawn(input): creates a child sequence from the input
  • Seq#free(): cancel all the child sequences
  • Seq#running: Boolean: is the sequence still running.
  • Seq#cancelled: Boolean: was the sequence cancelled.
  • Seq#resolved: Boolean: was the sequence resolved.
  • Seq#rejected: Boolean: was the sequence rejected.

Seq.handler(listener: (value) => Sequenceable): Function

Creates a handler function that will run a new sequence from the result of calling gen with the given parameter. This sequence will be cancelled if the handler function is called again while its still running, making it so there can only be one running sequence at once.

The handler function will return a Promise that resolves to the last emitted value of the underlying sequences.

Example:

const handleClick = Seq.handler(function* (event) {
  //...
})

document.addEventListener(btn, 'click', handleClick)

yield <expression>: Sequenceable

Within sequences you can yield many kinds of sequenceable expressions, depending on the type of the expression the sequence will handle it differently:

  • Iterables: the sequence will start iterating over the given iterable.
  • Promises: the sequence will wait for the promise to resolve or reject.
  • Arrays: the sequence will handle every item of the array at the same time, useful for concurrency.

yield fork(input: Sequenceable): Seq

Creates a new non-blocking sequence from the given input and appends it to the sequence that spawns it. Useful when you need to keep track of child sequence execution.

yield race(...inputs: Sequenceable): Promise

Creates a sequence for each different input and races them, the first one to finish will return and cancels the rest.

yield delay(ms: number, [val]): Promise

A promise that resolves after the given ms, if val is specified, it will resolve to that value.

yield latest(observable: observable, listener: (value) => Sequenceable): Seq

Same as yield observable but if the observable emits a value before the latest sequence started by the previous value is still running, it will cancel it and start again with the new value.

yield cancel(seq: Seq)

Cancels the given sequence, same as seq.cancel().

yield forever()

Hangs the sequence forever.

yield teardown(input: Sequenceable, handler: Function): Promise

Allows you to define custom cancellation logic for your blocking calls. When your sequence is cancelled the cancellation propagates to all of its children but this doesn't prevent external logic from cancelling as well. This effect allows you to "subscribe" to the cancellation event and fire up the handler when it happens, this is useful to tell external logic that the sequence is no longer paying attention to its result and that it may stop what its doing.

Example:

// `request` adds an `.abort` method to its returned promise
function* upload() {
  const result = yield teardown(
    // this value is "thenable" and blocks the sequence
    request(url, file),
    // will called if this sequence is cancelled at any time
    req => req.abort()
  )
  // if the sequence is cancelled at this point the `.abort` method
  // wont be called
  return result
}

effect(payloadCreator, handler): (...args) => effect

Allows you to define your own effects

  • payloadCreator: (...args): A function that will transform ...args passed to the effect into a single payload, you should define your effect's function signature using this function.
  • handler: (sequence: Seq, payload: any): The handler function will be invoked when the effect is yielded by a sequence, the sequence is the sequence yielding the effect and the payload is the result of calling payloadCreator with the arguments passed to the effect when yielded. The handler can return a promise as well.

Example:

const race = effect(
  // payloadCreator: concats all parameters into an array
  (...inputs) => inputs,
  // handler: invoked with the result of the payloadCreator
  (seq, inputs) => {
  const children = inputs
    .map(item => new Seq(item, seq.ctx, seq))
  return Promise.race(children.map(child => child.promise))
    .then(result => {
      children.forEach(child => child.cancel())
      return result
    })
  }
)

Experimental API

yield select(selector: ctx => ctx)

Selects part or all of the context object, the selector function will be called with the current context and return the result back to the sequence.

yield reduce(reducer: ctx => ctx)

Calls the reducer function with the state, the result of that function will be Object.assign'd to the current context, modifying it.

Importing/requiring

import { Seq, effect } from 'iterffect'
import { fork, race, delay } from 'iterffect/effects'

Concepts

Child sequences

Sequences can have child sequences, these will affect how your sequence behaves. They can be created by effects such as fork, race or latest.

The following rules apply:

  • When a child sequence throws, so will the parent.
  • When a parent sequence cancels it will also cancel all of its children.
  • When a parent sequence finishes, it wont be resolved until all of its children finish as well.
  • Cancelling a sequence means it will never resolve or reject.

Error handling

Sequences can throw errors as well as handle them, for most use cases the handling of errors should be exactly like async/await functions do. However if a child sequence throws an error to its parent there's no try/catch to stop it, make sure to catch errors within the sequence, otherwise they would just propagate indefinetly.

Examples

Concurrent sequences

Wait for an amount of sequences to complete

function* main() {
  try {
    // resolves when the 3 are done
    const [user, news, notifications] = yield [
      getUser(),
      getNewsfeed(),
      getNotifications()
    ]
  } catch (err) {
    // if any of the 3 above emit an error will be handled here
  }
}

Forking sequences

function* pollNewsfeed() {
  while (true) {
    const news = yield getNewsfeed()
    // ...
    yield delay(1000)
  }
}

function* main() {
  // start non-blocking sequence
  const seq = yield fork(pollNewsfeed())
  yield delay(8000)
  // cancel the sequence after 8 seconds
  yield cancel(seq)
}