| 1 | /** |
| 2 | * Low-level helpers for making custom values behave like Effects. The module |
| 3 | * exposes a prototype builder, an abstract base class, and a mixin that let |
| 4 | * domain-specific values, such as service keys or configuration descriptions, |
| 5 | * be evaluated by Effect and yielded inside `Effect.gen`. |
| 6 | * |
| 7 | * @since 4.0.0 |
| 8 | */ |
| 9 | import type * as Effect from "./Effect.ts" |
| 10 | import type * as Fiber from "./Fiber.ts" |
| 11 | import { evaluate, makePrimitiveProto } from "./internal/core.ts" |
| 12 | |
| 13 | /** |
| 14 | * Create a low-level `Effect` prototype. |
| 15 | * |
| 16 | * **When to use** |
| 17 | * |
| 18 | * Use when you need to create a custom Effect-like value without extending a |
| 19 | * class, by providing a label and an evaluate function that receives the |
| 20 | * current fiber. |
| 21 | * |
| 22 | * **Details** |
| 23 | * |
| 24 | * When the effect is evaluated, it calls `evaluate` with the current fiber. |
| 25 | * |
| 26 | * @see {@link Class} for a class-based approach to defining custom Effect values |
| 27 | * @see {@link Mixin} for wrapping an existing class constructor |
| 28 | * |
| 29 | * @category prototypes |
| 30 | * @since 4.0.0 |
| 31 | */ |
| 32 | export const Prototype = <A extends Effect.Effect<any, any, any>>(options: { |
| 33 | readonly label: string |
| 34 | readonly evaluate: ( |
| 35 | this: A, |
| 36 | fiber: Fiber.Fiber<any, any> |
| 37 | ) => Effect.Effect<Effect.Success<A>, Effect.Error<A>, Effect.Services<A>> |
| 38 | }): Effect.Effect<Effect.Success<A>, Effect.Error<A>, Effect.Services<A>> => |
| 39 | makePrimitiveProto({ |
| 40 | op: options.label, |
| 41 | [evaluate]: options.evaluate |
| 42 | }) as any |
| 43 | |
| 44 | const proto = Prototype<Class<any, any, any>>({ |
| 45 | label: "Effectable", |
| 46 | evaluate(_) { |
| 47 | return this.asEffect() |
| 48 | } |
| 49 | }) |
| 50 | |
| 51 | const Base: new<A, E, R>() => Effect.Effect<A, E, R> = (() => { |
| 52 | const Base = function() {} |
| 53 | Base.prototype = proto |
| 54 | return Base as any |
| 55 | })() |
| 56 | |
| 57 | /** |
| 58 | * Provides an abstract class that can be extended to create an `Effect`. |
| 59 | * |
| 60 | * **When to use** |
| 61 | * |
| 62 | * Use as an abstract base class to define custom classes whose instances behave |
| 63 | * as `Effect` values. |
| 64 | * |
| 65 | * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class |
| 66 | * @see {@link Mixin} for wrapping an existing class constructor |
| 67 | * @category constructors |
| 68 | * @since 2.0.0 |
| 69 | */ |
| 70 | export abstract class Class<A, E = never, R = never> extends Base<A, E, R> { |
| 71 | abstract asEffect(): Effect.Effect<A, E, R> |
| 72 | } |
| 73 | |
| 74 | type AsEffectReturn<Self> = Self extends { |
| 75 | asEffect(): infer A extends Effect.Effect<any, any, any> |
| 76 | } ? A |
| 77 | : never |
| 78 | |
| 79 | declare abstract class MixinBase extends Class<any, any, any> { |
| 80 | constructor(...args: ReadonlyArray<any>) |
| 81 | override readonly [Effect.TypeId]: AsEffectReturn<this>[Effect.TypeId] |
| 82 | override [Symbol.iterator](): Effect.EffectIterator<AsEffectReturn<this>> |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Returns a subclass of the provided class that inserts the Effect prototype |
| 87 | * into the inheritance chain. |
| 88 | * |
| 89 | * **When to use** |
| 90 | * |
| 91 | * Use to make instances of an existing class behave as `Effect` values without |
| 92 | * extending {@link Class} or modifying the original prototype. |
| 93 | * |
| 94 | * **Details** |
| 95 | * |
| 96 | * Pass the class to wrap, then implement `asEffect` on the final class. The |
| 97 | * returned class is abstract, and the success, error, and service types are |
| 98 | * inferred from the concrete `asEffect` return type. Concrete and abstract base |
| 99 | * classes are supported. Constructor parameters and instance members are |
| 100 | * preserved, except that Effect's prototype members shadow base prototype |
| 101 | * members with the same name: `pipe`, `toString`, `toJSON`, `[Symbol.iterator]`, |
| 102 | * and `[Symbol.for("nodejs.util.inspect.custom")]`. |
| 103 | * |
| 104 | * **Example** (Evaluating a mixed-in class) |
| 105 | * |
| 106 | * ```ts import.meta.vitest |
| 107 | * import { Effect, Effectable } from "effect" |
| 108 | * |
| 109 | * class Box { |
| 110 | * constructor(readonly value: number) {} |
| 111 | * } |
| 112 | * |
| 113 | * class EffectBox extends Effectable.Mixin(Box) { |
| 114 | * asEffect() { |
| 115 | * return Effect.succeed(this.value) |
| 116 | * } |
| 117 | * } |
| 118 | * |
| 119 | * const box = new EffectBox(2) |
| 120 | * Effect.isEffect(box) // => true |
| 121 | * await Effect.runPromise(box) // => 2 |
| 122 | * ``` |
| 123 | * |
| 124 | * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class |
| 125 | * @see {@link Class} for a base constructor to extend |
| 126 | * @category constructors |
| 127 | * @since 4.0.0 |
| 128 | */ |
| 129 | export const Mixin = <TBase extends abstract new(...args: ReadonlyArray<any>) => object>( |
| 130 | klass: TBase |
| 131 | ): TBase & typeof MixinBase => { |
| 132 | abstract class Mixed extends klass { |
| 133 | abstract asEffect(): Effect.Effect<any, any, any> |
| 134 | } |
| 135 | Object.defineProperties(Mixed.prototype, Object.getOwnPropertyDescriptors(proto)) |
| 136 | return Mixed as TBase & typeof MixinBase |
| 137 | } |