1import { proxyChain } from "@/Util/proxy-chain.ts";
2import { describe, expect, it } from "alchemy-test";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5
6const TIMEOUT = 5_000;
7
8/**
9 * A fake drizzle-like db. `select()` and `echo()` are real methods that
10 * read `this` — so replaying them with the wrong receiver would throw,
11 * which is how we assert `this`-binding is preserved through the proxy.
12 */
13const makeDb = () => ({
14 greeting: "hi",
15 echo(n: number) {
16 return Effect.succeed(`${this.greeting}:${n}`);
17 },
18 select() {
19 const greeting = this.greeting;
20 return {
21 from: (table: string) => Effect.succeed([`${greeting}/${table}`]),
22 };
23 },
24});
25
26type Db = ReturnType<typeof makeDb>;
27
28describe("proxyChain", { tags: ["unit", "local"], timeout: 5000 }, () => {
29 it.effect("replays a property-read + call chain when yielded", () =>
30 Effect.gen(function* () {
31 const db = proxyChain<Db>(Effect.succeed(makeDb()));
32 const rows = yield* db.select().from("users");
33 expect(rows).toEqual(["hi/users"]);
34 }),
35 );
36
37 it.effect("binds `this` to the receiver for method calls", () =>
38 Effect.gen(function* () {
39 const db = proxyChain<Db>(Effect.succeed(makeDb()));
40 // `echo` reads `this.greeting`; a dropped receiver would throw.
41 const result = yield* db.echo(7);
42 expect(result).toBe("hi:7");
43 }),
44 );
45
46 it.effect(
47 "is lazy — does not resolve the underlying effect until yielded",
48 () =>
49 Effect.gen(function* () {
50 let built = 0;
51 const db = proxyChain<Db>(
52 Effect.sync(() => {
53 built++;
54 return makeDb();
55 }),
56 );
57
58 // Building the op chain must not touch the underlying effect.
59 const query = db.select().from("users");
60 expect(built).toBe(0);
61
62 yield* query;
63 expect(built).toBe(1);
64 }),
65 );
66
67 it.effect(
68 "works as an Effect inside Effect.all (regression: used to hang)",
69 () =>
70 Effect.gen(function* () {
71 const db = proxyChain<Db>(Effect.succeed(makeDb()));
72 const results = yield* Effect.all([
73 db.select().from("users"),
74 db.select().from("posts"),
75 ]);
76 expect(results).toEqual([["hi/users"], ["hi/posts"]]);
77 }),
78 );
79
80 it.effect("works inside Effect.forEach", () =>
81 Effect.gen(function* () {
82 const db = proxyChain<Db>(Effect.succeed(makeDb()));
83 const results = yield* Effect.forEach(
84 ["users", "posts", "comments"],
85 (table) => db.select().from(table),
86 );
87 expect(results).toEqual([["hi/users"], ["hi/posts"], ["hi/comments"]]);
88 }),
89 );
90
91 it.effect(
92 "supports `.pipe` on a chain (forwarded to the resolved effect)",
93 () =>
94 Effect.gen(function* () {
95 const db = proxyChain<Db>(Effect.succeed(makeDb()));
96 const rows = yield* db
97 .select()
98 .from("users")
99 .pipe(Effect.map((rows) => rows.map((r) => r.toUpperCase())));
100 expect(rows).toEqual(["HI/USERS"]);
101 }),
102 );
103
104 it.effect("supports `.pipe` recovering a failure", () =>
105 Effect.gen(function* () {
106 const db = proxyChain<{ boom: () => Effect.Effect<string, string> }>(
107 Effect.succeed({ boom: () => Effect.fail("nope") }),
108 );
109 const result = yield* db
110 .boom()
111 .pipe(Effect.catch((e) => Effect.succeed(`recovered:${e}`)));
112 expect(result).toBe("recovered:nope");
113 }),
114 );
115
116 it.effect("is recognized by predicate-dispatched Effect operators", () =>
117 Effect.gen(function* () {
118 const api = proxyChain(
119 Effect.succeed({
120 transaction: () => Effect.fail({ _tag: "TestError" } as const),
121 }),
122 );
123
124 const transaction = api.transaction();
125
126 expect(Effect.isEffect(transaction)).toBe(true);
127
128 const result = yield* transaction.pipe(
129 Effect.catchTag("TestError", () => Effect.succeed("recovered")),
130 );
131
132 expect(result).toBe("recovered");
133 }),
134 );
135
136 /**
137 * effect ≥ 4.0.0-rc.113's generator runner asks `value[ExitTypeId] !==
138 * undefined` to tell an Exit from an Effect. If the proxy answered that
139 * brand probe with a chain step, every `yield*` would resume with the
140 * proxy itself instead of the query result (the bug behind `users.length`
141 * serializing as `{ _id: "Effect", op: "OnSuccess" }`).
142 */
143 it.effect("does not record Effect brand probes as chain steps", () =>
144 Effect.gen(function* () {
145 const db = proxyChain<Db>(Effect.succeed(makeDb()));
146 const query = db.select().from("users") as unknown as Record<
147 PropertyKey,
148 unknown
149 >;
150 expect(query["~effect/Exit"]).toBeUndefined();
151 expect(Exit.isExit(query)).toBe(false);
152 expect(Effect.isEffect(query)).toBe(true);
153 // Symbols the runtime does not define on an Effect are absent too.
154 expect(query[Symbol.for("some/unrelated/probe")]).toBeUndefined();
155 // …and the chain still resolves to the real value when yielded.
156 const rows = yield* db.select().from("users");
157 expect(Array.isArray(rows)).toBe(true);
158 expect(rows).toEqual(["hi/users"]);
159 }),
160 );
161
162 it.effect("a `.pipe`-d chain still composes inside Effect.all", () =>
163 Effect.gen(function* () {
164 const db = proxyChain<Db>(Effect.succeed(makeDb()));
165 const results = yield* Effect.all([
166 db
167 .select()
168 .from("users")
169 .pipe(Effect.map((r) => r.length)),
170 db
171 .select()
172 .from("posts")
173 .pipe(Effect.map((r) => r.length)),
174 ]);
175 expect(results).toEqual([1, 1]);
176 }),
177 );
178
179 it.effect("propagates failures from the resolved effect", () =>
180 Effect.gen(function* () {
181 const db = proxyChain<{ boom: () => Effect.Effect<never, string> }>(
182 Effect.succeed({ boom: () => Effect.fail("nope") }),
183 );
184 const error = yield* Effect.flip(db.boom());
185 expect(error).toBe("nope");
186 }),
187 );
188
189 it.effect("propagates failures from the underlying (cached) effect", () =>
190 Effect.gen(function* () {
191 const db = proxyChain<Db>(Effect.fail("connect failed"));
192 const error = yield* Effect.flip(db.select().from("users"));
193 expect(error).toBe("connect failed");
194 }),
195 );
196
197 /**
198 * A fake `@effect/sql`-like client: a tagged-template function with
199 * synchronous fragment helpers (`insert`, `and`) hanging off it — the shape
200 * that surfaced the nested-proxy bug (`sql.insert(row)` inside an outer
201 * `` sql`...` `` template was passed to the real client as a proxy, which
202 * D1 then tried to bind as a parameter).
203 */
204 const makeSql = () => {
205 const render = (v: unknown): string =>
206 typeof v === "string" && v.startsWith("frag:")
207 ? v.slice("frag:".length)
208 : typeof v === "function"
209 ? "?fn"
210 : `?${JSON.stringify(v)}`;
211 const sql = (strings: TemplateStringsArray, ...values: unknown[]) =>
212 Effect.succeed(
213 strings.reduce(
214 (acc, s, i) => acc + s + (i < values.length ? render(values[i]) : ""),
215 "",
216 ),
217 );
218 sql.insert = (row: Record<string, unknown>) =>
219 `frag:(${Object.keys(row).join(",")}) VALUES (${Object.values(row).join(",")})`;
220 sql.and = (parts: unknown[]) => `frag:(${parts.map(render).join(" AND ")})`;
221 return sql;
222 };
223
224 type Sql = ReturnType<typeof makeSql>;
225
226 describe("nested chains as call arguments", () => {
227 it.effect(
228 "replays a nested chain argument against the same root (sql.insert)",
229 () =>
230 Effect.gen(function* () {
231 const sql = proxyChain<Sql>(Effect.succeed(makeSql()));
232 const query =
233 yield* sql`INSERT INTO users ${sql.insert({ id: 1, name: "'alice'" })}`;
234 expect(query).toBe("INSERT INTO users (id,name) VALUES (1,'alice')");
235 }),
236 );
237
238 it.effect("replays nested chains inside array arguments (sql.and)", () =>
239 Effect.gen(function* () {
240 const sql = proxyChain<Sql>(Effect.succeed(makeSql()));
241 const query = yield* sql`SELECT * FROM users WHERE ${sql.and([
242 sql.insert({ a: 1 }),
243 sql.insert({ b: 2 }),
244 ])}`;
245 expect(query).toBe(
246 "SELECT * FROM users WHERE ((a) VALUES (1) AND (b) VALUES (2))",
247 );
248 }),
249 );
250
251 it.effect("leaves plain values and non-chain objects untouched", () =>
252 Effect.gen(function* () {
253 const sql = proxyChain<Sql>(Effect.succeed(makeSql()));
254 const query = yield* sql`SELECT ${42} FROM ${{ obj: true }}`;
255 expect(query).toBe('SELECT ?42 FROM ?{"obj":true}');
256 }),
257 );
258
259 it.effect(
260 "resolves the underlying effect exactly once for outer + nested chains",
261 () =>
262 Effect.gen(function* () {
263 let built = 0;
264 const sql = proxyChain<Sql>(
265 Effect.sync(() => {
266 built++;
267 return makeSql();
268 }),
269 );
270 const query = sql`INSERT INTO t ${sql.insert({ x: 9 })}`;
271 // Building the composite chain must not resolve anything…
272 expect(built).toBe(0);
273 yield* query;
274 // …and the nested chain reuses the outer chain's resolved root.
275 expect(built).toBe(1);
276 }),
277 );
278
279 it.effect(
280 "leaves a chain over a *different* underlying effect untouched",
281 () =>
282 Effect.gen(function* () {
283 const sqlA = proxyChain<Sql>(Effect.succeed(makeSql()));
284 const other = proxyChain<Db>(Effect.succeed(makeDb()));
285 // `other.select()` is a proxy over a different cached effect — it
286 // can't be replayed synchronously, so it must pass through as-is
287 // (rendered as an opaque bind value by the fake client).
288 const query = yield* sqlA`SELECT ${other.select()}`;
289 expect(query).toBe("SELECT ?fn");
290 }),
291 );
292 });
293});