1import * as Keyspaces from "@/AWS/Keyspaces";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8
9const main = path.resolve(import.meta.dirname, "streams-handler.ts");
10
11export class KeyspacesStreamsTestFunction extends Lambda.Function<Lambda.Function>()(
12 "KeyspacesStreamsTestFunction",
13) {}
14
15export default KeyspacesStreamsTestFunction.make(
16 {
17 main,
18 functionUrl: true,
19 timeout: Duration.seconds(30),
20 },
21 Effect.gen(function* () {
22 const keyspace = yield* Keyspaces.Keyspace("StreamsKs", {
23 keyspaceName: "alchemy_streams_test_ks",
24 });
25 const table = yield* Keyspaces.Table("Orders", {
26 keyspaceName: keyspace.keyspaceName,
27 tableName: "orders",
28 columns: [
29 { name: "id", type: "uuid" },
30 { name: "total", type: "int" },
31 ],
32 partitionKeys: ["id"],
33 cdcSpecification: {
34 status: "ENABLED",
35 viewType: "NEW_AND_OLD_IMAGES",
36 },
37 });
38
39 const streams = yield* Keyspaces.TableStreams(table);
40
41 return {
42 fetch: Effect.gen(function* () {
43 const request = yield* HttpServerRequest;
44 const pathname = new URL(request.originalUrl).pathname;
45
46 // Traverse the table's CDC stream end-to-end: list streams →
47 // describe the latest → obtain a TRIM_HORIZON iterator on the first
48 // shard → read records (an idle table legitimately returns none).
49 if (request.method === "GET" && pathname === "/traverse") {
50 const listed = yield* streams.listStreams();
51 const latest = [...(listed.streams ?? [])].sort((a, b) =>
52 b.streamLabel.localeCompare(a.streamLabel),
53 )[0];
54 if (!latest) {
55 return yield* HttpServerResponse.json(
56 { error: "no stream visible yet" },
57 { status: 503 },
58 );
59 }
60 const stream = yield* streams.getStream({
61 streamArn: latest.streamArn,
62 });
63 const shardId = stream.shards?.[0]?.shardId;
64 let recordCount = -1;
65 if (shardId !== undefined) {
66 const iterator = yield* streams.getShardIterator({
67 streamArn: latest.streamArn,
68 shardId,
69 shardIteratorType: "TRIM_HORIZON",
70 });
71 if (iterator.shardIterator !== undefined) {
72 const records = yield* streams.getRecords({
73 shardIterator: iterator.shardIterator,
74 });
75 recordCount = records.changeRecords?.length ?? 0;
76 }
77 }
78 return yield* HttpServerResponse.json({
79 streamArn: stream.streamArn,
80 streamStatus: stream.streamStatus,
81 viewType: stream.streamViewType,
82 shardCount: stream.shards?.length ?? 0,
83 recordCount,
84 });
85 }
86
87 return yield* HttpServerResponse.json(
88 { error: "Not found", method: request.method, pathname },
89 { status: 404 },
90 );
91 }).pipe(Effect.orDie),
92 };
93 }).pipe(Effect.provide(Keyspaces.TableStreamsHttp)),
94);