1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as Lambda from "@distilled.cloud/aws/lambda";
4import { expect } from "alchemy-test";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import InitIOProbe from "./init-io-probe.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13interface InitIOBody {
14 nonce: string;
15 traceLength: number;
16 hasUag: boolean;
17 initFetches: number;
18}
19
20class FunctionNotReady extends Data.TaggedError("FunctionNotReady")<{
21 status: number;
22}> {}
23
24/**
25 * Pins the init-phase I/O contract on Lambda: one-shot async I/O in a
26 * Layer build (a real `fetch` at cold start, run under the instance scope)
27 * completes once per sandbox and the cached plain value serves every
28 * invocation.
29 */
30test.provider(
31 "async I/O in a layer build runs once per sandbox and serves every invocation",
32 (stack) =>
33 Effect.gen(function* () {
34 yield* stack.destroy();
35
36 const fn = yield* stack.deploy(
37 Effect.gen(function* () {
38 return yield* InitIOProbe;
39 }),
40 );
41
42 const client = yield* HttpClient.HttpClient;
43 const get = Effect.gen(function* () {
44 const res = yield* client.get(fn.functionUrl!).pipe(
45 Effect.flatMap((res) =>
46 res.status === 200
47 ? Effect.succeed(res)
48 : Effect.fail(new FunctionNotReady({ status: res.status })),
49 ),
50 Effect.retry({
51 while: (e): e is FunctionNotReady => e instanceof FunctionNotReady,
52 schedule: Schedule.max([
53 Schedule.exponential("500 millis"),
54 Schedule.recurs(10),
55 ]),
56 }),
57 );
58 return (yield* res.json) as unknown as InitIOBody;
59 }).pipe(Effect.orDie);
60
61 const first = yield* get;
62 const second = yield* get;
63 const concurrent = yield* Effect.all(
64 Array.from({ length: 4 }, () => get),
65 { concurrency: "unbounded" },
66 );
67
68 for (const body of [first, second, ...concurrent]) {
69 expect(body.hasUag).toBe(true);
70 expect(body.traceLength).toBeGreaterThan(0);
71 expect(body.nonce).toMatch(/^[0-9a-f-]{36}$/);
72 // Exactly one init fetch on whichever sandbox served this
73 // invocation, no matter how many invocations it has handled.
74 expect(body.initFetches).toBe(1);
75 }
76
77 yield* stack.destroy();
78
79 // Out-of-band proof the destroy removed the function from the cloud.
80 yield* Lambda.getFunction({ FunctionName: fn.functionName }).pipe(
81 Effect.flatMap(() =>
82 Effect.fail(new Error(`Function ${fn.functionName} still exists`)),
83 ),
84 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
85 Effect.retry({
86 schedule: Schedule.max([
87 Schedule.exponential(500),
88 Schedule.recurs(8),
89 ]),
90 }),
91 );
92 }),
93 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 600_000 },
94);