Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace process

Provides utilities for running complex, multi-step asynchronous processes.

// esm
import { process } from '@paychex/core';

// cjs
const { process } = require('@paychex/core');

// iife
const { process } = window['@paychex/core'];

// amd
require(['@paychex/core'], function({ process }) { ... });
define(['@paychex/core'], function({ process }) { ... });

Overview

A process consists of 2 things:

  • a collection of actions to invoke
  • the logic for picking which actions should run at any given time

This abstraction enables both workflow-style processes (multiple steps running in parallel, with some steps dependent on the completion of earlier steps) as well as state machine-style processes (one state active at a time, with the next state determined by examining the process' current set of conditions).

You can even set up custom process logic by providing your own ProcessLogic instance to the process method. See the example here.

Action Methods

Each Action in a process can implement methods that the process will invoke at the appropriate time. These methods can be broken down into 2 categories:

  1. exec methods
  2. post-exec methods

The "exec" methods are run when the action is invoked. These include init, execute and retry.

The "post-exec" methods are run after the process completes. These include rollback, failure, and success.

IMPORTANT! The post-exec methods are run in parallel and at the same time that the ExecutionPromise returned by ProcessStart is resolved or rejected, meaning there is no guaranteed order between your process callbacks and your actions' post-exec methods.

Index

Type aliases

ProcessTransition: [string, string, any?]

An array containing the following values:

0 (string): the step name that just completed 1 (string): the step name to transition to 2 (iteratee): optional predicate function to run to determine if the transition should occur

see

lodash iteratee options

example
// automatic transition when first action ends
const transition = ['from step', 'to step'];
example
// transition if the machine's current conditions match this set
const transition = ['from step', 'to step', { condition: 'value', another: 'condition' }];
example
// transition if the machine's current conditions has a truthy value for 'property'
const transition = ['from step', 'to step', 'property'];
example
// transition if the machine's current conditions have a 'property' key with a 'value' value
const transition = ['from step', 'to step', ['property', 'value']];
example
// transition if the function returns true
const transition = ['from step', 'to step', function(conditions) {
switch(conditions.key) {
case 'value 1': return true;
case 'value 2': return conditions.another > 12;
default: return false;
}
}]
example
// transition if the current condition values match the corresponding predicates
import { conforms, isNil, isNumber } from 'lodash';

const transition = ['from step', 'to step', conforms({
'error': isNil,
'value': isNumber,
'property': (value) => value > 0 && value < 100
})];
ProcessTransitions: ProcessTransition[]

An array of ProcessTransition array instances.

see

lodash iteratee options

example
import { conforms, isNil, isNumber } from 'lodash';

const transitions = [

// automatic transition when first action ends
['from step', 'to step'],

// transition if the machine's current conditions match this set
['from step', 'to step', { condition: 'value', another: 'condition' }],

// transition if the machine's current conditions has a truthy value for 'property'
['from step', 'to step', 'property'],

// transition if the machine's current conditions have a 'property' key with a 'value' value
['from step', 'to step', ['property', 'value']],

// transition if the function returns true
['from step', 'to step', function(conditions) {
switch(conditions.key) {
case 'value 1': return true;
case 'value 2': return conditions.another > 12;
default: return false;
}
}],

// transition if the current condition values match the corresponding predicates
['from step', 'to step', conforms({
'error': isNil,
'value': isNumber,
'property': (value) => value > 0 && value < 100
})]

];

Functions

  • action(name: string, api?: Function | Partial<Action>): Action
  • Creates a fully realized Action for use within a process.

    example
    async function loadData() {
    // make a data call
    }

    function processResults() {
    // access this.results.load
    // value returned will be assigned
    // to this.results.process
    }

    const actions = models.collection();
    actions.add(process.action('load', loadData));
    actions.add(process.action('process', processResults));

    // "load" should transition to "process" automatically:
    const criteria = [ ['load', 'process'] ];

    export const start = process.create('get data', actions, process.transitions(criteria));

    Parameters

    • name: string

      The name of the process action.

    • Optional api: Function | Partial<Action>

      The execute method or partial Action to fill out.

    Returns Action

  • Returns a method you can invoke to begin a complex asynchronous process. The order of actions taken is determined using the ProcessLogic object passed as the last argument. You can use the built-in dependencies and transitions logic factories to create this object for you, or supply your own logic to create custom process behaviors.

    example
    // workflow

    import { loadUserInfo } from '../data/user';
    import { loadClientData } from '../data/clients';

    import { start } from '../path/to/machine';

    const actions = models.collection();

    actions.add(process.action('loadUserInfo', loadUserInfo));

    actions.add(process.action('loadClientData', {
    async execute() {
    const clientId = this.args[0];
    return await loadClientData(clientId)
    .catch(errors.rethrow({ clientId }));
    }
    }));

    actions.add(process.action('merge', {
    execute() {
    const user = this.results.loadUserInfo;
    const clients = this.results.loadClientData;
    return Object.assign({}, user, { clients });
    }
    }));

    actions.add(process.action('eula', function execute() {
    const conditions = this.results;
    return start('initial', conditions);
    }));

    export const dispatch = process.create('load user clients', actions, process.dependencies({
    'eula': ['merge'],
    'merge': ['loadUserInfo', 'loadClientData'],
    }));

    // USAGE: dispatch(clientId);
    example
    // state machine

    import { tracker } from '~/tracking';
    import { showDialog } from '../some/dialog';
    import { dispatch } from '../some/workflow';

    const actions = new Set();

    // if no start state is explicitly passed to the start()
    // method then this first action will be used automatically
    actions.add(process.action('start', {
    success() {
    tracker.event(`${this.process} succeeded`);
    },
    failure(err) {
    Object.assign(err, errors.fatal());
    tracker.error(err);
    }
    }));

    actions.add(process.action('show dialog', {
    execute() {
    return showDialog('accept.cookies');
    }
    }));

    // we can dispatch a workflow in one of our actions
    actions.add(process.action('run workflow', function execute() {
    const cookiesEnabled = this.results['show dialog'];
    const ignoreError = errors.rethrow(errors.ignore({ cookiesEnabled }));
    return dispatch(cookiesEnabled).catch(ignoreError);
    }));

    actions.add(process.action('stop', function() {
    this.stop(); // stop the machine
    }));

    const transitions = process.transitions([

    // show the dialog after starting the machine
    ['start', 'show dialog'],

    // only run the workflow if the user has not
    // logged in within the past 2 weeks
    ['show dialog', 'run workflow', function notRecentlyLoggedIn() {
    const TWO_WEEKS = 1000 * 60 * 60 * 24 * 7 * 2;
    const lastLogin = Date.parse(localStorage.getItem('lastLogin'));
    return lastLogin < Date.now() - TWO_WEEKS;
    }],

    // only if the above transition's condition returns
    // false (i.e. the user has recently logged in) will
    // this next transition will be evaluated; and since
    // this next transition always returns true, the machine
    // will always have a path forward
    ['show dialog', 'stop'],

    // if we did get into the "run workflow" state, make
    // sure we stop the workflow afterwards
    ['run workflow', 'stop']

    ]);

    export const start = process.create('intro', actions, transitions);

    Parameters

    • name: string

      The name of the process to run.

    • actions: Iterable<Action>

      An interable collection (e.g. Set, array, or ModelCollection) of Actions to run.

    • logic: ProcessLogic

      The logic that determines how to start and continue a process.

    Returns ProcessStart

    A method you can invoke to begin the process. The arguments will depend in part on the ProcessLogic object you passed.

  • Creates a ProcessLogic instance that can be passed to the process method. When started, the process will use the dependency map to determine which actions can be invoked immediately (having no dependencies) and which should be run when their dependent actions have completed.

    This method results in the process running like a workflow, with some actions run in parallel and the execution order of actions dependent upon the stated dependencies.

    example
    const dependencies = process.dependencies({
    'step b': ['step a'], // action b runs after action a
    'step c': ['step b', 'step d'] // action c runs after actions b and d
    });

    const actions = [
    process.action('step a', () => console.log('step a run')),
    process.action('step b', () => console.log('step b run')),
    process.action('step c', () => console.log('step c run')),
    process.action('step d', () => console.log('step d run')),
    ];

    export const dispatch = process.create('my workflow', actions, dependencies);
    example
    const actions = [
    process.action('start', function execute() {
    console.log('args:', this.args);
    }),
    process.action('parallel 1', function execute() {
    console.log('in', this.name);
    }),
    process.action('parallel 2', function execute() {
    console.log('in', this.name);
    }),
    ];

    const order = process.dependencies({
    'parallel 1': ['start'],
    'parallel 2': ['start'],
    });

    export const dispatch = process('my workflow', actions, order);

    // USAGE:
    // dispatch(123, 'abc');

    Parameters

    • deps: Record<string, string[]> = {}

      The dependency map that should be used to determine the initial and follow-up actions to invoke in this process.

    Returns ProcessLogic

    A ProcessLogic instance that process can use to determine how to run the ModelCollection actions it was provided.

  • run(item: Action, context: any, initialize?: boolean): Promise<any>
  • Utility method to run a single Action in isolation. This method is used internally by process but is made available publicly for unusual situations.

    NOTE: The success and failure methods will not be run using this method since their invocation depends on whether or not a collection of Actions has completed successfully. If you want to invoke the success and failure methods, you should do so manually. See the example for details.

    example
    const step = process.action('something', {
    count: 0,
    init() { this.count = 0; },
    execute() {
    console.log(this.args);
    this.count = this.count + 1;
    return this.count * this.factor;
    },
    success() {}, // must be invoked manually
    failure(err) {}, // must be invoked manually
    });

    export async function invokeSomething(...args) {
    const context = { args, factor: 3 };
    const promise = process.run(step, context);
    // invoke success and failure methods
    // on separate promise chain than the
    // one we return to callers; we don't
    // care if these fail and we don't want
    // their return values to override the
    // return value from the execute method
    promise.then(
    () => step.success.call(context),
    (err) => step.failure.call(context, err)
    );
    return await promise; // value returned by execute()
    }

    Parameters

    • item: Action

      The Action whose methods should be invoked.

    • context: any

      The context accessed using this within an action method.

    • initialize: boolean = true

      Whether to run the Action's init method.

    Returns Promise<any>

  • Creates a ProcessLogic instance that can be passed to the process method. When started, the process will use the transition criteria to determine which actions can be invoked based on the current set of conditions as passed to the start method or through calls to update.

    NOTE: A process using transitions logic will not stop until and unless one of the following occurs:

    • someone invokes the stop() method
    • someone invokes the cancel() method
    • a Action method throws an Error or returns a rejected Promise

    This method results in the process running like a state machine, with one action allowed to run at any time and the next action determined using the current conditions and the given transition logic.

    example
    const states = models.collection();

    states.add(process.action('start', () => console.log('start')));
    states.add(process.action('next', () => console.log('next')));
    states.add(process.action('stop', function() {
    this.stop();
    }));

    const criteria = process.transitions([
    ['start', 'next'],
    ['next', 'stop']
    ]);

    export const start = process('my machine', states, criteria);

    // USAGE:
    start();
    start('next');
    start('stop', { initial: 'conditions' });
    // you can also just provide initial conditions;
    // the first action will still be used as the start action
    start({ initial: 'conditions' });

    Parameters

    • criteria: ProcessTransition[] = []

      The transitions that should be used to determine the initial and follow-up actions to invoke in this process.

    Returns ProcessLogic

    A ProcessLogic instance that process can use to determine how to run the ModelCollection actions it was provided.