1import { fromCloudflareFetcher } from "@/Cloudflare/Fetcher";
2import { describe, expect, it } from "alchemy-test";
3import * as Effect from "effect/Effect";
4import * as HttpClientRequest from "effect/http/HttpClientRequest";
5
6// A Cloudflare fetcher whose first `failures` `.fetch()` calls reject with the
7// given message, then resolve with a 200 — models a Durable Object / service
8// binding that is briefly mid-propagation after deploy (workerd routes to a
9// stale script version that has no fetch handler yet).
10//
11// The retry lives in `fromCloudflareFetcher` — the single adapter every CF
12// binding flows through — NOT in any one higher-level wrapper (`toHttpClient`,
13// the RPC DO transport, etc.), because the "no fetch handler" window is a
14// property of invoking the binding itself, so every consumer should be
15// resilient to it without re-implementing the retry.
16const flakyFetcher = (failures: number, message: string) => {
17 let attempts = 0;
18 return {
19 attempts: () => attempts,
20 fetch: (_request: any, _opts?: any) => {
21 attempts++;
22 return attempts <= failures
23 ? Promise.reject(new Error(message))
24 : Promise.resolve(new Response(null, { status: 200 }));
25 },
26 connect: () => {
27 throw new Error("connect not supported in test");
28 },
29 };
30};
31
32describe(
33 "fromCloudflareFetcher",
34 { tags: ["unit", "provider:cloudflare", "local"] },
35 () => {
36 // `it.live` uses the real clock so the retry's backoff delays actually
37 // elapse (the default `it.effect` TestClock would never advance them).
38 it.live(
39 "retries the just-deployed 'no fetch handler' propagation window",
40 () =>
41 Effect.gen(function* () {
42 const fetcher = flakyFetcher(
43 2,
44 "Handler does not export a fetch() function.",
45 );
46 const client = fromCloudflareFetcher(fetcher as any);
47
48 const res = yield* client.fetch(HttpClientRequest.get("http://do/"));
49
50 expect(res.status).toBe(200);
51 // Two not-ready failures + one success.
52 expect(fetcher.attempts()).toBe(3);
53 }),
54 );
55
56 it.live("does not retry unrelated failures", () =>
57 Effect.gen(function* () {
58 const fetcher = flakyFetcher(99, "some other boom");
59 const client = fromCloudflareFetcher(fetcher as any);
60
61 const outcome = yield* client
62 .fetch(HttpClientRequest.get("http://do/"))
63 .pipe(Effect.exit);
64
65 expect(outcome._tag).toBe("Failure");
66 // A non-propagation error is surfaced on the first attempt — no retry.
67 expect(fetcher.attempts()).toBe(1);
68 }),
69 );
70 },
71);