1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Effectable from "effect/Effectable";
4
5export type EffectClass<Shape, A, Err = never, Req = never> = Effect.Effect<
6 A,
7 Err,
8 Req
9> & {
10 new (_: never): Shape;
11};
12
13export const effectClass: {
14 <A, Err = never, Req = never>(
15 impl: Effect.Effect<A, Err, Req>,
16 ): EffectClass<A, A, Err, Req>;
17 <Shape>(): <A, Err = never, Req = never>(
18 impl: Effect.Effect<A, Err, Req>,
19 ) => EffectClass<Shape, A, Err, Req>;
20} = ((impl?: any) =>
21 impl === undefined
22 ? (innerImpl: any) => effectClass(innerImpl)
23 : (Object.assign(
24 class {},
25 // Spreading the Effect prototype onto the class (static side) makes the
26 // class itself a real Effect — `Effect.isEffect(X)` is true, so
27 // `Effect.all([X])` / `Effect.forEach` work — and subclasses
28 // (`class Y extends effectClass(impl)`) inherit the protocol statically.
29 Effectable.Prototype({
30 label: "alchemy/EffectClass",
31 evaluate: () => impl,
32 }),
33 ) as unknown as EffectClass<any, any, any, any>)) as any;
34
35export const taggedFunction = <
36 Tag extends Context.ServiceClass<any, any, any>,
37 Fn extends (...args: any[]) => any,
38>(
39 tag: Tag,
40 fn: Fn,
41): Tag & Fn => {
42 // The Proxy below forwards every Effect-protocol key to `tag` (already an
43 // Effect), so `asEffect`/`[Symbol.iterator]`/`pipe` need no explicit
44 // override — only `toString` diverges.
45 const overrides = {
46 toString: () => `${tag.toString()}.${fn.name}`,
47 };
48
49 return new Proxy(fn, {
50 get: (target, prop, receiver) =>
51 Reflect.has(overrides, prop)
52 ? Reflect.get(overrides, prop, receiver)
53 : Reflect.has(target, prop)
54 ? Reflect.get(target, prop, receiver)
55 : Reflect.get(tag as object, prop, tag),
56 has: (target, prop) =>
57 Reflect.has(overrides, prop) ||
58 Reflect.has(target, prop) ||
59 Reflect.has(tag as object, prop),
60 }) as Tag & Fn;
61};
62
63export const isYieldableEffect = (
64 value: unknown,
65): value is Effect.Effect<unknown, unknown, unknown> =>
66 Effect.isEffect(value) &&
67 typeof (value as any as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===
68 "function";
69
70export type YieldableEffectLike<A = unknown, E = unknown, R = unknown> =
71 | Effect.Effect<A, E, R>
72 | {
73 asEffect: () => Effect.Effect<A, E, R>;
74 [Symbol.iterator]: () => Iterator<unknown>;
75 };
76
77export const isEffectClassLike = (
78 value: unknown,
79): value is YieldableEffectLike =>
80 typeof value === "function" &&
81 typeof (value as { asEffect?: unknown }).asEffect === "function";
82
83export const isYieldableEffectLike = (
84 value: unknown,
85): value is YieldableEffectLike =>
86 (isYieldableEffect(value) || isEffectClassLike(value)) &&
87 !("~alchemy/Kind" in value);
88
89export type UnwrapEffect<T> =
90 T extends Effect.Effect<infer A, any, any> ? A : T;
91
92export type ToEffectInterface<T> = {
93 raw: T;
94} & {
95 [K in keyof T]: T[K] extends (...args: any[]) => any
96 ? (...args: Parameters<T[K]>) => Effect.Effect<Awaited<ReturnType<T[K]>>>
97 : T[K];
98};
99
100export const toEffectInterface = <T extends object>(raw: T) =>
101 ({
102 raw,
103 ...Object.fromEntries(
104 Object.entries(raw).map(([key, value]) => [
105 key,
106 typeof value === "function"
107 ? (...args: any[]) => Effect.tryPromise(async () => value(...args))
108 : value,
109 ]),
110 ),
111 }) as ToEffectInterface<T>;