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";
9import DynamicLoaderGetWorker from "./fixtures/dynamic-worker-loader/get-worker.ts";
10
11// `dev: true` runs local providers behind the RPC sidecar proxy by default,
12// matching the process topology of the real `alchemy dev` command.
13const { test } = Test.make({
14 providers: Cloudflare.providers(),
15 dev: true,
16});
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
24 status: number;
25 body: string;
26}> {}
27
28const getJsonReady = (url: string) =>
29 Effect.gen(function* () {
30 const client = yield* HttpClient.HttpClient;
31 const res = yield* client.get(url).pipe(
32 Effect.flatMap((res) =>
33 res.status === 200
34 ? Effect.succeed(res)
35 : res.text.pipe(
36 Effect.flatMap((body) =>
37 Effect.fail(new WorkerNotReady({ status: res.status, body })),
38 ),
39 ),
40 ),
41 Effect.retry({
42 while: (e): e is WorkerNotReady => e instanceof WorkerNotReady,
43 schedule: Schedule.max([
44 Schedule.min([
45 Schedule.exponential("500 millis"),
46 Schedule.spaced("2 seconds"),
47 ]),
48 Schedule.recurs(10),
49 ]),
50 }),
51 );
52 return yield* res.json;
53 }).pipe(Effect.orDie);
54
55/**
56 * Deploys an Effect-native Worker that calls `loader.get()` then
57 * `worker.fetch()` against local workerd. Native get() returns a WorkerStub
58 * (fetcher is getEntrypoint()); wrapping it as a Fetcher made fetch throw
59 * (#1382). A second request with the same id must reuse the isolate.
60 */
61test.provider(
62 "local worker get() fetch works and reuses the isolate",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const deployed = yield* stack.deploy(
68 Effect.gen(function* () {
69 const worker = yield* DynamicLoaderGetWorker;
70 return { worker };
71 }),
72 );
73
74 expect(deployed.worker.url).toMatch(/^http:\/\/localhost:\d+$/);
75
76 const first = (yield* getJsonReady(
77 `${deployed.worker.url}/?id=local-reuse`,
78 )) as { id: string; hits: number };
79 expect(first).toEqual({ id: "local-reuse", hits: 1 });
80
81 const second = (yield* getJsonReady(
82 `${deployed.worker.url}/?id=local-reuse`,
83 )) as { id: string; hits: number };
84 expect(second).toEqual({ id: "local-reuse", hits: 2 });
85
86 const named = (yield* getJsonReady(
87 `${deployed.worker.url}/?id=local-named&entrypoint=1`,
88 )) as { id: string; hits: number };
89 expect(named).toEqual({ id: "local-named", hits: 1 });
90
91 yield* stack.destroy();
92 }).pipe(logLevel),
93 {
94 tags: ["provider:cloudflare", "provider:cloudflare:worker", "local"],
95 timeout: 180_000,
96 },
97);