Install
$
pnpm dlx shadcn@latest add https://archyn.net/r/when.jsonCopy the files below into the target paths shown in each tab.
ts@lib/flow/when.ts
import { type Awaitable } from "./core/result";import { type Behavior, type RuntimeContext } from "./core/runtime";import { choose } from "./choose";import { task } from "./task";
/** * Branches between two behaviors using a predicate function. * * `when` is a convenience wrapper around `choose` for predicates that can be expressed as ordinary * synchronous or asynchronous functions. * * @typeParam I - Shared input type. * @typeParam O1 - Yes branch output type. * @typeParam O2 - No branch output type. * @typeParam E1 - Yes branch domain error type. * @typeParam E2 - No branch domain error type. * @typeParam C - Custom context type. * @param predicate - Function that chooses the branch. * @param branches - `yes` and `no` behaviors. * @returns A behavior producing either branch output and either branch domain error. * * @example * ```ts * const route = when((user) => user.role === "admin", { * yes: loadAdminDashboard, * no: loadUserDashboard, * }); * ``` */export function when<I, O1, O2, E1, E2, C extends object>( predicate: (input: I, context: RuntimeContext<C>) => Awaitable<boolean>, branches: { yes: Behavior<I, O1, E1, C>; no: Behavior<I, O2, E2, C>; },): Behavior<I, O1 | O2, E1 | E2, C> { const predicateTask = task<I, boolean, never, C>(async (input, context) => context.ok(await predicate(input, context)), );
return choose(predicateTask, branches.yes, branches.no);}ts@lib/flow/choose.ts
import { type Behavior, type RuntimeContext, resultFromSignal, runBehavior } from "./core/runtime";
/** * Branches between two behaviors using a behavior predicate. * * The predicate runs first. If it succeeds with `true`, `onTrue` runs; otherwise `onFalse` runs. * Predicate, true-branch, and false-branch domain errors are preserved in the return type. * * @typeParam I - Shared input type. * @typeParam O1 - True branch output type. * @typeParam O2 - False branch output type. * @typeParam EP - Predicate domain error type. * @typeParam E1 - True branch domain error type. * @typeParam E2 - False branch domain error type. * @typeParam C - Custom context type. * @param predicate - Behavior that decides which branch to run. * @param onTrue - Behavior run when the predicate succeeds with `true`. * @param onFalse - Behavior run when the predicate succeeds with `false`. * @returns A behavior producing either branch output and every branch domain error. * * @example * ```ts * const route = choose(isAdmin, loadAdminDashboard, loadUserDashboard); * ``` */export function choose<I, O1, O2, EP, E1, E2, C extends object>( predicate: Behavior<I, boolean, EP, C>, onTrue: Behavior<I, O1, E1, C>, onFalse: Behavior<I, O2, E2, C>,): Behavior<I, O1 | O2, EP | E1 | E2, C> { return async (input: I, context: RuntimeContext<C>) => { if (context.signal.aborted) { return resultFromSignal(context.signal); }
const predicateResult = await runBehavior(predicate, input, context);
if (!predicateResult.ok) { return predicateResult; }
if (context.signal.aborted) { return resultFromSignal(context.signal); }
const branch = predicateResult.value ? onTrue : onFalse;
return runBehavior(branch as Behavior<I, O1 | O2, E1 | E2, C>, input, context); };}ts@lib/flow/core/runtime.ts
import { type Awaitable, type Err, type Result, err } from "./result";
/** * Runtime error returned when execution is cancelled through an `AbortSignal`. * * Cancellation is cooperative. Flow helpers check the signal before and after async work and pass * linked signals to child behaviors where appropriate. */export type Cancelled = { kind: "cancelled"; reason?: unknown;};
/** * Runtime error returned when user code throws. * * Thrown exceptions are kept separate from expected domain errors so recovery helpers do not hide * programming or platform failures. */export type Thrown = { kind: "thrown"; error: unknown;};
/** * Runtime-level failure union added around every behavior's domain error type. * * Domain errors describe expected application failures. `RuntimeError` describes cancellation or * unexpected thrown exceptions observed by the Flow runtime. */export type RuntimeError = Cancelled | Thrown;
/** * Execution context passed to every behavior. * * The context always includes an `AbortSignal` and may include application-specific fields. * * @typeParam C - Custom context object type. */export type RuntimeContext<C extends object = {}> = C & { signal: AbortSignal;};
/** * Executable Flow workflow unit. * * A behavior receives typed input, reads runtime context, and resolves to a `Result` containing * either typed output, typed domain error, or a runtime error. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. */export type Behavior<I, O, E = unknown, C extends object = {}> = ( input: I, context: RuntimeContext<C>,) => Promise<Result<O, E | RuntimeError>>;
/** * Extracts the input type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorInput<T> = T extends Behavior<infer I, infer _O, infer _E, infer _C> ? I : never;
/** * Extracts the output type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorOutput<T> = T extends Behavior<infer _I, infer O, infer _E, infer _C> ? O : never;
/** * Extracts the expected domain error type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorError<T> = T extends Behavior<infer _I, infer _O, infer E, infer _C> ? E : never;
/** * Extracts the custom context type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorContext<T> = T extends Behavior<infer _I, infer _O, infer _E, infer C> ? C : never;
/** * Executes a behavior with runtime error protection. * * The runner checks cancellation before and after behavior execution and converts thrown * exceptions into `Thrown` results. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param behavior - Behavior to execute. * @param input - Input passed to the behavior. * @param context - Runtime context containing an `AbortSignal`. * @returns The behavior result, cancellation result, or thrown runtime result. */export async function runBehavior<I, O, E, C extends object>( behavior: Behavior<I, O, E, C>, input: I, context: RuntimeContext<C>,): Promise<Result<O, E | RuntimeError>> { if (context.signal.aborted) { return resultFromSignal(context.signal); }
try { const result = await behavior(input, context);
if (context.signal.aborted) { return resultFromSignal(context.signal); }
return result; } catch (error) { if (context.signal.aborted) { return resultFromSignal(context.signal); }
return thrown(error); }}
/** * Defines a behavior from a callback that returns a `Result`. * * Use `define` for low-level helpers that already operate in the result channel. Most application * code should use `task`, which also accepts plain successful values. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param run - Callback invoked for each behavior execution. * @returns A behavior protected by cancellation and thrown-exception handling. */export function define<I, O, E = unknown, C extends object = {}>( run: (input: I, context: RuntimeContext<C>) => Awaitable<Result<O, E>>,): Behavior<I, O, E, C> { return async (input, context) => { if (context.signal.aborted) { return resultFromSignal(context.signal); }
try { const result = await run(input, context);
if (context.signal.aborted) { return resultFromSignal(context.signal); }
return result; } catch (error) { if (context.signal.aborted) { return cancelled(context.signal.reason); }
return thrown(error); } };}
/** * Runs a behavior with an optional application context. * * If no signal is provided, Flow creates a fresh non-aborted signal. Expected failures remain in * the returned `Result`; thrown exceptions are converted to `Thrown`. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param behavior - Behavior to execute. * @param input - Input passed to the behavior. * @param context - Optional custom context and optional `AbortSignal`. * @returns A result containing behavior output, domain error, or runtime error. * * @example * ```ts * const result = await run(loadUser, "user_1"); * ``` */export async function run<I, O, E, C extends object = {}>( behavior: Behavior<I, O, E, C>, input: I, context: C & { signal?: AbortSignal } = {} as C & { signal?: AbortSignal },): Promise<Result<O, E | RuntimeError>> { const { signal = new AbortController().signal, ...rest } = context;
return runBehavior(behavior, input, { ...(rest as C), signal, });}
/** * Creates a cancellation result. * * @param reason - Optional cancellation reason from an `AbortSignal`. * @returns A failed result containing `Cancelled`. */export function cancelled(reason?: unknown): Err<Cancelled> { if (reason === undefined) { return err({ kind: "cancelled" }); }
return err({ kind: "cancelled", reason });}
/** * Narrows an error value to `Cancelled`. * * @param error - Error value to inspect. * @returns `true` when the value is a cancellation runtime error. */export function isCancelled(error: unknown): error is Cancelled { return ( typeof error === "object" && error !== null && "kind" in error && error.kind === "cancelled" );}
/** * Creates a thrown-exception runtime result. * * @param error - Exception or thrown value. * @returns A failed result containing `Thrown`. */export function thrown(error: unknown): Err<Thrown> { return err({ kind: "thrown", error });}
/** * Narrows an error value to `Thrown`. * * @param error - Error value to inspect. * @returns `true` when the value is a thrown runtime error. */export function isThrown(error: unknown): error is Thrown { return typeof error === "object" && error !== null && "kind" in error && error.kind === "thrown";}
/** * Narrows an error value to a Flow runtime error. * * Recovery helpers use this guard so they only handle expected domain errors. * * @param error - Error value to inspect. * @returns `true` for `Cancelled` or `Thrown`. */export function isRuntimeError(error: unknown): error is RuntimeError { return isCancelled(error) || isThrown(error);}
/** * Converts an aborted signal into a cancellation result. * * @param signal - Aborted signal whose reason should be preserved. * @returns A failed result containing `Cancelled`. */export function resultFromSignal(signal: AbortSignal): Err<Cancelled> { return cancelled(signal.reason);}
/** * Replaces the signal on a runtime context. * * This advanced helper is used by timeout and parallel helpers when they create child abort * controllers. * * @typeParam C - Custom context type. * @param context - Existing runtime context. * @param signal - Replacement signal. * @returns A context with the same custom fields and the replacement signal. */export function contextWithSignal<C extends object>( context: RuntimeContext<C>, signal: AbortSignal,): RuntimeContext<C> { return { ...context, signal };}
/** * Aborts a controller only when it has not already been aborted. * * @param controller - Controller to abort. * @param reason - Optional abort reason. */export function abortController(controller: AbortController, reason?: unknown): void { if (!controller.signal.aborted) { controller.abort(reason); }}
/** * Links a parent signal to a child controller. * * The returned cleanup function removes the listener when the child work has settled. * * @param parent - Parent signal to observe. * @param controller - Child controller to abort when the parent aborts. * @returns Cleanup function that unlinks the abort listener. */export function linkAbortSignal(parent: AbortSignal, controller: AbortController): () => void { if (parent.aborted) { abortController(controller, parent.reason); return () => {}; }
const onAbort = () => abortController(controller, parent.reason); parent.addEventListener("abort", onAbort, { once: true });
return () => parent.removeEventListener("abort", onAbort);}
/** * Creates a child controller linked to a parent signal. * * This advanced helper powers timeout, race, and parallel execution helpers. * * @param parent - Parent signal to link from. * @returns The child controller and an unlink cleanup function. */export function createLinkedController(parent: AbortSignal): { controller: AbortController; unlink: () => void;} { const controller = new AbortController(); const unlink = linkAbortSignal(parent, controller);
return { controller, unlink };}ts@lib/flow/task.ts
import { type Awaitable, type Result, err, isResult, ok } from "./core/result";import { type Behavior, type RuntimeContext, define } from "./core/runtime";
/** * Runtime context provided to a `task` callback. * * The context includes the standard Flow runtime context plus helpers for creating explicit * success and failure results. * * @typeParam C - Custom context object type. */export type TaskContext<C extends object = {}> = RuntimeContext<C> & { ok: typeof ok; succeed: typeof ok; fail: typeof err; err: typeof err;};
/** * Return type accepted from a `task` callback. * * Plain values are treated as success. Branded `Result` values are preserved. * * @typeParam O - Output type. * @typeParam E - Expected domain error type. */export type TaskReturn<O, E> = O | Result<O, E>;
/** * Creates a typed behavior from synchronous or asynchronous application code. * * `task` is the main authoring boundary for Flow programs. Return plain values for success or use * `context.fail`/`context.err` for typed domain failures. Thrown exceptions are captured by the * runtime as `Thrown` errors. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param run - Callback invoked for each behavior execution. * @returns A behavior that wraps plain values in `ok` and preserves explicit results. * * @example * ```ts * const loadUser = task<string, { id: string }, { kind: "missing" }>((id, context) => { * if (!id) return context.fail({ kind: "missing" }); * return { id }; * }); * ``` */export function task<I, O, E = never, C extends object = {}>( run: (input: I, context: TaskContext<C>) => Awaitable<TaskReturn<O, E>>,): Behavior<I, O, E, C> { return define(async (input: I, context: RuntimeContext<C>) => { const result = await run(input, { ...context, ok, succeed: ok, fail: err, err, });
return isResult(result) ? result : ok(result); });}ts@lib/flow/core/result.ts
/** * Represents a value that may be returned directly or through a Promise. * * Flow helpers accept `Awaitable` callbacks so synchronous and asynchronous logic can share the * same behavior authoring APIs. * * @typeParam T - Value produced either immediately or asynchronously. */export type Awaitable<T> = T | Promise<T>;
/** * Successful `Result` variant. * * `Ok` carries the typed success value through Flow pipelines. Use `ok` to create branded values * that can be detected by `isResult`. * * @typeParam T - Success value type. */export type Ok<T> = { ok: true; value: T;};
/** * Failed `Result` variant. * * `Err` carries expected domain errors. Runtime failures such as cancellation and thrown * exceptions are modeled separately by the runtime helpers. * * @typeParam E - Expected error value type. */export type Err<E> = { ok: false; error: E;};
/** * Explicit success-or-failure outcome used throughout Flow. * * Flow behaviors return `Result` values instead of throwing for expected application failures. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. */export type Result<T, E> = Ok<T> | Err<E>;
/** * Structural result shape accepted by `from`. * * Use this when adapting unbranded result-like values into branded Flow results. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. */export type ResultLike<T, E> = Ok<T> | Err<E>;
const resultBrand = Symbol("archyn.result");
/** * Creates a branded successful result. * * Branded results are recognized by `isResult`, which allows `task` callbacks to distinguish * explicit `Result` returns from plain successful values. * * @typeParam T - Success value type. * @param value - Value to carry in the success variant. * @returns A branded `Ok` result. * * @example * ```ts * const result = ok({ id: "user_1" }); * ``` */export function ok<T>(value: T): Ok<T> { return brandResult({ ok: true, value });}
/** * Creates a branded failed result. * * Use `err` for expected domain failures that should stay typed and recoverable by Flow helpers. * * @typeParam E - Expected error value type. * @param error - Error value to carry in the failure variant. * @returns A branded `Err` result. * * @example * ```ts * const result = err({ kind: "not-found" }); * ``` */export function err<E>(error: E): Err<E> { return brandResult({ ok: false, error });}
/** * Converts a structural result-like value into a branded Flow result. * * This is useful when adapting results from another library or from plain object literals. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Structural success or failure object. * @returns A branded Flow `Result`. */export function from<T, E>(result: ResultLike<T, E>): Result<T, E> { return result.ok ? ok(result.value) : err(result.error);}
/** * Narrows a result to the successful variant. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to inspect. * @returns `true` when the result is `Ok`. */export function isOk<T, E>(result: Result<T, E>): result is Ok<T> { return result.ok;}
/** * Narrows a result to the failed variant. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to inspect. * @returns `true` when the result is `Err`. */export function isErr<T, E>(result: Result<T, E>): result is Err<E> { return !result.ok;}
/** * Exhaustively handles both result variants and returns one value. * * `match` keeps success and error handling explicit without manual branching at the call site. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @typeParam R - Handler return type. * @param result - Result to handle. * @param handlers - Callbacks for the success and failure variants. * @returns The value returned by the matching handler. * * @example * ```ts * const label = match(result, { * ok: (user) => user.name, * err: (error) => error.kind, * }); * ``` */export function match<T, E, R>( result: Result<T, E>, handlers: { ok: (value: T) => R; err: (error: E) => R; },): R { return result.ok ? handlers.ok(result.value) : handlers.err(result.error);}
/** * Returns the success value or throws the error value. * * Prefer typed recovery helpers for expected failures. `unwrap` is best reserved for tests, * boundaries that intentionally throw, or places where a failure is impossible by construction. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to unwrap. * @returns The success value. * @throws The `Err.error` value when the result failed. */export function unwrap<T, E>(result: Result<T, E>): T { if (result.ok) { return result.value; }
throw result.error;}
/** * Returns the success value or a fallback. * * Use this when a failure can be safely collapsed into a default value. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to read. * @param fallback - Value returned when the result failed. * @returns The success value or fallback. * * @example * ```ts * const count = unwrapOr(result, 0); * ``` */export function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T { return result.ok ? result.value : fallback;}
/** * Namespace-style collection of core result helpers. * * This object is convenient when passing the result toolkit as a single value. The individual * named functions remain the preferred imports for tree-shakeable application code. */export const Result = { ok, err, from, isOk, isErr, match, unwrap, unwrapOr,};
/** * Detects branded Flow results created by `ok`, `err`, or `from`. * * `task` uses this guard to preserve explicit results while wrapping plain callback returns in * `ok`. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param value - Plain value or potential result. * @returns `true` when the value is a branded Flow `Result`. */export function isResult<T, E>(value: T | Result<T, E>): value is Result<T, E> { return typeof value === "object" && value !== null && resultBrand in value;}
function brandResult<R extends Ok<unknown> | Err<unknown>>(result: R): R { Object.defineProperty(result, resultBrand, { value: true, });
return result;}Update the import paths to match your project setup.
Branch between two Behavior values using a plain predicate function.
Usage
when is a convenience wrapper over choose. It turns a plain predicate callback into a predicate task and then selects the yes or no branch.
Use it when:
- The branch condition is simple and cannot fail with a domain error.
- You want less ceremony than creating a predicate
Behaviormanually. - Both branches should receive the same input and runtime context.
import { task } from "@/lib/flow/task";import { when } from "@/lib/flow/when";
const approve = task((amount: number) => ({ status: "approved" as const, amount }));const review = task((amount: number) => ({ status: "review" as const, amount }));
const routePayment = when((amount: number) => amount < 1000, { yes: approve, no: review });Behavior
when creates an internal predicate task and delegates branch execution to choose.
Branch domain errors pass through. Predicate exceptions become runtime errors through the internal task; cancellation follows the same behavior as choose.
Types
Branch outputs and domain errors are unioned.
Import from @/lib/flow/when.
Composition Notes
Use when for ergonomic synchronous or asynchronous branching.
Do not use when when the predicate needs typed domain errors; use choose with a predicate behavior.
Related APIs
Source
ts@lib/flow/when.ts
import { type Awaitable } from "./core/result";import { type Behavior, type RuntimeContext } from "./core/runtime";import { choose } from "./choose";import { task } from "./task";
/** * Branches between two behaviors using a predicate function. * * `when` is a convenience wrapper around `choose` for predicates that can be expressed as ordinary * synchronous or asynchronous functions. * * @typeParam I - Shared input type. * @typeParam O1 - Yes branch output type. * @typeParam O2 - No branch output type. * @typeParam E1 - Yes branch domain error type. * @typeParam E2 - No branch domain error type. * @typeParam C - Custom context type. * @param predicate - Function that chooses the branch. * @param branches - `yes` and `no` behaviors. * @returns A behavior producing either branch output and either branch domain error. * * @example * ```ts * const route = when((user) => user.role === "admin", { * yes: loadAdminDashboard, * no: loadUserDashboard, * }); * ``` */export function when<I, O1, O2, E1, E2, C extends object>( predicate: (input: I, context: RuntimeContext<C>) => Awaitable<boolean>, branches: { yes: Behavior<I, O1, E1, C>; no: Behavior<I, O2, E2, C>; },): Behavior<I, O1 | O2, E1 | E2, C> { const predicateTask = task<I, boolean, never, C>(async (input, context) => context.ok(await predicate(input, context)), );
return choose(predicateTask, branches.yes, branches.no);}ts@lib/flow/choose.ts
import { type Behavior, type RuntimeContext, resultFromSignal, runBehavior } from "./core/runtime";
/** * Branches between two behaviors using a behavior predicate. * * The predicate runs first. If it succeeds with `true`, `onTrue` runs; otherwise `onFalse` runs. * Predicate, true-branch, and false-branch domain errors are preserved in the return type. * * @typeParam I - Shared input type. * @typeParam O1 - True branch output type. * @typeParam O2 - False branch output type. * @typeParam EP - Predicate domain error type. * @typeParam E1 - True branch domain error type. * @typeParam E2 - False branch domain error type. * @typeParam C - Custom context type. * @param predicate - Behavior that decides which branch to run. * @param onTrue - Behavior run when the predicate succeeds with `true`. * @param onFalse - Behavior run when the predicate succeeds with `false`. * @returns A behavior producing either branch output and every branch domain error. * * @example * ```ts * const route = choose(isAdmin, loadAdminDashboard, loadUserDashboard); * ``` */export function choose<I, O1, O2, EP, E1, E2, C extends object>( predicate: Behavior<I, boolean, EP, C>, onTrue: Behavior<I, O1, E1, C>, onFalse: Behavior<I, O2, E2, C>,): Behavior<I, O1 | O2, EP | E1 | E2, C> { return async (input: I, context: RuntimeContext<C>) => { if (context.signal.aborted) { return resultFromSignal(context.signal); }
const predicateResult = await runBehavior(predicate, input, context);
if (!predicateResult.ok) { return predicateResult; }
if (context.signal.aborted) { return resultFromSignal(context.signal); }
const branch = predicateResult.value ? onTrue : onFalse;
return runBehavior(branch as Behavior<I, O1 | O2, E1 | E2, C>, input, context); };}ts@lib/flow/core/runtime.ts
import { type Awaitable, type Err, type Result, err } from "./result";
/** * Runtime error returned when execution is cancelled through an `AbortSignal`. * * Cancellation is cooperative. Flow helpers check the signal before and after async work and pass * linked signals to child behaviors where appropriate. */export type Cancelled = { kind: "cancelled"; reason?: unknown;};
/** * Runtime error returned when user code throws. * * Thrown exceptions are kept separate from expected domain errors so recovery helpers do not hide * programming or platform failures. */export type Thrown = { kind: "thrown"; error: unknown;};
/** * Runtime-level failure union added around every behavior's domain error type. * * Domain errors describe expected application failures. `RuntimeError` describes cancellation or * unexpected thrown exceptions observed by the Flow runtime. */export type RuntimeError = Cancelled | Thrown;
/** * Execution context passed to every behavior. * * The context always includes an `AbortSignal` and may include application-specific fields. * * @typeParam C - Custom context object type. */export type RuntimeContext<C extends object = {}> = C & { signal: AbortSignal;};
/** * Executable Flow workflow unit. * * A behavior receives typed input, reads runtime context, and resolves to a `Result` containing * either typed output, typed domain error, or a runtime error. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. */export type Behavior<I, O, E = unknown, C extends object = {}> = ( input: I, context: RuntimeContext<C>,) => Promise<Result<O, E | RuntimeError>>;
/** * Extracts the input type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorInput<T> = T extends Behavior<infer I, infer _O, infer _E, infer _C> ? I : never;
/** * Extracts the output type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorOutput<T> = T extends Behavior<infer _I, infer O, infer _E, infer _C> ? O : never;
/** * Extracts the expected domain error type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorError<T> = T extends Behavior<infer _I, infer _O, infer E, infer _C> ? E : never;
/** * Extracts the custom context type from a behavior. * * @typeParam T - Behavior type to inspect. */export type BehaviorContext<T> = T extends Behavior<infer _I, infer _O, infer _E, infer C> ? C : never;
/** * Executes a behavior with runtime error protection. * * The runner checks cancellation before and after behavior execution and converts thrown * exceptions into `Thrown` results. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param behavior - Behavior to execute. * @param input - Input passed to the behavior. * @param context - Runtime context containing an `AbortSignal`. * @returns The behavior result, cancellation result, or thrown runtime result. */export async function runBehavior<I, O, E, C extends object>( behavior: Behavior<I, O, E, C>, input: I, context: RuntimeContext<C>,): Promise<Result<O, E | RuntimeError>> { if (context.signal.aborted) { return resultFromSignal(context.signal); }
try { const result = await behavior(input, context);
if (context.signal.aborted) { return resultFromSignal(context.signal); }
return result; } catch (error) { if (context.signal.aborted) { return resultFromSignal(context.signal); }
return thrown(error); }}
/** * Defines a behavior from a callback that returns a `Result`. * * Use `define` for low-level helpers that already operate in the result channel. Most application * code should use `task`, which also accepts plain successful values. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param run - Callback invoked for each behavior execution. * @returns A behavior protected by cancellation and thrown-exception handling. */export function define<I, O, E = unknown, C extends object = {}>( run: (input: I, context: RuntimeContext<C>) => Awaitable<Result<O, E>>,): Behavior<I, O, E, C> { return async (input, context) => { if (context.signal.aborted) { return resultFromSignal(context.signal); }
try { const result = await run(input, context);
if (context.signal.aborted) { return resultFromSignal(context.signal); }
return result; } catch (error) { if (context.signal.aborted) { return cancelled(context.signal.reason); }
return thrown(error); } };}
/** * Runs a behavior with an optional application context. * * If no signal is provided, Flow creates a fresh non-aborted signal. Expected failures remain in * the returned `Result`; thrown exceptions are converted to `Thrown`. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param behavior - Behavior to execute. * @param input - Input passed to the behavior. * @param context - Optional custom context and optional `AbortSignal`. * @returns A result containing behavior output, domain error, or runtime error. * * @example * ```ts * const result = await run(loadUser, "user_1"); * ``` */export async function run<I, O, E, C extends object = {}>( behavior: Behavior<I, O, E, C>, input: I, context: C & { signal?: AbortSignal } = {} as C & { signal?: AbortSignal },): Promise<Result<O, E | RuntimeError>> { const { signal = new AbortController().signal, ...rest } = context;
return runBehavior(behavior, input, { ...(rest as C), signal, });}
/** * Creates a cancellation result. * * @param reason - Optional cancellation reason from an `AbortSignal`. * @returns A failed result containing `Cancelled`. */export function cancelled(reason?: unknown): Err<Cancelled> { if (reason === undefined) { return err({ kind: "cancelled" }); }
return err({ kind: "cancelled", reason });}
/** * Narrows an error value to `Cancelled`. * * @param error - Error value to inspect. * @returns `true` when the value is a cancellation runtime error. */export function isCancelled(error: unknown): error is Cancelled { return ( typeof error === "object" && error !== null && "kind" in error && error.kind === "cancelled" );}
/** * Creates a thrown-exception runtime result. * * @param error - Exception or thrown value. * @returns A failed result containing `Thrown`. */export function thrown(error: unknown): Err<Thrown> { return err({ kind: "thrown", error });}
/** * Narrows an error value to `Thrown`. * * @param error - Error value to inspect. * @returns `true` when the value is a thrown runtime error. */export function isThrown(error: unknown): error is Thrown { return typeof error === "object" && error !== null && "kind" in error && error.kind === "thrown";}
/** * Narrows an error value to a Flow runtime error. * * Recovery helpers use this guard so they only handle expected domain errors. * * @param error - Error value to inspect. * @returns `true` for `Cancelled` or `Thrown`. */export function isRuntimeError(error: unknown): error is RuntimeError { return isCancelled(error) || isThrown(error);}
/** * Converts an aborted signal into a cancellation result. * * @param signal - Aborted signal whose reason should be preserved. * @returns A failed result containing `Cancelled`. */export function resultFromSignal(signal: AbortSignal): Err<Cancelled> { return cancelled(signal.reason);}
/** * Replaces the signal on a runtime context. * * This advanced helper is used by timeout and parallel helpers when they create child abort * controllers. * * @typeParam C - Custom context type. * @param context - Existing runtime context. * @param signal - Replacement signal. * @returns A context with the same custom fields and the replacement signal. */export function contextWithSignal<C extends object>( context: RuntimeContext<C>, signal: AbortSignal,): RuntimeContext<C> { return { ...context, signal };}
/** * Aborts a controller only when it has not already been aborted. * * @param controller - Controller to abort. * @param reason - Optional abort reason. */export function abortController(controller: AbortController, reason?: unknown): void { if (!controller.signal.aborted) { controller.abort(reason); }}
/** * Links a parent signal to a child controller. * * The returned cleanup function removes the listener when the child work has settled. * * @param parent - Parent signal to observe. * @param controller - Child controller to abort when the parent aborts. * @returns Cleanup function that unlinks the abort listener. */export function linkAbortSignal(parent: AbortSignal, controller: AbortController): () => void { if (parent.aborted) { abortController(controller, parent.reason); return () => {}; }
const onAbort = () => abortController(controller, parent.reason); parent.addEventListener("abort", onAbort, { once: true });
return () => parent.removeEventListener("abort", onAbort);}
/** * Creates a child controller linked to a parent signal. * * This advanced helper powers timeout, race, and parallel execution helpers. * * @param parent - Parent signal to link from. * @returns The child controller and an unlink cleanup function. */export function createLinkedController(parent: AbortSignal): { controller: AbortController; unlink: () => void;} { const controller = new AbortController(); const unlink = linkAbortSignal(parent, controller);
return { controller, unlink };}ts@lib/flow/task.ts
import { type Awaitable, type Result, err, isResult, ok } from "./core/result";import { type Behavior, type RuntimeContext, define } from "./core/runtime";
/** * Runtime context provided to a `task` callback. * * The context includes the standard Flow runtime context plus helpers for creating explicit * success and failure results. * * @typeParam C - Custom context object type. */export type TaskContext<C extends object = {}> = RuntimeContext<C> & { ok: typeof ok; succeed: typeof ok; fail: typeof err; err: typeof err;};
/** * Return type accepted from a `task` callback. * * Plain values are treated as success. Branded `Result` values are preserved. * * @typeParam O - Output type. * @typeParam E - Expected domain error type. */export type TaskReturn<O, E> = O | Result<O, E>;
/** * Creates a typed behavior from synchronous or asynchronous application code. * * `task` is the main authoring boundary for Flow programs. Return plain values for success or use * `context.fail`/`context.err` for typed domain failures. Thrown exceptions are captured by the * runtime as `Thrown` errors. * * @typeParam I - Input type. * @typeParam O - Output type. * @typeParam E - Expected domain error type. * @typeParam C - Custom context type. * @param run - Callback invoked for each behavior execution. * @returns A behavior that wraps plain values in `ok` and preserves explicit results. * * @example * ```ts * const loadUser = task<string, { id: string }, { kind: "missing" }>((id, context) => { * if (!id) return context.fail({ kind: "missing" }); * return { id }; * }); * ``` */export function task<I, O, E = never, C extends object = {}>( run: (input: I, context: TaskContext<C>) => Awaitable<TaskReturn<O, E>>,): Behavior<I, O, E, C> { return define(async (input: I, context: RuntimeContext<C>) => { const result = await run(input, { ...context, ok, succeed: ok, fail: err, err, });
return isResult(result) ? result : ok(result); });}ts@lib/flow/core/result.ts
/** * Represents a value that may be returned directly or through a Promise. * * Flow helpers accept `Awaitable` callbacks so synchronous and asynchronous logic can share the * same behavior authoring APIs. * * @typeParam T - Value produced either immediately or asynchronously. */export type Awaitable<T> = T | Promise<T>;
/** * Successful `Result` variant. * * `Ok` carries the typed success value through Flow pipelines. Use `ok` to create branded values * that can be detected by `isResult`. * * @typeParam T - Success value type. */export type Ok<T> = { ok: true; value: T;};
/** * Failed `Result` variant. * * `Err` carries expected domain errors. Runtime failures such as cancellation and thrown * exceptions are modeled separately by the runtime helpers. * * @typeParam E - Expected error value type. */export type Err<E> = { ok: false; error: E;};
/** * Explicit success-or-failure outcome used throughout Flow. * * Flow behaviors return `Result` values instead of throwing for expected application failures. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. */export type Result<T, E> = Ok<T> | Err<E>;
/** * Structural result shape accepted by `from`. * * Use this when adapting unbranded result-like values into branded Flow results. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. */export type ResultLike<T, E> = Ok<T> | Err<E>;
const resultBrand = Symbol("archyn.result");
/** * Creates a branded successful result. * * Branded results are recognized by `isResult`, which allows `task` callbacks to distinguish * explicit `Result` returns from plain successful values. * * @typeParam T - Success value type. * @param value - Value to carry in the success variant. * @returns A branded `Ok` result. * * @example * ```ts * const result = ok({ id: "user_1" }); * ``` */export function ok<T>(value: T): Ok<T> { return brandResult({ ok: true, value });}
/** * Creates a branded failed result. * * Use `err` for expected domain failures that should stay typed and recoverable by Flow helpers. * * @typeParam E - Expected error value type. * @param error - Error value to carry in the failure variant. * @returns A branded `Err` result. * * @example * ```ts * const result = err({ kind: "not-found" }); * ``` */export function err<E>(error: E): Err<E> { return brandResult({ ok: false, error });}
/** * Converts a structural result-like value into a branded Flow result. * * This is useful when adapting results from another library or from plain object literals. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Structural success or failure object. * @returns A branded Flow `Result`. */export function from<T, E>(result: ResultLike<T, E>): Result<T, E> { return result.ok ? ok(result.value) : err(result.error);}
/** * Narrows a result to the successful variant. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to inspect. * @returns `true` when the result is `Ok`. */export function isOk<T, E>(result: Result<T, E>): result is Ok<T> { return result.ok;}
/** * Narrows a result to the failed variant. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to inspect. * @returns `true` when the result is `Err`. */export function isErr<T, E>(result: Result<T, E>): result is Err<E> { return !result.ok;}
/** * Exhaustively handles both result variants and returns one value. * * `match` keeps success and error handling explicit without manual branching at the call site. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @typeParam R - Handler return type. * @param result - Result to handle. * @param handlers - Callbacks for the success and failure variants. * @returns The value returned by the matching handler. * * @example * ```ts * const label = match(result, { * ok: (user) => user.name, * err: (error) => error.kind, * }); * ``` */export function match<T, E, R>( result: Result<T, E>, handlers: { ok: (value: T) => R; err: (error: E) => R; },): R { return result.ok ? handlers.ok(result.value) : handlers.err(result.error);}
/** * Returns the success value or throws the error value. * * Prefer typed recovery helpers for expected failures. `unwrap` is best reserved for tests, * boundaries that intentionally throw, or places where a failure is impossible by construction. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to unwrap. * @returns The success value. * @throws The `Err.error` value when the result failed. */export function unwrap<T, E>(result: Result<T, E>): T { if (result.ok) { return result.value; }
throw result.error;}
/** * Returns the success value or a fallback. * * Use this when a failure can be safely collapsed into a default value. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param result - Result to read. * @param fallback - Value returned when the result failed. * @returns The success value or fallback. * * @example * ```ts * const count = unwrapOr(result, 0); * ``` */export function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T { return result.ok ? result.value : fallback;}
/** * Namespace-style collection of core result helpers. * * This object is convenient when passing the result toolkit as a single value. The individual * named functions remain the preferred imports for tree-shakeable application code. */export const Result = { ok, err, from, isOk, isErr, match, unwrap, unwrapOr,};
/** * Detects branded Flow results created by `ok`, `err`, or `from`. * * `task` uses this guard to preserve explicit results while wrapping plain callback returns in * `ok`. * * @typeParam T - Success value type. * @typeParam E - Expected error value type. * @param value - Plain value or potential result. * @returns `true` when the value is a branded Flow `Result`. */export function isResult<T, E>(value: T | Result<T, E>): value is Result<T, E> { return typeof value === "object" && value !== null && resultBrand in value;}
function brandResult<R extends Ok<unknown> | Err<unknown>>(result: R): R { Object.defineProperty(result, resultBrand, { value: true, });
return result;}