function-tree

Install 

NPM

npm install function-tree

Description 

Function-tree is the what Cerebral extends to create its signal implementation. You can use this library on the server, or standalone in the client as a replacement for Cerebral. Basically a function-tree allows you to execute a tree of functions. You can use the Cerebral debugger to debug function tree execution in any JS environment.

Function-tree is somewhat in the same family as Rxjs and Promises. The main difference is that Rxjs and Promises are based on value transformation. That means only the value returned from the previous function is available in the next. This works when you indeed want to transform values, but events in your application are rarely about value transformation, they are about running side effects and going through one of multiple execution paths. Function tree embraces the fact that most of what we do in application development is running side effects.

Rxjs and Promises are also about execution control, but neither of them have declarative conditional execution paths, you have to write an _IF_ or SWITCH statement or decouple streams. With function tree you are able to diverge the execution down paths just as declaratively as functions. This helps readability.

Instantiate 

import FunctionTree from 'function-tree'
// Node:
// const FunctionTree = require('function-tree').FunctionTree

const ft = new FunctionTree({
  // add side effect libraries to context
})

ft
  .run(
    [
      // returns a promise
      function someFunc(context) {},
      function someOtherFunc(context) {}
    ],
    {
      foo: 'bar' // optional payload
    }
  )
  .catch((error) => {
    // Current payload with execution details,
    // can be passed in to a new execution (will be indicated in debugger)
    error.payload

    // A serialized version of the error. Name, message and stack, or custom error serialization
    error.payload.error
  })

You can also add multiple custom context providers by using an array:

const ft = new FunctionTree({
  // add side effect libraries to context
  uuid,
  axios
})

errors 

FunctionTreeError (base) 

import {FunctionTreeError} from 'function-tree'
// Node:
// const FunctionTreeError = require('function-tree').FunctionTreeError

// Error structure
{
  name: 'FunctionTreeError',
  message: 'Some function-tree error'
  stack: '...'  
}

FunctionTreeExecutionError 

import {FunctionTreeExecutionError} from 'function-tree'
// Node:
// const FunctionTreeExecutionError = require('function-tree').FunctionTreeExecutionError

// Error structure
{
  name: 'FunctionTreeExecutionError',
  message: 'Some execution error'
  execution: {name: 'someName'},
  funcDetails: {name: 'someFunction', functionIndex: 5},
  payload: {foo: 'bar'},
  stack: '...'  
}

devtools 

Download the function tree standalone debugger for Mac, Windows or Linux.

import FunctionTree from 'function-tree'
import Devtools from 'function-tree/devtools'
// Node:
// const FunctionTree = require('function-tree').FunctionTree
// const Devtools = require('function-tree/devtools').Devtools

const devtools = new Devtools({
  // Set url of remote debugger
  host: 'localhost:8585',

  // By default debugger tries to reconnect when it is not active
  reconnect: true,

  // By default devtools connects to "ws://". This option should be set to true
  // when browser operates on https. Follow debugger instructions for
  // further configuration
  https: false
})
const ft = new FunctionTree([])

// Add your function tree to the debugger
devtools.add(ft)

// If you are not going to use it anymore, remove it
devtools.remove(ft)

// Remove all function trees from debugger
devtools.destroy()

sequence 

You can use an array literal to define a sequence of functions.

function someFunction(context) {}
function someOtherFunction(context) {}

module.exports = [someFunction, someOtherFunction]

Or you can be explicit by using the sequence function:

import { sequence } from 'function-tree'
// Node:
// const sequence = require('function-tree').sequence

function someFunction(context) {}
function someOtherFunction(context) {}

module.exports = sequence([someFunction, someOtherFunction])

The first argument to sequence can be a string, which names the sequence. This will be shown in the debugger. If it is the root sequence it will be used as the name of the execution itself.

import { sequence } from 'function-tree'
// Node:
// const sequence = require('function-tree').sequence

function someFunction(context) {}
function someOtherFunction(context) {}

module.exports = sequence('My awesome sequence', [
  someFunction,
  someOtherFunction
])

parallel 

import { parallel } from 'function-tree'
// Node:
// const sequence = require('function-tree').parallel

function someFunction(context) {}
function someOtherFunction(context) {}

module.exports = parallel([someFunction, someOtherFunction])

Even though someFunction returns a Promise, someOtherFunction will be run immediately.

context 

path 

The path is only available on the context when the function can diverge the execution down a path.

import FunctionTree from 'function-tree'
// Node:
// const FunctionTree = require('function-tree').FunctionTree

function funcA(context) {
  context.props.foo // "bar"

  return context.path.pathA({ foo2: 'bar2' })
}

function funcB(context) {
  context.props.foo // "bar"
  context.props.foo2 // "bar2"

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ foo3: 'bar3' })
    }, 100)
  })
}

function funcC(context) {
  context.props.foo // "bar"
  context.props.foo2 // "bar2"
  context.props.foo3 // "bar3"
}

const ft = new FunctionTree({})
const tree = [
  funcA,
  {
    pathA: [funcB, funcC],
    pathB: []
  }
]

