Skip to content

Core concepts

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. The onLeft hook decides what happens next.
  • Throwing, or returning a promise that rejects, is an exception. The onException hook 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<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 Either
const greet: Step<Err, Ctx> = (ctx) => right({ ...ctx, greeting: `Hello, ${ctx.user?.name}` });
// returns an EitherAsync: no async/await needed
const 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 directly
const noop: Step<Err, Ctx> = (ctx) => EitherAsync.right(ctx);