| 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 | |
| 20 | export const extraWorkerSource = (marker: string) => |
| 21 | `export default {\n` + |
| 22 | ` fetch: async () => Response.json({ marker: ${JSON.stringify(marker)} }),\n` + |
| 23 | `};\n`; |
| 24 | |
| 25 | export 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 | |
| 34 | export 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 | |
| 40 | export const portSquatterSource = |
| 41 | `export default { fetch: async () => new Response("squatter") };\n`; |
| 42 | |
| 43 | export 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 | |
| 51 | export 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 | |
| 87 | export const reportFunctionImport = |
| 88 | `import ReportFunction from "./src/extra/ReportFunction.ts";\n`; |
| 89 | |
| 90 | export const reportFunctionDeclaration = |
| 91 | ` const reportFunction = yield* ReportFunction;\n`; |
| 92 | |
| 93 | export 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 | |
| 100 | export 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 | |
| 112 | export 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 | |
| 123 | export const echoQueueLayers = |
| 124 | ` Cloudflare.Queues.WriteQueueBinding,\n` + |
| 125 | ` Cloudflare.Queues.EventSourceLive,\n`; |
| 126 | |
| 127 | export 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. */ |
| 148 | export const echoStreamImport = `import * as Stream from "effect/Stream";\n`; |
| 149 | |
| 150 | // ─── A second S3 bucket + its bindings on the already-running Lambda ──── |
| 151 | |
| 152 | export 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 | |
| 157 | export 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 | |
| 170 | export 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 | |
| 205 | export 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. */ |
| 209 | export const secondImageLayer = ` WorkerImageLive,\n`; |
| 210 | |
| 211 | /** Bindings + a route on `MicrovmWorker` that drive the SECOND image. */ |
| 212 | export const secondImageWorkerImport = |
| 213 | `import { WorkerMicrovm } from "./extra/WorkerImage.ts";\n`; |
| 214 | |
| 215 | export const secondImageBindings = |
| 216 | ` const runWorkerVm = yield* AWS.Lambda.RunMicrovm(WorkerMicrovm);\n` + |
| 217 | ` const terminateWorkerVm = yield* AWS.Lambda.TerminateMicrovm(WorkerMicrovm);\n`; |
| 218 | |
| 219 | export 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`; |