import { Effect, Layer, Option } from "effect"; import { Input, type InputFailure, type Item, Output, type OutputFailure, type Processed, Transform, type TransformFailure, } from "./contracts.ts"; export type Session = "input" | "output"; export interface DriverOptions { readonly items?: ReadonlyArray; readonly openInput?: () => Effect.Effect; readonly openOutput?: () => Effect.Effect; readonly next?: (index: number) => Effect.Effect; readonly transform?: (item: Item) => Effect.Effect; readonly write?: ( batch: ReadonlyArray, ) => Effect.Effect; readonly close?: (session: Session) => Effect.Effect; } export const sampleItems = (count: number): ReadonlyArray => Array.from( { length: count }, (_, index) => ({ id: index + 1, value: index + 1 }), ); /** Primitive recording operations; source owns the complete stream pipeline. */ export function makeDriver(options: DriverOptions = {}) { const items = options.items ?? sampleItems(5); const events: string[] = []; const activeSessions = new Set(); const activeTransforms = new Set(); const activeOperations = new Set(); const pulls: number[] = []; const completed: number[] = []; const batches: ReadonlyArray[] = []; let opened = 0; let maximumTransforms = 0; const open = (session: Session, effect: Effect.Effect) => Effect.gen(function* () { events.push(`open:${session}`); yield* effect; activeSessions.add(session); const id = ++opened; events.push(`opened:${session}:${id}`); return id; }); const close = (session: Session) => Effect.suspend(() => { events.push(`close:${session}`); return (options.close?.(session) ?? Effect.void).pipe( Effect.ensuring(Effect.sync(() => { activeSessions.delete(session); events.push(`closed:${session}`); })), ); }); const operation = ( name: string, session: Session, effect: Effect.Effect, ) => Effect.suspend(() => { if (!activeSessions.has(session)) { return Effect.die(`closed ${session} session`); } activeOperations.add(name); events.push(name); return effect.pipe(Effect.ensuring(Effect.sync(() => { activeOperations.delete(name); events.push(`done:${name}`); }))); }); const input = Layer.succeed(Input)({ open: () => Effect.suspend(() => open("input", options.openInput?.() ?? Effect.void)), next: (_session, index) => operation( `pull:${index}`, "input", Effect.gen(function* () { pulls.push(index); yield* options.next?.(index) ?? Effect.void; const item = items[index]; return item === undefined ? Option.none() : Option.some([item, index + 1] as const); }), ), close: () => close("input"), }); const transform = Layer.succeed(Transform)({ apply: (item) => Effect.suspend(() => { activeTransforms.add(item.id); maximumTransforms = Math.max(maximumTransforms, activeTransforms.size); events.push(`transform:${item.id}`); return (options.transform?.(item) ?? Effect.succeed(item.value * 10)) .pipe( Effect.tap(() => Effect.sync(() => { completed.push(item.id); }) ), Effect.ensuring(Effect.sync(() => { activeTransforms.delete(item.id); events.push(`transformed:${item.id}`); })), ); }), }); const output = Layer.succeed(Output)({ open: () => Effect.suspend(() => open("output", options.openOutput?.() ?? Effect.void) ), write: (_session, batch) => operation( `write:${batch.map((item) => item.id).join(",")}`, "output", Effect.suspend(() => (options.write?.(batch) ?? Effect.void).pipe( Effect.andThen(Effect.sync(() => { batches.push([...batch]); })), ) ), ), close: () => close("output"), }); return { layer: Layer.mergeAll(input, transform, output), events, activeSessions, activeTransforms, activeOperations, pulls, completed, batches, get maximumTransforms() { return maximumTransforms; }, get opened() { return opened; }, }; }