1/** @effect-diagnostics anyUnknownInErrorContext:off */
2import * as Effect from "effect/Effect";
3
4type Op =
5 | { kind: "get"; prop: PropertyKey }
6 | { kind: "call"; args: unknown[] };
7
8interface ChainState {
9 readonly cached: Effect.Effect<unknown, any, any>;
10 readonly ops: ReadonlyArray<Op>;
11}
12
13/**
14 * Every proxy produced by {@link chain} is registered here so that a chain
15 * passed as an *argument* to another chain over the same `cached` effect
16 * (e.g. `` sql`... ${sql.insert(row)}` `` — the nested `sql.insert(row)` is
17 * itself a proxy) can be recognized and replayed against the same resolved
18 * root instead of being handed to the real client as an opaque proxy.
19 */
20const chainStates = new WeakMap<object, ChainState>();
21
22/**
23 * Replay an op chain against a real value. `get` reads a property,
24 * `call` invokes — bound to the previous receiver so `this` resolves
25 * to the object the method was read from (drizzle's `select()` etc.
26 * read `this._.session`, so dropping `this` would throw).
27 */
28const replay = (
29 root: unknown,
30 ops: ReadonlyArray<Op>,
31 cached: Effect.Effect<unknown, any, any>,
32): unknown => {
33 let cur: any = root;
34 let receiver: any = root;
35 for (const op of ops) {
36 if (op.kind === "get") {
37 receiver = cur;
38 cur = cur[op.prop];
39 } else {
40 cur = cur.apply(receiver, replayArgs(root, op.args, cached));
41 receiver = cur;
42 }
43 }
44 return cur;
45};
46
47/**
48 * Resolve nested chain proxies inside a call's arguments. Only proxies over
49 * the *same* `cached` effect can be replayed synchronously (the root is
50 * already resolved); proxies over a different effect pass through untouched.
51 * Returns the original array when nothing needed resolving, so identity-
52 * sensitive values (e.g. a `TemplateStringsArray` with its `raw` property)
53 * are preserved.
54 */
55const replayArgs = (
56 root: unknown,
57 args: ReadonlyArray<unknown>,
58 cached: Effect.Effect<unknown, any, any>,
59): ReadonlyArray<unknown> => {
60 let out: unknown[] | undefined;
61 for (let i = 0; i < args.length; i++) {
62 const resolved = replayArg(root, args[i], cached);
63 if (resolved !== args[i]) {
64 out ??= [...args];
65 out[i] = resolved;
66 }
67 }
68 return out ?? args;
69};
70
71const replayArg = (
72 root: unknown,
73 arg: unknown,
74 cached: Effect.Effect<unknown, any, any>,
75): unknown => {
76 if (arg === null || (typeof arg !== "object" && typeof arg !== "function")) {
77 return arg;
78 }
79 const state = chainStates.get(arg);
80 if (state?.cached === cached) {
81 return replay(root, state.ops, cached);
82 }
83 // Fragments may arrive wrapped in arrays (e.g. `sql.and([a, b])`).
84 if (Array.isArray(arg)) {
85 return replayArgs(root, arg, cached);
86 }
87 return arg;
88};
89
90/**
91 * Wrap a cached `Effect<T>` in a chainable Proxy so callers can use the
92 * returned value as if it were `T` itself — every property read and call
93 * records a step, and the chain is replayed against the resolved value
94 * when it's finally yielded as an Effect.
95 *
96 * Compare:
97 *
98 * ```typescript
99 * // Without proxyChain — caller has to yield the cached Effect first:
100 * const conn = yield* makeConnection(); // Effect<Db>
101 * fetch: Effect.gen(function* () {
102 * const db = yield* conn;
103 * const rows = yield* db.select().from(users);
104 * });
105 *
106 * // With proxyChain — caller treats the return as the value directly:
107 * const db = proxyChain(yield* Effect.cached(makeDb)); // T
108 * fetch: Effect.gen(function* () {
109 * const rows = yield* db.select().from(users);
110 * });
111 * ```
112 *
113 * The chain ends when the proxy is yielded as an Effect — the resolved
114 * value at that point must be a `Yieldable` (an Effect, drizzle query
115 * builder, etc). Anything before that is recorded as ops.
116 *
117 * A chain passed as an *argument* to another chain over the same effect —
118 * e.g. `` sql`INSERT INTO users ${sql.insert(row)}` ``, where `sql.insert(row)`
119 * is itself a deferred proxy — is replayed against the same resolved root
120 * before the outer call runs, so synchronous fragment helpers compose.
121 */
122export const proxyChain = <T>(cached: Effect.Effect<T, any, any>): T =>
123 chain(cached) as T;
124
125/**
126 * Property reads the Effect runtime uses as *brand probes* — `symbol` keys
127 * and `~effect/...` type-id strings (`~effect/Exit`, `~effect/Effect`, …).
128 * The fiber's generator runner distinguishes a yielded Effect from an Exit
129 * with `value[ExitTypeId] !== undefined` (effect ≥ 4.0.0-rc.113; earlier
130 * releases used `in`, which the `has` trap already answers). Recording such
131 * a read as a chain step would hand back a truthy proxy, so the runner
132 * would treat every chain as an already-settled Exit and resume the
133 * generator with `proxy.value` — another proxy — instead of running it.
134 * Brand probes therefore answer `undefined` when the underlying effect
135 * lacks the key; the `has` trap keeps `in`-based probes correct too.
136 */
137const isBrandProbe = (prop: PropertyKey): boolean =>
138 typeof prop === "symbol" ||
139 (typeof prop === "string" && prop.startsWith("~effect/"));
140
141const chain = (
142 cached: Effect.Effect<unknown, any, any>,
143 ops: ReadonlyArray<Op> = [],
144): unknown => {
145 const effect = Effect.flatMap(
146 cached,
147 (root) =>
148 replay(root, ops, cached) as Effect.Effect<unknown, unknown, unknown>,
149 );
150 const proxy = new Proxy(function () {}, {
151 get(_, prop) {
152 if (Reflect.has(effect, prop)) {
153 return Reflect.get(effect, prop);
154 }
155 if (isBrandProbe(prop)) {
156 return undefined;
157 }
158 return chain(cached, [...ops, { kind: "get", prop }]);
159 },
160 has(target, prop) {
161 return Reflect.has(target, prop) || Reflect.has(effect, prop);
162 },
163 apply(_, __, args) {
164 return chain(cached, [...ops, { kind: "call", args }]);
165 },
166 });
167 chainStates.set(proxy, { cached, ops });
168 return proxy;
169};