1import * as Cloudflare from "@/Cloudflare";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Console from "effect/Console";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as RpcClient from "effect/rpc/RpcClient";
9import { CallerRpcs, TargetRpcs } from "./fixtures/rpc-worker-binding/group.ts";
10import Stack from "./fixtures/rpc-worker-binding/stack.ts";
11
12const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
13 providers: Cloudflare.providers(),
14});
15
16// `Test.rpcClientLayer` guards the transport against edge-generated HTML
17// bodies (workers.dev placeholder, error pages) that the RPC protocol would
18// otherwise surface as an opaque `RpcClientDefect`; see Test/Http.ts.
19const clientLayer = Test.rpcClientLayer;
20
21const logLevel = Effect.provideService(
22 MinimumLogLevel,
23 process.env.DEBUG ? "Debug" : "Info",
24);
25
26// Cap exponential backoff at 3s so retries stay bounded when the CF edge is
27// slow (otherwise the geometric blow-up dominates wall time).
28const readinessSchedule = Schedule.min([
29 Schedule.exponential("500 millis"),
30 Schedule.spaced("3 seconds"),
31]);
32
33// The caller worker forwards to the target via the service binding and wraps
34// the call in `Effect.orDie` (see fixtures/caller-worker.ts). On a freshly
35// deployed worker the service binding hasn't propagated to every Cloudflare
36// edge yet, so the first calls fail with `Worker not found.` — which arrives
37// at the client as a DEFECT, and `Effect.retry` does not retry defects.
38// Promote defects to failures so the readiness retry can absorb the transient
39// binding-propagation error (a genuine bug would simply keep failing until the
40// retry budget is exhausted).
41const retryReadyN =
42 (times: number) =>
43 <A, E, R>(eff: Effect.Effect<A, E, R>) =>
44 eff.pipe(
45 Effect.catchDefect((defect) => Effect.fail(defect)),
46 Effect.retry({ schedule: readinessSchedule, times }),
47 );
48
49const stack = beforeAll(deploy(Stack));
50afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
51
52test(
53 "RpcWorker: target worker exposes Greet",
54 Effect.gen(function* () {
55 const { targetUrl } = yield* stack;
56 yield* Effect.gen(function* () {
57 const client = yield* RpcClient.make(TargetRpcs);
58 const result = yield* client.Greet({ name: "world" }).pipe(
59 Effect.tapError(Console.log),
60 Effect.retry({
61 schedule: Schedule.exponential("500 millis"),
62 times: 5,
63 }),
64 );
65 expect(result.greeting).toBe("hello world");
66 }).pipe(Effect.scoped, Effect.provide(clientLayer(targetUrl)));
67 }).pipe(logLevel),
68 {
69 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
70 timeout: 60_000,
71 },
72);
73
74test(
75 "RpcWorker.bind: caller proxies through service binding to target",
76 Effect.gen(function* () {
77 const { callerUrl } = yield* stack;
78 yield* Effect.gen(function* () {
79 const client = yield* RpcClient.make(CallerRpcs);
80 const result = yield* client
81 .ProxyGreet({ name: "alchemy" })
82 .pipe(Effect.tapError(Console.log), retryReadyN(10));
83 expect(result.greeting).toBe("hello alchemy");
84 }).pipe(Effect.scoped, Effect.provide(clientLayer(callerUrl)));
85 }).pipe(logLevel),
86 {
87 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
88 timeout: 60_000,
89 },
90);
91
92test(
93 "RpcWorker.bind: 100 concurrent ProxyGreet calls do not hang",
94 Effect.gen(function* () {
95 const { callerUrl } = yield* stack;
96 yield* Effect.gen(function* () {
97 const client = yield* RpcClient.make(CallerRpcs);
98
99 // Warm the service binding by serially exercising it before the
100 // burst — workerd surfaces "Worker not found" defects until the
101 // target script is fully propagated to the same edge that the
102 // caller worker hits.
103 yield* client.ProxyGreet({ name: "warmup" }).pipe(retryReadyN(10));
104
105 const N = 100;
106 const results = yield* Effect.forEach(
107 Array.from({ length: N }, (_, i) => i),
108 (i) =>
109 client
110 .ProxyGreet({ name: `peer-${i}` })
111 .pipe(Effect.timeout("10 seconds"), retryReadyN(10)),
112 { concurrency: 32 },
113 );
114
115 expect(results).toHaveLength(N);
116 for (let i = 0; i < N; i++) {
117 expect(results[i].greeting).toBe(`hello peer-${i}`);
118 }
119 }).pipe(Effect.scoped, Effect.provide(clientLayer(callerUrl)));
120 }).pipe(logLevel),
121 {
122 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
123 timeout: 60_000,
124 },
125);