ft.run(tree, { foo: 'bar' })

props 

import FunctionTree from 'function-tree'
// Node:
// const FunctionTree = require('function-tree').FunctionTree

function funcA(context) {
  context.props.foo // "bar"
}

const ft = new FunctionTree()
const tree = [funcA]

ft.run(tree, { foo: 'bar' })

error 

import FunctionTree from 'function-tree'
// Node:
// const FunctionTree = require('function-tree').FunctionTree
const ft = new FunctionTree({})

// As an event (async)
ft.on('error', function(error, execution, payload) {})

// As callback for single execution
// Triggers sync/async depending on where error occurs
ft.run(tree, (error) => {})

// As callback (sync)
ft.run(tree, (error, execution, payload) => {
  if (error) {
    // There is an error
  }
})

Provider 

You can create your own custom providers which exposes the domain specific API you need for your application. You can also use it to set defaults for side effects, or even just transform the API more to your liking. With a Provider the debugger will know about the executions and inform you which functions executed them.

import axios from 'axios'
import { Provider } from 'function-tree'
// Node:
// const FunctionTree = require('function-tree').Provider

const http = new Provider({
  get(...args) {
    return axios.get(...args)
  }
})

const api = new Provider({
  getArticles() {
    return this.context.http.get('/articles')
  }
})

const ft = new FunctionTree({
  http,
  api
})

tags 

Tags gives you more power to write declarative code.

createTemplateTag 

Allows you to add new tags, targeting your custom providers.

import { createTemplateTag } from 'function-tree'

export const myTag = createTemplateTag('myTag', (path, context) => {
  return context.myProvider.get(path)
})

Example use

someChain = [set(props`foo`, myTag`other.path`)]

extractValueWithPath 

Easy way to support nested values extraction from objects using the path.

import { createTemplateTag, extractValueWithPath } from 'function-tree'

export const myTag = createTemplateTag('myTag', (path, context) => {
  return extractValueWithPath(context.something, path)
})

props 

Targets the props passed into the execution of a function tree:

import { props } from 'function-tree/tags'

export default [shout(props`text`)]

resolveObject 

Wraps an object passed to a function factory, resolving any template tags.

import { props, resolveObject, string } from 'function-tree'

const mychain = [
  setOptions(resolveObject({ foo: props`foo`, bar: string`bar` }))
]

ResolveProvider 

By default your function tree instance includes the ResolveProvider. It allows you to evaluate tags.

function myFunctionFactory(someValue) {
  function myFunction({ resolve }) {
    // Evaluates to the contextual value's value, if not a contextual value
    // it will just return the value.
    resolve.value(someValue)

    // Evaluates the path in the tag (someValue must be a Tag).
    resolve.path(someValue)

    // Evaluates if variable is a tag. We can optionally pass an array of
    // types such as ['props', 'string'] as second argument.
    resolve.isTag(someValue)

    // Evaluates if variable is a ResolveValue. Useful when you have created custom
    // stuff to be evaluated in tags or other, extending ResolveValue class.
    resolve.isResolveValue(someValue)
  }

  return myFunction
}

ResolveValue 

Allows you to create a custom tag value container.

import { ResolveValue } from 'function-tree'

class Uppercaser extends ResolveValue {
  constructor(tag) {
    super()
    this.tag = tag
  }
  // Required method
  getValue(context) {
    return this.tag.getValue(context).toUpperCase()
  }
}

export default (tag) => {
  return new Uppercaser(tag)
}

Example use

import uppercaser from './uppercaser'

export default [shout(string`Hello, ${uppercaser(props`name`)}`)]

string 

Returns the evaluated string.

import { string, props } from 'function-tree/tags'

export default [shout(string`Hello ${props`name`}`)]

events 

The execute function is also an event emitter.

import FunctionTree from 'function-tree'
// Node:
// const FunctionTree = require('function-tree').FunctionTree

const ft = new FunctionTree([])
const tree = [funcA]

// When an error is thrown, also catches promise errors
ft.on('error', (error, execution, payload) => {})

// When a function tree is executed
ft.on('start', (execution, payload) => {})

// When a function tree execution has ended
ft.on('end', (execution, payload) => {})

// When a function tree goes down a path
ft.on('pathStart', (execution, payload) => {})

// When a function tree ends execution of a path
ft.on('pathEnd', (execution, payload) => {})

// When a function in a function tree starts executing
ft.on('functionStart', (execution, functionDetails, payload) => {})

// When a function in a function tree stops executing
ft.on('functionEnd', (execution, functionDetails, payload) => {})

// Triggers when an async function has been run
ft.on('asyncFunction', (execution, functionDetails, payload) => {})

// When a parallel execution is about to happen (array in array)
ft.on('parallelStart', (execution, payload, functionsToResolveCount) => {})

// When a function in parallel execution is done executing
ft.on(
  'parallelProgress',
  (execution, payload, functionsStillResolvingCount) => {}
)

// When a parallel execution is done
ft.on('parallelEnd', (execution, payload, functionsExecutedCount) => {})

ft.run(tree)