1import * as Alchemy from "@/index.ts";
2import * as Cloudflare from "@/Cloudflare";
3import * as Test from "@/Test/Alchemy";
4import { expect } from "alchemy-test";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import InitIOWorker from "./fixtures/init-io/worker.ts";
11
12const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
13 providers: Cloudflare.providers(),
14});
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21const Stack = Alchemy.Stack(
22 "InitIOTestStack",
23 { providers: Cloudflare.providers(), state: Cloudflare.state() },
24 Effect.gen(function* () {
25 const worker = yield* InitIOWorker;
26 return { url: worker.url.as<string>() };
27 }),
28);
29
30const stack = beforeAll(deploy(Stack));
31afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
32
33interface InitIOBody {
34 nonce: string;
35 traceLength: number;
36 hasUag: boolean;
37 initFetches: number;
38}
39
40class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
41 status: number;
42}> {}
43
44/**
45 * Pins the init-phase I/O contract: one-shot async I/O in a Layer build
46 * (here a real `fetch` of cloudflare.com/cdn-cgi/trace) runs exactly once
47 * per isolate, and the cached plain value serves every event — sequential
48 * and concurrent. A regression to per-event builds would report
49 * `initFetches > 1` and a fresh `nonce` per request; a broken cross-request
50 * build would fail the concurrent batch outright.
51 */
52test(
53 "async I/O in a layer build runs once per isolate and serves every event",
54 Effect.gen(function* () {
55 const { url } = yield* stack;
56 const client = yield* HttpClient.HttpClient;
57
58 const get = Effect.gen(function* () {
59 const res = yield* client.get(url).pipe(
60 Effect.flatMap((res) =>
61 res.status === 200
62 ? Effect.succeed(res)
63 : Effect.fail(new WorkerNotReady({ status: res.status })),
64 ),
65 Effect.retry({
66 while: (e): e is WorkerNotReady => e instanceof WorkerNotReady,
67 schedule: Schedule.max([
68 Schedule.exponential("500 millis"),
69 Schedule.recurs(10),
70 ]),
71 }),
72 );
73 return (yield* res.json) as unknown as InitIOBody;
74 }).pipe(Effect.orDie);
75
76 const first = yield* get;
77 const second = yield* get;
78 const concurrent = yield* Effect.all(
79 Array.from({ length: 6 }, () => get),
80 { concurrency: "unbounded" },
81 );
82
83 for (const body of [first, second, ...concurrent]) {
84 // The layer's fetch really ran and produced a usable value...
85 expect(body.hasUag).toBe(true);
86 expect(body.traceLength).toBeGreaterThan(0);
87 expect(body.nonce).toMatch(/^[0-9a-f-]{36}$/);
88 // ...exactly once on whichever isolate served this event, no matter
89 // how many events that isolate has handled.
90 expect(body.initFetches).toBe(1);
91 }
92 }).pipe(logLevel),
93 {
94 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
95 timeout: 180_000,
96 },
97);