hyperproc
v0.1.0
Published
Chainable, async pipeline over an explicit state object
Maintainers
Readme
HyperProc
Chainable, async pipeline over an explicit state object.
- Linear ops:
applyTo,transform,augment,noop/log,chain - Per-instance
envinjected into every op - Clear error semantics via
HyperProcError - Subclassable policy (e.g., bubbling vs. swallowing errors)
Default error policy:
onErrorlogs and returns the last good state (swallow). Override or subclass to bubble.
Table of Contents
Why
You often need a precise, deterministic pipeline over a single state object, with explicit control of mutation vs. replacement and predictable error behavior. HyperProc gives you that with a minimal surface.
Quick Start
import HyperProc from "hyperproc"; // adjust name/path if different
const hp = HyperProc.init({ inc: n => n + 1 });
const out = await hp
.augment("a", async () => 1) // state.a = 1
.transform("a", async (v) => v + 1) // state.a = 2
.applyTo(async (s, env) => ({ ...s, b: env.inc(1) })) // replace state (must be a plain object)
.log(s => `a=${s.a}, b=${s.b}`)
.run({}); // => { a: 2, b: 2 }Semantics
State contract: state is a plain object (prototype is
Object.prototypeornull). Replacement steps (applyTo,chain) must also return a plain object.Mutation model:
transform(id, f)updates an existing key (throws if missing).augment(id, f)creates a new key (throws if it already exists).applyTo(f)may replace the state; result is validated.
Env: per-instance object passed into every op.
Chaining:
chain(child)runs the child pipeline using the child’senvandonError.Errors: any thrown error inside ops is normalized to
HyperProcErrorbefore youronErrorruns.
API
Types: STATE := PlainObject, ENV := Object, ID := String, VALUE := any.
T* means T | Promise<T>.
Constructor & Statics
new HyperProc :: ENV -> this
HyperProc.init :: ENV -> this // subclass-friendly factory
HyperProc.isState :: * -> BOOL // plain-object check
APPLY_TO, TRANSFORM, AUGMENT, CHAIN, NOOP :: SymbolInstance methods
applyTo :: (STATE, ENV) -> STATE* -> this
transform :: ID -> (VALUE, STATE, ENV) -> VALUE* -> this
augment :: ID -> (STATE, ENV) -> VALUE* -> this
noop :: (STATE, ENV) -> any* -> this
log :: (STATE, ENV) -> STRING* | STRING -> this
onError :: (HyperProcError, STATE, ENV) -> (STATE | undefined)* -> this
chain :: HyperProc -> this
run :: STATE -> Promise<STATE>Operational notes
applyTovalidates its return; throws if not a plain object.transformrequires the key to exist; throws if missing.augmentrequires the key to not exist; throws if present.chainuses the child’senvandonError; the parent validates the child’s returned state (must be plain object).runthrows if the input is not a plain object.onErrorcontract:- return
STATE→ recover with that state (must be plain object). - return
undefined→ keep the last good state (swallow). - throw → bubble to caller (or to parent pipeline).
- return
Errors (HyperProcError)
Normalized error passed to your onError and used for bubbling.
Core fields:
name: "HyperProcError"message: stringop: Symbol | undefined(serialized to a string; absent appears as"UNDEFINED"intoJSON())id: string | undefinedcause?: Error(nativeError.causeused; stack chain preserved)statemay be attached as non-enumerable (not emitted viatoJSON())
Example handling:
try {
await HyperProc.init()
.applyTo(() => { throw new TypeError("bad"); })
.run({});
} catch (e) {
if (e.name === "HyperProcError") {
console.error(e.message, String(e.op), e.id, e.cause?.name);
}
}Examples
Error recovery (override default swallow)
const out = await HyperProc.init()
.augment("n", () => 5)
.transform("n", (v) => { if (v > 3) throw new Error("too big"); return v; })
.onError((err, s) => ({ ...s, n: 3, recovered: true }))
.run({});
// => { n: 3, recovered: true }Bubbling child via subclass
import HyperProc from "hyperproc";
export class HyperSubProc extends HyperProc {
constructor(env = {}) { super(env); this._onError = (err) => { throw err; }; }
}
const child = HyperSubProc.init()
.augment("x", () => 1)
.applyTo(() => { throw new Error("boom"); }); // bubbles
const parent = HyperProc.init()
.chain(child)
.onError((err, s) => ({ ...s, parentRecovered: true }));
const res = await parent.run({});
// => { x: 1, parentRecovered: true }ETL-style
const etl = HyperProc.init({ parse: JSON.parse })
.applyTo((s) => ({ raw: '{"a":1,"b":2}', ...s }))
.augment("doc", (s, env) => env.parse(s.raw))
.augment("sum", (s) => s.doc.a + s.doc.b);
await etl.run({}); // => { raw:'...', doc:{a:1,b:2}, sum:3 }Notes
- Reusing the same instance accumulates ops; treat an instance as a small program.
- In a chain, the parent continues after the child completes; subsequent parent ops see the child’s mutations/replacement.
- To assert on thrown errors in tests, use a bubbling policy (
.onError(e => { throw e; })or a subclass likeHyperSubProc). - Docs were created by API. Because that's the world we live in now...
TODO
- pause :: NUMBER, (state, env) -> PROMISE(STRING)|STRING|VOID -> this
- startTimer :: (state, env) -> PROMISE(STRING)-> this
- stopTimer :: (timeElapsed, state, env) -> PROMISE(STRING) -> this
- branch :: (state, env) -> PROMISE(BOOL), {TRUE:HYPERPROC|VOID, FALSE:HYPERPROCVOID} -> this
- batch :: [HyperProc], (([STATE], STATE, ENV) -> STATE*) ?, { clone?: 'structured'|'shallow'|(STATE->STATE), settle?: 'fail-fast'|'all' } ? -> this
License
MIT
