| 1 | /** @effect-diagnostics anyUnknownInErrorContext:off */ |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | |
| 4 | type Op = |
| 5 | | { kind: "get"; prop: PropertyKey } |
| 6 | | { kind: "call"; args: unknown[] }; |
| 7 | |
| 8 | interface 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 | */ |
| 20 | const 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 | */ |
| 28 | const 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 | */ |
| 55 | const 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 | |
| 71 | const 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 | */ |
| 122 | export 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 | */ |
| 137 | const isBrandProbe = (prop: PropertyKey): boolean => |
| 138 | typeof prop === "symbol" || |
| 139 | (typeof prop === "string" && prop.startsWith("~effect/")); |
| 140 | |
| 141 | const 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 | }; |