1import * as DynamoDB from "alchemy/AWS/DynamoDB";
2import * as Lambda from "alchemy/AWS/Lambda";
3import * as S3 from "alchemy/AWS/S3";
4import * as SQS from "alchemy/AWS/SQS";
5import * as Config from "effect/Config";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Stream from "effect/Stream";
9import { HttpServerRequest } from "effect/http/HttpServerRequest";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11import { LAMBDA_MARKER } from "./lambda/marker.ts";
12
13/**
14 * The AWS half of the stack: one Lambda owning an S3 bucket, a DynamoDB
15 * table, and an SQS queue it also consumes. Under `alchemy dev` all four
16 * land in the floci emulator and the function URL is served on
17 * `*.lambda-url.us-east-1.localhost:4566`.
18 *
19 * The stack imports this module (`main: import.meta.url`), so editing it —
20 * or `./lambda/marker.ts` — takes the full-restart reload path and the dev
21 * provider hot-swaps the function's code inside the emulator.
22 *
23 * `EchoWorker`'s sibling `ApiWorker` reaches this function URL from local
24 * workerd, which is the cross-cloud hop the stress suite pins.
25 *
26 * Routes:
27 * - `GET /` → marker + a `Config`-provided env var
28 * - `GET /s3` → PutObject/GetObject roundtrip
29 * - `GET /dynamo` → PutItem/GetItem roundtrip
30 * - `POST /queue/send` → SendMessage over the binding
31 * - `GET /queue/messages` → what the queue consumer recorded
32 */
33export default class ApiFunction extends Lambda.Function<ApiFunction>()(
34 "ApiFunction",
35 {
36 main: import.meta.url,
37 functionUrl: true,
38 memorySize: 512,
39 env: { LAMBDA_VARIABLE: "lambda-variable-v1" },
40 },
41 Effect.gen(function* () {
42 const bucket = yield* S3.Bucket("StressBucket", { forceDestroy: true });
43 const table = yield* DynamoDB.Table("StressTable", {
44 partitionKey: "id",
45 attributes: { id: "S" },
46 });
47 const queue = yield* SQS.Queue("StressQueue");
48
49 const getObject = yield* S3.GetObject(bucket);
50 const putObject = yield* S3.PutObject(bucket);
51 const getItem = yield* DynamoDB.GetItem(table);
52 const putItem = yield* DynamoDB.PutItem(table);
53 const sendMessage = yield* SQS.SendMessage(queue);
54 // <<LAMBDA_BINDINGS>>
55 // <</LAMBDA_BINDINGS>>
56
57 // Produce → deliver → consume, so the suite can prove the event-source
58 // glue survives every reload it inflicts on the stack.
59 yield* SQS.consumeQueueMessages(queue, (records) =>
60 records.pipe(
61 Stream.mapEffect((record) => {
62 const parsed = JSON.parse(record.body) as { id: string };
63 return putItem({
64 Item: { id: { S: `msg:${parsed.id}` }, body: { S: record.body } },
65 });
66 }),
67 Stream.runDrain,
68 Effect.orDie,
69 ),
70 );
71
72 return {
73 fetch: Effect.gen(function* () {
74 const request = yield* HttpServerRequest;
75 const url = new URL(request.originalUrl);
76
77 if (url.pathname === "/") {
78 const variable = yield* Config.String("LAMBDA_VARIABLE");
79 return yield* HttpServerResponse.json({
80 marker: LAMBDA_MARKER,
81 variable,
82 });
83 }
84
85 if (url.pathname === "/s3") {
86 yield* putObject({ Key: "hello.txt", Body: "hello from s3" });
87 const object = yield* getObject({ Key: "hello.txt" });
88 const text = yield* (
89 object.Body?.pipe(Stream.decodeText, Stream.mkString) ??
90 Effect.succeed("")
91 );
92 return yield* HttpServerResponse.json({ text });
93 }
94
95 if (url.pathname === "/dynamo") {
96 yield* putItem({
97 Item: { id: { S: "hello" }, content: { S: "hello from dynamo" } },
98 });
99 const item = yield* getItem({ Key: { id: { S: "hello" } } });
100 return yield* HttpServerResponse.json({
101 text: item.Item?.content?.S ?? null,
102 });
103 }
104
105 if (url.pathname === "/queue/send" && request.method === "POST") {
106 const body = yield* request.text;
107 yield* sendMessage({ MessageBody: body });
108 return yield* HttpServerResponse.json({ sent: true });
109 }
110
111 if (url.pathname === "/queue/messages") {
112 const id = url.searchParams.get("id") ?? "";
113 const item = yield* getItem({ Key: { id: { S: `msg:${id}` } } });
114 return yield* HttpServerResponse.json({
115 body: item.Item?.body?.S ?? null,
116 });
117 }
118
119 // <<LAMBDA_ROUTES>>
120 // <</LAMBDA_ROUTES>>
121
122 return HttpServerResponse.text("Not found", { status: 404 });
123 }).pipe(Effect.orDie),
124 };
125 }).pipe(
126 Effect.provide(
127 Layer.mergeAll(
128 Lambda.QueueEventSource,
129 S3.GetObjectHttp,
130 S3.PutObjectHttp,
131 DynamoDB.GetItemHttp,
132 DynamoDB.PutItemHttp,
133 SQS.SendMessageHttp,
134 // <<LAMBDA_LAYERS>>
135 // <</LAMBDA_LAYERS>>
136 ),
137 ),
138 ),
139) {}