/** * `Op` — `Effect.Effect` wrapper with named factories. * * @module */ import type { Scope, Result } from 'effect'; import { Effect, Schedule, Duration } from 'effect'; import { IoError } from '@czap/error'; import type { Millis } from './brands.js'; interface OpShape { readonly _tag: 'Op'; readonly effect: Effect.Effect; run(): Effect.Effect; map(f: (a: A) => B): OpShape; flatMap(f: (a: A) => OpShape): OpShape; } type OpValue> = T extends OpShape ? A : never; type OpError> = T extends OpShape ? E : never; type OpRequirement> = T extends OpShape ? R : never; type OpValues[]> = { [K in keyof T]: OpValue }; type SettledOpValues[]> = { [K in keyof T]: Result.Result, OpError>; }; /** * Wraps an Effect into an Op, providing `.map()` and `.flatMap()` chaining. * * @example * ```ts * const op = Op.make(Effect.succeed(42)); * const doubled = op.map(n => n * 2); * const result = Effect.runSync(doubled.run()); // 84 * ``` */ const _make = (effect: Effect.Effect): OpShape => ({ _tag: 'Op' as const, effect, run: () => effect, map: (fn: (a: A) => B): OpShape => _make(Effect.map(effect, fn)), flatMap: (fn: (a: A) => OpShape): OpShape => _make(Effect.flatMap(effect, (a) => fn(a).effect)), }); /** * Creates an Op from a Promise-returning function, catching errors as `Error`. * * @example * ```ts * const op = Op.fromPromise(() => fetch('/api/data').then(r => r.json())); * const result = await Effect.runPromise(op.run()); * console.log(result); // parsed JSON response * ``` */ const _fromPromise = (f: () => Promise): OpShape => _make( Effect.tryPromise({ try: f, catch: (error) => (error instanceof Error ? error : new Error(String(error))), }), ); /** * Creates an Op that immediately succeeds with the given value. * * @example * ```ts * const op = Op.succeed({ name: 'dark', contrast: 0.9 }); * const result = Effect.runSync(op.run()); // { name: 'dark', contrast: 0.9 } * ``` */ const _succeed = (value: A): OpShape => _make(Effect.succeed(value)); /** * Creates an Op that immediately fails with the given error. * * @example * ```ts * const op = Op.fail(new Error('GPU not available')); * // Effect.runSync(op.run()) would throw * ``` */ const _fail = (error: E): OpShape => _make(Effect.fail(error)); /** * Runs all Ops concurrently and returns their results as a tuple. * Fails if any Op fails. * * @example * ```ts * const a = Op.succeed(10); * const b = Op.succeed('hello'); * const combined = Op.all([a, b] as const); * const [num, str] = Effect.runSync(combined.run()); // [10, 'hello'] * ``` */ // Effect.all's overloads infer tuple results only when the input shape itself is tuple-typed. // `tasks.map(...)` widens to unknown[], so we contain one cast into a typed wrapper and // apply it at the boundary where the tuple→Op product is materialized. const _all = []>( tasks: T, ): OpShape, OpError, OpRequirement> => { const effects = tasks.map((task) => task.effect); const combined = Effect.all(effects, { concurrency: 'unbounded' }) as unknown as Effect.Effect< OpValues, OpError, OpRequirement >; return _make(combined); }; /** * Runs all Ops concurrently and returns a Result for each, never failing. * Each result is either a success or a failure. * * @example * ```ts * const a = Op.succeed(1); * const b = Op.fail(new Error('oops')); * const settled = Op.allSettled([a, b] as const); * const results = Effect.runSync(settled.run()); * // results[0] is Result.success(1), results[1] is Result.failure(Error) * ``` */ // Mirrors _all: one boundary cast from unknown[] back to the tuple-projected product. const _allSettled = []>( tasks: T, ): OpShape, never, OpRequirement> => { const resultEffects = tasks.map((task) => Effect.result(task.effect)); const combined = Effect.all(resultEffects, { concurrency: 'unbounded' }) as unknown as Effect.Effect< SettledOpValues, never, OpRequirement >; return _make(combined); }; /** * Races multiple Ops concurrently, returning the first to complete. * Fails with an error if the array is empty. * * @example * ```ts * const fast = Op.succeed('fast'); * const slow = Op.fromPromise(() => new Promise(r => setTimeout(() => r('slow'), 100))); * const winner = Op.race([fast, slow]); * const result = Effect.runSync(winner.run()); // 'fast' * ``` */ const _race = (tasks: ReadonlyArray>): OpShape => { if (tasks.length === 0) { return _fail(new Error('Op.race: empty array')); } if (tasks.length === 1) { return tasks[0]!; } const effects = tasks.map((task) => task.effect); const raced = effects.reduce((acc, effect) => Effect.race(acc, effect)); return _make(raced); }; /** * Retries a failing Op with exponential backoff. * * @example * ```ts * const flaky = Op.fromPromise(() => fetch('/unstable-api').then(r => r.json())); * const resilient = Op.retry(flaky, { times: 3, delay: Millis(200), factor: 2 }); * const result = await Effect.runPromise(resilient.run()); * ``` */ const _retry = ( task: OpShape, options: { times: number; delay?: Millis; factor?: number }, ): OpShape => { const delay = options.delay ?? 100; const factor = options.factor ?? 2; const schedule = Schedule.exponential(Duration.millis(delay), factor).pipe( Schedule.both(Schedule.recurs(options.times)), ); return _make(Effect.retry(task.effect, schedule)); }; /** * Wraps an Op with a timeout, failing with an Error if it exceeds the given duration. * * @example * ```ts * const slow = Op.fromPromise(() => new Promise(r => setTimeout(() => r('done'), 5000))); * const bounded = Op.timeout(slow, Millis(1000)); * // Will fail with Error('Op timed out after 1000ms') if not resolved in time * ``` */ const _timeout = (task: OpShape, ms: Millis): OpShape => _make( Effect.timeout(task.effect, Duration.millis(ms)).pipe( Effect.catchTag('TimeoutError', () => Effect.fail(IoError('op.timeout', `Op timed out after ${ms}ms`))), ), ); /** * Op -- Effect.Effect wrapper providing named factories and combinators * for async operations with retry, timeout, race, and parallel execution. * * @example * ```ts * const op = Op.succeed(42).map(n => n * 2); * const result = Effect.runSync(op.run()); // 84 * * const tasks = Op.all([Op.succeed(1), Op.succeed(2)] as const); * const [a, b] = Effect.runSync(tasks.run()); // [1, 2] * ``` */ export const Op = { make: _make, fromPromise: _fromPromise, succeed: _succeed, fail: _fail, all: _all, allSettled: _allSettled, race: _race, retry: _retry, timeout: _timeout, }; export declare namespace Op { /** Structural shape of an {@link Op}: a thin alias over `Effect.Effect` produced by the `Op.*` factories. */ export type Shape = OpShape; }