/** * @module projection-builder * @category Builders * * Fluent builder for composing projection handlers — read-model updaters * that react to events and update external state (databases, caches, etc.). * * Projections differ from slices: they don't contain states, don't dispatch * actions, and are pure side-effect handlers routed to a named stream. */ import type { ZodType } from "zod"; import { type FoldConfig } from "../internal/index.js"; import type { BatchHandler, CacheEntry, Committed, EventRegister, FoldOptions, ReactionResolver, Schema, Schemas, State } from "../types/index.js"; /** * A self-contained projection grouping read-model update handlers. * Projections are composed into an Act orchestrator via `act().withProjection(projection)`. * * @template TEvents - Event schemas handled by this projection */ export type Projection = { readonly _tag: "Projection"; readonly events: EventRegister; readonly target?: string; readonly batchHandler?: BatchHandler; /** * State-fold spec from `.of()`. The builder only records intent — the * orchestrator resolves the REGISTRY-MERGED full state at * `act().build()` and synthesizes the batch handler there, so the * fold always covers every reducer of the state, including partials * merged by slices the projection never saw. * @internal */ readonly fold?: { readonly name: string; readonly flush: (rows: ReadonlyArray>) => Promise; readonly config: FoldConfig; }; }; /** * The `.of()` continuation: state projections flush the cache layer * outward — the rows ARE the streams' {@link CacheEntry} values, one * per dirty stream per flush round. Must be an idempotent upsert keyed * on `stream` (guard with `event_id` for order safety when a rebuild * races a live worker). */ type FoldFlush = { flush: (handler: (rows: ReadonlyArray>) => Promise) => { build: () => Projection; }; }; /** * `.of()` accepts the partials of ONE state (same name, enforced at the * type level via `TName`) purely for typing and event registration — * the fold itself always runs on the registry-merged full state, * resolved at `act().build()`. Passing every partial is required: the * orchestrator validates completeness at build and throws on missing * events. */ type OfSignatures = { (s1: State, options?: FoldOptions): FoldFlush; (s1: State, s2: State, options?: FoldOptions): FoldFlush; (s1: State, s2: State, s3: State, options?: FoldOptions): FoldFlush; (s1: State, s2: State, s3: State, s4: State, options?: FoldOptions): FoldFlush; }; /** Helper: a single-key record mapping an event name to its Zod schema. */ type EventEntry = { [P in TKey]: ZodType; }; /** Infer the handler-result type after registering one event. */ type DoResult = ProjectionBuilder & { to: (resolver: ReactionResolver | string) => ProjectionBuilder; }; /** * Fluent builder interface for composing projections. * * When a static target is provided via `projection("target")`, the builder * exposes a `.batch()` method for registering a batch handler that processes * all events in a single call. * * @template TEvents - Event schemas * @template TTarget - Static target string or undefined */ export type ProjectionBuilder = { /** * Begins defining a projection handler for a specific event. * * Pass a `{ EventName: schema }` record — use shorthand `{ EventName }` * when the variable name matches the event name. The key becomes the * event name, the value the Zod schema. */ on: (entry: EventEntry) => { do: (handler: (event: Committed, stream: string) => Promise) => DoResult; }; /** * Builds and returns the Projection data structure. */ build: () => Projection; /** * The registered event schemas and their reaction maps. */ readonly events: EventRegister; } & (TTarget extends string ? { /** * Registers a batch handler that processes all events in a single call. * * Only available on projections with a static target (`projection("target")`). * The handler receives a discriminated union of all declared events, * enabling bulk DB operations in a single transaction. * * When defined, the batch handler is always called — even for a single event. * Individual `.do()` handlers serve as fallback for projections without `.batch()`. */ batch: (handler: BatchHandler) => { build: () => Projection; }; } & (THasHandlers extends false ? { /** * Declares a state projection: fold every event of the given * state through its own reducers and flush one row per stream — * the queryable list of the aggregates themselves. * * The state is the filter: the projection consumes exactly the * state's event register, so in a multi-state app only that * state's streams are folded — and every event of a folded * stream reaches the reducer. Write amplification tracks the * distinct stream count, not the event count; `app.reset` * rebuilds in O(streams) upserts. * * The fluent chain enforces the shape: `.of()` is only offered * before any `.on()` handler, and narrows to `.flush()` + * `.build()` — a projection either folds a state or declares * handlers, never both. */ of: OfSignatures; } : {}) : {}); /** * Creates a new projection builder with a static target stream. * * All handlers inherit the target resolver automatically. Enables `.batch()` * for bulk event processing in a single transaction. * * @param target - Static target stream for all handlers */ export declare function projection(target: string): ProjectionBuilder; /** * Creates a new projection builder without a default target. * * Use per-handler `.to()` to route events to different streams. */ export declare function projection(target?: undefined): ProjectionBuilder; export {}; //# sourceMappingURL=projection-builder.d.ts.map