1import * as Cloudflare from "alchemy/Cloudflare";
2import * as Effect from "effect/Effect";
3import { HttpServerRequest } from "effect/http/HttpServerRequest";
4import * as HttpServerResponse from "effect/http/HttpServerResponse";
5import { ECHO_MARKER } from "./echo/marker.ts";
6import { PORTS } from "./ports.ts";
7import SandboxDO from "./SandboxDO.ts";
8// <<ECHO_IMPORTS>>
9// <</ECHO_IMPORTS>>
10
11/**
12 * Effect-native Worker declared with `main: import.meta.url` — the shape
13 * where the STACK PROCESS imports the worker's own source. Editing this
14 * module (or anything it imports, e.g. `./echo/marker.ts`) is therefore
15 * seen by `bun --watch`, so a hot swap is followed by a full stack re-run.
16 * `api-worker.ts` covers the other path, where it is not.
17 *
18 * The `<<ECHO_BINDINGS>>` / `<<ECHO_ROUTES>>` regions are where the stress
19 * suite grows and shrinks this worker's binding set at runtime: new
20 * resources, new event sources, and new Durable Object classes (which
21 * carry class migrations) are added to a LIVE worker and removed again.
22 *
23 * Routes:
24 * - `GET /marker` → the marker constant (proof a reload landed)
25 * - `GET /kv` → KV put/get roundtrip through the local simulator
26 * - `GET /r2` → R2 put/get/list roundtrip
27 * - `GET /counter` → Durable Object increment (state survives reloads)
28 * - `GET /sandbox` → proxied into the local Cloudflare Container
29 */
30export default class EchoWorker extends Cloudflare.Worker<EchoWorker>()(
31 "EchoWorker",
32 {
33 main: import.meta.url,
34 // Pinned so the stress suite can probe this worker even while the stack
35 // module is in a broken state and the CLI prints no outputs.
36 dev: { port: PORTS.echo, strictPort: true },
37 },
38 Effect.gen(function* () {
39 const kvNamespace = yield* Cloudflare.KV.Namespace("EchoKV");
40 const bucket = yield* Cloudflare.R2.Bucket("EchoBucket", {
41 forceDestroy: true,
42 });
43 const kv = yield* Cloudflare.KV.ReadWriteNamespace(kvNamespace);
44 const r2 = yield* Cloudflare.R2.ReadWriteBucket(bucket);
45 const counters = yield* Counter;
46 const sandbox = yield* SandboxDO;
47 // <<ECHO_BINDINGS>>
48 // <</ECHO_BINDINGS>>
49
50 return {
51 fetch: Effect.gen(function* () {
52 const request = yield* HttpServerRequest;
53 const url = new URL(request.url, "http://echo");
54
55 if (url.pathname === "/marker") {
56 return yield* HttpServerResponse.json({ marker: ECHO_MARKER });
57 }
58
59 if (url.pathname === "/kv") {
60 const key = url.searchParams.get("key") ?? "hello";
61 yield* kv.put(key, `kv:${key}`).pipe(Effect.orDie);
62 const value = yield* kv.get(key).pipe(Effect.orDie);
63 return yield* HttpServerResponse.json({ value });
64 }
65
66 if (url.pathname === "/r2") {
67 const key = url.searchParams.get("key") ?? "hello.txt";
68 yield* r2.put(key, "hello from r2").pipe(Effect.orDie);
69 const object = yield* r2.get(key).pipe(Effect.orDie);
70 const text = object ? yield* object.text().pipe(Effect.orDie) : null;
71 const { objects } = yield* r2.list().pipe(Effect.orDie);
72 return yield* HttpServerResponse.json({
73 text,
74 keys: objects.map((o) => o.key),
75 });
76 }
77
78 if (url.pathname === "/counter") {
79 const name = url.searchParams.get("name") ?? "default";
80 const count = yield* counters.getByName(name).increment();
81 return yield* HttpServerResponse.json({ count });
82 }
83
84 if (url.pathname.startsWith("/sandbox")) {
85 return yield* sandbox
86 .getByName("stress")
87 .fetch(request)
88 .pipe(Effect.orDie);
89 }
90
91 // <<ECHO_ROUTES>>
92 // <</ECHO_ROUTES>>
93
94 return yield* HttpServerResponse.json({ marker: ECHO_MARKER });
95 }).pipe(Effect.orDie),
96 };
97 }).pipe(
98 Effect.provide([
99 Cloudflare.KV.ReadWriteNamespaceBinding,
100 Cloudflare.R2.ReadWriteBucketBinding,
101 // <<ECHO_LAYERS>>
102 // <</ECHO_LAYERS>>
103 ]),
104 ),
105) {}
106
107/**
108 * Durable Object state is the stress suite's proof that a hot reload
109 * REPLACED CODE rather than recreating the resource: the counter keeps
110 * climbing across every worker-source edit.
111 */
112export class Counter extends Cloudflare.DurableObject<Counter>()(
113 "Counter",
114 Effect.succeed(
115 Effect.gen(function* () {
116 const state = yield* Cloudflare.DurableObjectState;
117 return {
118 increment: Effect.fn(function* () {
119 const count = ((yield* state.storage.get<number>("count")) ?? 0) + 1;
120 yield* state.storage.put("count", count);
121 return count;
122 }),
123 };
124 }),
125 ),
126) {}
127
128// <<ECHO_CLASSES>>
129// <</ECHO_CLASSES>>