1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9
10const { test } = Test.make({
11 providers: Cloudflare.providers(),
12 dev: true,
13});
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
21 status: number;
22}> {}
23
24const getJsonReady = (url: string) =>
25 Effect.gen(function* () {
26 const client = yield* HttpClient.HttpClient;
27 const res = yield* client.get(url).pipe(
28 Effect.flatMap((res) =>
29 res.status === 200
30 ? Effect.succeed(res)
31 : Effect.fail(new WorkerNotReady({ status: res.status })),
32 ),
33 Effect.retry({
34 while: (e): e is WorkerNotReady => e instanceof WorkerNotReady,
35 // Cap the backoff: an uncapped exponential over 10 recurs sums to
36 // ~8.5 minutes and turns a persistent non-200 into an apparent hang.
37 schedule: Schedule.max([
38 Schedule.min([
39 Schedule.exponential("500 millis"),
40 Schedule.spaced("2 seconds"),
41 ]),
42 Schedule.recurs(10),
43 ]),
44 }),
45 );
46 return yield* res.json;
47 }).pipe(Effect.orDie);
48
49const scriptFor = (version: string) => `export default {
50 async fetch() {
51 return Response.json({ version: "${version}" });
52 },
53};`;
54
55/**
56 * Inline \`script:\` workers serve under the local provider without the
57 * bundler (the string IS the module), and editing the script restarts the
58 * instance via the hashed config. This used to hang silently: \`start\`
59 * returned the proxy URL while the bundler waited forever on a missing
60 * \`main\`.
61 */
62test.provider(
63 "inline script worker serves locally and restarts on script change",
64 (stack) =>
65 Effect.gen(function* () {
66 yield* stack.destroy();
67
68 const deploy = (version: string) =>
69 stack.deploy(
70 Effect.gen(function* () {
71 return {
72 worker: yield* Cloudflare.Worker("script-local-worker", {
73 script: scriptFor(version),
74 }),
75 };
76 }),
77 );
78
79 const v1 = yield* deploy("v1");
80
81 // The local provider serves from the dev proxy — proof no cloud call
82 // ran.
83 expect(v1.worker.url).toMatch(/^http:\/\/localhost:\d+$/);
84
85 const first = (yield* getJsonReady(`${v1.worker.url}`)) as {
86 version: string;
87 };
88 expect(first.version).toBe("v1");
89
90 // Changing the script changes the config hash — the instance restarts
91 // with the new module.
92 const v2 = yield* deploy("v2");
93 const second = (yield* getJsonReady(`${v2.worker.url}`)) as {
94 version: string;
95 };
96 expect(second.version).toBe("v2");
97
98 yield* stack.destroy();
99 }).pipe(logLevel),
100 {
101 tags: ["provider:cloudflare", "provider:cloudflare:worker", "local"],
102 timeout: 120_000,
103 },
104);