UNPKG

hyperproc

Version:

Chainable, async pipeline over an explicit state object

183 lines (167 loc) 6.95 kB
// Dependencies: import HyperProcError from "./HyperProcError/index.js"; // Implements a chainable process for managing explicit state with export default class HyperProc { /* Static Fields */ static APPLY_TO = Symbol("APPLY_TO"); // Signifies an operation applied to state static TRANSFORM = Symbol("TRANSFORM"); // Signifies an operation for writing a new value to an existing property of state static AUGMENT = Symbol("AUGMENT"); // Signifies an operation that adds a new property to state static CHAIN = Symbol("CHAIN"); // Signifies an operation that calls another "hyperProc" instance static NOOP = Symbol("NOOP"); // Signifies an operation that does not change state // CONSTRUCTOR :: OBJECT -> this constructor(env = {}) { this._env = env; // Additional functions, classes, and constants we can apply to functions to change with state this._ops = [] ; // What to apply to state this._onError = (err, state, env) => { // Default error handling function console.error(err) return state; } } /** * * Instance Methods * */ // :: (value, state, env) -> PROMISE(STATE) -> this // Applies function to state: applyTo(fn) { const op = HyperProc.APPLY_TO; this._ops.push({fn, op}); return this; } // :: STRING, (value, state, env) -> PROMISE(VALUE) -> this // Applies function to a specific property of state transform(id, fn) { const op = HyperProc.TRANSFORM; this._ops.push({id, fn, op}) return this; } // :: STRING, (state, env) -> PROMISE(VALUE) -> this // Writes result of function to new property of state: // NOTE: This intent here is to add a new property to state augment(id, fn) { const op = HyperProc.AUGMENT; this._ops.push({id, fn, op}) return this; } // :: (state, env) -> PROMISE(STATE) // Runs function but returns state unchanged: noop(fn) { const op = HyperProc.NOOP; this._ops.push({fn, op}) return this; } // :: (state, env -> PROMISE(STRING)) -> this // :: STRING -> this // Helper method for writing messages to console: log(arg) { const op = HyperProc.NOOP; const fn = typeof(arg) === "function" ? async (state, env) => console.log(await arg(state,env)) : () => console.log(arg) this._ops.push({fn, op}) return this; } // :: (err, state, env) -> PROMISE(state) // Sets "on error" function to handle errors thrown when "run" is called: onError(fn) { this._onError = fn; return this; } // :: hyperProc -> this // Combines "this" hyperProc instance with another hyperproc instance: chain(hyperProc) { if (!(hyperProc instanceof HyperProc)) { throw new TypeError("Can only chain to an instance of HyperProc"); } const op = HyperProc.CHAIN; const fn = hyperProc; this._ops.push({fn, op}); return this; } // :: OBJECT -> PROMISE(OBJECT) // Applies stored operations to given state: // WARNING: Any operation applied to state will mutate the given OBJECT: async run(state) { // Ensure state is something hyperProc can operate on: if (!this.constructor.isState(state)) { throw new TypeError("HyperProc.run expects an OBJECT as state"); } // For passing info to errors: let spec = {}; // For checking state before assigning it: let nextState; try { for (const {id, fn, op} of this._ops) { spec = {id, op}; switch(op) { case HyperProc.APPLY_TO: nextState = await fn(state, this._env); if (!this.constructor.isState(nextState)) { throw new TypeError("HyperProc.applyTo must return an OBJECT as state"); } state = nextState; break; case HyperProc.TRANSFORM: if (!Object.prototype.hasOwnProperty.call(state, id)) { throw new HyperProcError(`HyperProc.transform missing property "${id}"`, { id, op }); } state[id] = await fn(state[id], state, this._env); break; case HyperProc.AUGMENT: if (Object.prototype.hasOwnProperty.call(state, id)) { throw new HyperProcError(`HyperProc.augment property "${id}" already exists`, { id, op }); } state[id] = await fn(state, this._env); break; case HyperProc.CHAIN: nextState = await fn.run(state); if (!this.constructor.isState(nextState)) { throw new TypeError("HyperProc.chain must return an OBJECT as state"); } state = nextState; break; case HyperProc.NOOP: await fn(state, this._env); break; default: throw new HyperProcError(`Unknown Operation: ${String(op)}`, {state, id, op}) } } } catch (err) { // Normalize error: const hyperProcError = err instanceof HyperProcError ? err : HyperProcError.from(err, {state, ...spec}); // When running onError, we need to catch any possible errors that may "bubble up" from other ops: try { const result = await this._onError(hyperProcError, state, this._env); state = result === undefined ? state : result if (!this.constructor.isState(state)) { throw new TypeError("HyperProc.onError must return UNDEFINED or an OBJECT as state"); } } catch (rethrow) { throw rethrow; } } return state; } /** * * Static Methods * */ // Static Factory Method :: OBJECT -> hyperProc static init(env) { return new this(env); } // :: * -> BOOL // Return BOOL for if value can be used as "state" for hyperProc to operate on: static isState(value) { if (value !== null && typeof value === "object" && !Array.isArray(value)) { const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; // plain object only } return false; } }