Core concepts
Context
Section titled “Context”A pipeline works on one plain object, the context. Each step reads the current context and returns a new one with its own results added. Declare everything the steps will write as optional fields, with no default value:
type Ctx = { input: string; // given by the caller parsed?: number; // written by the parse step doubled?: number; // written by the double step};The pipeline freezes a shallow copy of the context it starts with (your object is not frozen) and of every context a step returns. So a step cannot change the context it receives: it spreads it into a new object ({ ...ctx, parsed }).
Use plain objects, not class instances: the copy keeps the object’s own fields, not its prototype, so methods and getters are lost.
A Step<E, Ctx> is a function that takes the current context and returns an Either<E, Ctx>, sync or async:
import { left, right } from "@ngwes/mini-fp";import type { Step } from "@ngwes/groucho-fp";
type Ctx = { input: string; parsed?: number; doubled?: number };
const parse: Step<string, Ctx> = (ctx) => { const parsed = Number.parseInt(ctx.input, 10); return Number.isNaN(parsed) ? left(`"${ctx.input}" is not a number`) : right({ ...ctx, parsed });};
const double: Step<string, Ctx> = async (ctx) => right({ ...ctx, doubled: (ctx.parsed ?? 0) * 2 });- Return
right(newCtx)to continue with the next step. - Return
left(error)for a failure you expect. TheonLefthook decides what happens next. - Throwing, or returning a promise that rejects, is an exception. The
onExceptionhook decides what happens next.
A step always returns a context, even when it adds nothing (right(ctx)). mini-fp 2 does not allow an empty Either: right(undefined) and left(undefined) throw.
StepResult
Section titled “StepResult”StepResult<E, Ctx> is what a step, or a hook, may return: an Either<E, Ctx>, or anything you can await to get one. That covers a plain Either, a Promise<Either> (an async function) and a mini-fp EitherAsync:
import { EitherAsync, Option, right } from "@ngwes/mini-fp";import type { Step } from "@ngwes/groucho-fp";
type User = { id: string; name: string };type Ctx = { userId: string; user?: User; greeting?: string };type Err = { tag: "user-not-found"; userId: string };
declare const db: { findUser(id: string): Promise<User | undefined> };
// returns an Eitherconst greet: Step<Err, Ctx> = (ctx) => right({ ...ctx, greeting: `Hello, ${ctx.user?.name}` });
// returns an EitherAsync: no async/await neededconst fetchUser: Step<Err, Ctx> = (ctx) => Option.fromAsync(db.findUser(ctx.userId)) .toEither<Err>({ tag: "user-not-found", userId: ctx.userId }) .map((user) => ({ ...ctx, user }));
// returns an EitherAsync built directlyconst noop: Step<Err, Ctx> = (ctx) => EitherAsync.right(ctx);