1/**
2 * The source fragments the stress suite splices into the fixture's
3 * `<<REGION>>` markers.
4 *
5 * These are the substance of the suite: each one is a real change to the
6 * RESOURCE GRAPH — a new Lambda with its own bucket, a Queue plus its
7 * consumer event source, a new Durable Object class (and therefore a class
8 * migration) grafted onto a running Worker, a replacement-forcing schema
9 * change, a second MicroVM image. Keeping them here as named constants
10 * keeps the test file about *what is asserted* rather than about TypeScript
11 * embedded in string literals.
12 *
13 * Indentation matters: `patchRegion` splices these between markers that sit
14 * at a fixed depth in the fixture, so every fragment is written at the
15 * depth its region lives at.
16 */
17
18// ─── An entire extra Cloudflare Worker (add / rename / remove) ──────────
19
20export const extraWorkerSource = (marker: string) =>
21 `export default {\n` +
22 ` fetch: async () => Response.json({ marker: ${JSON.stringify(marker)} }),\n` +
23 `};\n`;
24
25export const extraWorkerDeclaration = (
26 logicalId: string,
27 port: "extra" | "extraAlt",
28) =>
29 ` const extraWorker = yield* Cloudflare.Worker(${JSON.stringify(logicalId)}, {\n` +
30 ` main: "./src/extra/extra-worker.ts",\n` +
31 ` dev: { port: PORTS.${port}, strictPort: true },\n` +
32 ` });\n`;
33
34export const extraWorkerOutput = " extraUrl: extraWorker.url,\n";
35
36// ─── A Worker that demands a port another Worker already holds ─────────
37// The cleanest deterministic APPLY failure available locally: the stack
38// imports and plans fine and exactly one resource cannot reconcile.
39
40export const portSquatterSource =
41 `export default { fetch: async () => new Response("squatter") };\n`;
42
43export const portSquatterDeclaration =
44 ` yield* Cloudflare.Worker("PortSquatter", {\n` +
45 ` main: "./src/extra/squatter.ts",\n` +
46 ` dev: { port: PORTS.echo, strictPort: true },\n` +
47 ` });\n`;
48
49// ─── An entire extra AWS Lambda, with its own S3 bucket ────────────────
50
51export const reportFunctionSource = (marker: string) =>
52 `import * as Lambda from "alchemy/AWS/Lambda";\n` +
53 `import * as S3 from "alchemy/AWS/S3";\n` +
54 `import * as Effect from "effect/Effect";\n` +
55 `import * as Layer from "effect/Layer";\n` +
56 `import * as Stream from "effect/Stream";\n` +
57 `import { HttpServerRequest } from "effect/http/HttpServerRequest";\n` +
58 `import * as HttpServerResponse from "effect/http/HttpServerResponse";\n` +
59 `\n` +
60 `/** Added to (and later removed from) the stack while dev is running. */\n` +
61 `export default class ReportFunction extends Lambda.Function<ReportFunction>()(\n` +
62 ` "ReportFunction",\n` +
63 ` { main: import.meta.url, functionUrl: true, memorySize: 256 },\n` +
64 ` Effect.gen(function* () {\n` +
65 ` const bucket = yield* S3.Bucket("ReportBucket", { forceDestroy: true });\n` +
66 ` const putObject = yield* S3.PutObject(bucket);\n` +
67 ` const getObject = yield* S3.GetObject(bucket);\n` +
68 ` return {\n` +
69 ` fetch: Effect.gen(function* () {\n` +
70 ` const request = yield* HttpServerRequest;\n` +
71 ` const url = new URL(request.originalUrl);\n` +
72 ` if (url.pathname === "/report") {\n` +
73 ` yield* putObject({ Key: "report.txt", Body: ${JSON.stringify(marker)} });\n` +
74 ` const object = yield* getObject({ Key: "report.txt" });\n` +
75 ` const text = yield* (\n` +
76 ` object.Body?.pipe(Stream.decodeText, Stream.mkString) ??\n` +
77 ` Effect.succeed("")\n` +
78 ` );\n` +
79 ` return yield* HttpServerResponse.json({ marker: ${JSON.stringify(marker)}, text });\n` +
80 ` }\n` +
81 ` return HttpServerResponse.text("not found", { status: 404 });\n` +
82 ` }).pipe(Effect.orDie),\n` +
83 ` };\n` +
84 ` }).pipe(Effect.provide(Layer.mergeAll(S3.GetObjectHttp, S3.PutObjectHttp))),\n` +
85 `) {}\n`;
86
87export const reportFunctionImport =
88 `import ReportFunction from "./src/extra/ReportFunction.ts";\n`;
89
90export const reportFunctionDeclaration =
91 ` const reportFunction = yield* ReportFunction;\n`;
92
93export const reportFunctionOutput =
94 " reportUrl: reportFunction.functionUrl,\n";
95
96// ─── A Queue + its consumer + a new Durable Object class, grafted onto
97// the already-running EchoWorker. This adds a resource, an event source,
98// AND a class migration to a live Worker. ──────────────────────────────
99
100export const echoQueueBindings =
101 ` const echoQueue = yield* Cloudflare.Queues.Queue("EchoQueue");\n` +
102 ` const echoQueueSend = yield* Cloudflare.Queues.WriteQueue(echoQueue);\n` +
103 ` const inbox = yield* Inbox;\n` +
104 ` yield* Cloudflare.Queues.consumeQueueMessages<{ id: string }>(\n` +
105 ` echoQueue,\n` +
106 ` (stream) =>\n` +
107 ` Stream.runForEach(stream, (message) =>\n` +
108 ` inbox.getByName("global").record(message.body.id),\n` +
109 ` ),\n` +
110 ` );\n`;
111
112export const echoQueueRoutes =
113 ` if (url.pathname === "/queue/send") {\n` +
114 ` const id = url.searchParams.get("id") ?? "none";\n` +
115 ` yield* echoQueueSend.send({ id }).pipe(Effect.orDie);\n` +
116 ` return yield* HttpServerResponse.json({ sent: id });\n` +
117 ` }\n` +
118 ` if (url.pathname === "/queue/received") {\n` +
119 ` const ids = yield* inbox.getByName("global").list();\n` +
120 ` return yield* HttpServerResponse.json({ ids });\n` +
121 ` }\n`;
122
123export const echoQueueLayers =
124 ` Cloudflare.Queues.WriteQueueBinding,\n` +
125 ` Cloudflare.Queues.EventSourceLive,\n`;
126
127export const echoInboxClass =
128 `/** Durable Object added to a LIVE worker — this carries a class migration. */\n` +
129 `export class Inbox extends Cloudflare.DurableObject<Inbox>()(\n` +
130 ` "Inbox",\n` +
131 ` Effect.succeed(\n` +
132 ` Effect.gen(function* () {\n` +
133 ` const state = yield* Cloudflare.DurableObjectState;\n` +
134 ` return {\n` +
135 ` record: Effect.fn(function* (id: string) {\n` +
136 ` const ids = (yield* state.storage.get<string[]>("ids")) ?? [];\n` +
137 ` yield* state.storage.put("ids", [...ids, id]);\n` +
138 ` }),\n` +
139 ` list: Effect.fn(function* () {\n` +
140 ` return (yield* state.storage.get<string[]>("ids")) ?? [];\n` +
141 ` }),\n` +
142 ` };\n` +
143 ` }),\n` +
144 ` ),\n` +
145 `) {}\n`;
146
147/** `Stream` is only imported once the queue consumer needs it. */
148export const echoStreamImport = `import * as Stream from "effect/Stream";\n`;
149
150// ─── A second S3 bucket + its bindings on the already-running Lambda ────
151
152export const lambdaArchiveBindings =
153 ` const archive = yield* S3.Bucket("ArchiveBucket", { forceDestroy: true });\n` +
154 ` const putArchive = yield* S3.PutObject(archive);\n` +
155 ` const getArchive = yield* S3.GetObject(archive);\n`;
156
157export const lambdaArchiveRoutes =
158 ` if (url.pathname === "/archive") {\n` +
159 ` yield* putArchive({ Key: "archive.txt", Body: "archived" });\n` +
160 ` const object = yield* getArchive({ Key: "archive.txt" });\n` +
161 ` const text = yield* (\n` +
162 ` object.Body?.pipe(Stream.decodeText, Stream.mkString) ??\n` +
163 ` Effect.succeed("")\n` +
164 ` );\n` +
165 ` return yield* HttpServerResponse.json({ text });\n` +
166 ` }\n`;
167
168// ─── A second MicroVM image, and the binding set to drive it ───────────
169
170export const secondImageSource = (marker: string) =>
171 `import * as AWS from "alchemy/AWS";\n` +
172 `import * as Effect from "effect/Effect";\n` +
173 `import { HttpServerRequest } from "effect/http/HttpServerRequest";\n` +
174 `import * as HttpServerResponse from "effect/http/HttpServerResponse";\n` +
175 `\n` +
176 `export const WorkerImageBuildRole = AWS.IAM.Role("StressWorkerVmBuildRole");\n` +
177 `\n` +
178 `/** A SECOND MicroVM image, added to the stack while dev is running. */\n` +
179 `export class WorkerMicrovm extends AWS.Lambda.MicrovmImage<\n` +
180 ` WorkerMicrovm,\n` +
181 ` { ping: () => Effect.Effect<string> }\n` +
182 `>()("StressWorkerVm") {}\n` +
183 `\n` +
184 `export default WorkerMicrovm.make(\n` +
185 ` WorkerImageBuildRole.pipe(\n` +
186 ` Effect.map((buildRole) => ({\n` +
187 ` main: import.meta.filename,\n` +
188 ` buildRole,\n` +
189 ` runtime: "bun" as const,\n` +
190 ` resources: [{ minimumMemoryInMiB: 512 }],\n` +
191 ` cpuConfigurations: [{ architecture: "ARM_64" as const }],\n` +
192 ` })),\n` +
193 ` ),\n` +
194 ` Effect.gen(function* () {\n` +
195 ` return {\n` +
196 ` fetch: Effect.gen(function* () {\n` +
197 ` yield* HttpServerRequest;\n` +
198 ` return HttpServerResponse.text(${JSON.stringify(marker)});\n` +
199 ` }),\n` +
200 ` ping: () => Effect.succeed(${JSON.stringify(marker)}),\n` +
201 ` };\n` +
202 ` }),\n` +
203 `);\n`;
204
205export const secondImageImport =
206 `import WorkerImageLive from "./src/extra/WorkerImage.ts";\n`;
207
208/** Spliced into the stack's `Layer.mergeAll`, so it ends with a comma. */
209export const secondImageLayer = ` WorkerImageLive,\n`;
210
211/** Bindings + a route on `MicrovmWorker` that drive the SECOND image. */
212export const secondImageWorkerImport =
213 `import { WorkerMicrovm } from "./extra/WorkerImage.ts";\n`;
214
215export const secondImageBindings =
216 ` const runWorkerVm = yield* AWS.Lambda.RunMicrovm(WorkerMicrovm);\n` +
217 ` const terminateWorkerVm = yield* AWS.Lambda.TerminateMicrovm(WorkerMicrovm);\n`;
218
219export const secondImageRoutes =
220 ` if (url.pathname === "/second") {\n` +
221 ` const vm = yield* runWorkerVm({\n` +
222 ` idlePolicy: {\n` +
223 ` maxIdleDurationSeconds: 900,\n` +
224 ` suspendedDurationSeconds: 300,\n` +
225 ` autoResumeEnabled: true,\n` +
226 ` },\n` +
227 ` });\n` +
228 ` yield* terminateWorkerVm({ microvmIdentifier: vm.microvmId }).pipe(\n` +
229 ` Effect.ignore,\n` +
230 ` );\n` +
231 ` return yield* HttpServerResponse.json({ microvmId: vm.microvmId });\n` +
232 ` }\n`;