1import * as AppConfig from "@/AWS/AppConfig";
2import * as Lambda from "@/AWS/Lambda";
3import * as S3 from "@/AWS/S3";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Stream from "effect/Stream";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "events-handler.ts");
13
14export class AppConfigEventsTestFunction extends Lambda.Function<Lambda.Function>()(
15 "AppConfigEventsTestFunction",
16) {}
17
18export default AppConfigEventsTestFunction.make(
19 {
20 main,
21 functionUrl: true,
22 timeout: Duration.seconds(30),
23 },
24 Effect.gen(function* () {
25 const app = yield* AppConfig.Application("EventsApp", {});
26 const env = yield* AppConfig.Environment("EventsEnv", {
27 applicationId: app.applicationId,
28 });
29 const profile = yield* AppConfig.ConfigurationProfile("EventsProfile", {
30 applicationId: app.applicationId,
31 locationUri: "hosted",
32 });
33 yield* AppConfig.HostedConfigurationVersion("EventsV1", {
34 applicationId: app.applicationId,
35 configurationProfileId: profile.configurationProfileId,
36 content: JSON.stringify({ revision: 1 }),
37 contentType: "application/json",
38 });
39 const strategy = yield* AppConfig.DeploymentStrategy("EventsAllAtOnce", {
40 deploymentDuration: 0,
41 growthFactor: 100,
42 finalBakeTime: 0,
43 replicateTo: "NONE",
44 });
45
46 // Deployment notifications arrive as separate async invocations that may
47 // land on a DIFFERENT warm instance than the one serving the test's
48 // polls, so instance memory cannot capture them. Persist each event to
49 // S3 under a deterministic key and read it back by key.
50 const bucket = yield* S3.Bucket("EventsBucket", { forceDestroy: true });
51 const putObject = yield* S3.PutObject(bucket);
52 const getObject = yield* S3.GetObject(bucket);
53
54 // Deployments are triggered at runtime (POST /deploy) so the extension
55 // association below exists before the first deployment starts.
56 const startDeployment = yield* AppConfig.StartDeployment(
57 app,
58 env,
59 profile,
60 strategy,
61 );
62
63 yield* AppConfig.consumeDeploymentEvents(
64 env,
65 { events: ["ON_DEPLOYMENT_START", "ON_DEPLOYMENT_COMPLETE"] },
66 (events) =>
67 events.pipe(
68 Stream.runForEach((event) =>
69 putObject({
70 Key: `events/${event.DeploymentNumber}-${event.Type}.json`,
71 Body: JSON.stringify(event),
72 ContentType: "application/json",
73 }).pipe(Effect.orDie),
74 ),
75 ),
76 );
77
78 return {
79 fetch: Effect.gen(function* () {
80 const request = yield* HttpServerRequest;
81 const url = new URL(request.originalUrl);
82 const pathname = url.pathname;
83
84 if (request.method === "POST" && pathname === "/deploy") {
85 const body = (yield* request.json) as unknown as {
86 version: string;
87 };
88 const started = yield* startDeployment({
89 ConfigurationVersion: body.version,
90 });
91 return yield* HttpServerResponse.json({
92 deploymentNumber: started.DeploymentNumber,
93 state: started.State,
94 });
95 }
96
97 if (request.method === "GET" && pathname === "/event") {
98 const number = url.searchParams.get("number");
99 const type = url.searchParams.get("type");
100 return yield* getObject({
101 Key: `events/${number}-${type}.json`,
102 }).pipe(
103 Effect.flatMap((result) =>
104 Stream.mkString(Stream.decodeText(result.Body!)),
105 ),
106 Effect.flatMap((text) =>
107 HttpServerResponse.json({ event: JSON.parse(text) }),
108 ),
109 // Notification not delivered yet — the test polls until it is.
110 Effect.catchTag("NoSuchKey", () =>
111 HttpServerResponse.json({ event: null }, { status: 404 }),
112 ),
113 );
114 }
115
116 if (request.method === "GET" && pathname === "/ping") {
117 return yield* HttpServerResponse.json({ ok: true });
118 }
119
120 return yield* HttpServerResponse.json(
121 { error: "Not found", method: request.method, pathname },
122 { status: 404 },
123 );
124 }).pipe(Effect.orDie),
125 };
126 }).pipe(
127 Effect.provide(
128 Layer.mergeAll(
129 Lambda.AppConfigDeploymentEventSource,
130 AppConfig.StartDeploymentHttp,
131 S3.PutObjectHttp,
132 S3.GetObjectHttp,
133 ),
134 ),
135 ),
136);