1/**
2 * Bootstrap for `AWS.Lambda.Function` (zip, Node runtime). The generated
3 * entry imports this module and the user's `main`, and re-exports the
4 * handler this returns — see {@link ./Process.ts} for why the entry is a
5 * shim.
6 */
7import * as Credentials from "@distilled.cloud/aws/Credentials";
8import * as Endpoint from "@distilled.cloud/aws/Endpoint";
9import * as Region from "@distilled.cloud/aws/Region";
10import { layer as nodeServicesLayer } from "@effect/platform-node/NodeServices";
11import * as ConfigProvider from "effect/ConfigProvider";
12import * as Effect from "effect/Effect";
13import * as Exit from "effect/Exit";
14import * as Layer from "effect/Layer";
15import * as Logger from "effect/Logger";
16import { MinimumLogLevel } from "effect/References";
17import * as Scope from "effect/Scope";
18import { layer as fetchHttpClientLayer } from "effect/http/FetchHttpClient";
19import { registerLambdaExtension } from "../../AWS/Lambda/RuntimeExtension.ts";
20import { reifyBoundConfigProvider } from "../../Runtime.ts";
21import { entrypointLayer, entrypointTag, stackFromEnv } from "./Process.ts";
22
23/**
24 * Build the sandbox-lifetime layer stack and return the Lambda handler the
25 * bundled function registered.
26 *
27 * The layer build lives under an instance scope (not a transient
28 * `Effect.provide`/`Effect.scoped` region) so services and init-level
29 * finalizers live for the sandbox and are released at Shutdown — Lambda's
30 * SIGTERM phase, which the internal extension registered here buys us
31 * (without any registered extension the sandbox is killed with no signal at
32 * all). Each invocation still gets its own request scope from the handler
33 * dispatch.
34 */
35export const bootstrap = async (entrypoint: unknown): Promise<unknown> => {
36 await registerLambdaExtension();
37
38 const instanceScope = Scope.makeUnsafe();
39
40 const platform = Layer.mergeAll(
41 nodeServicesLayer,
42 fetchHttpClientLayer,
43 // TODO(sam): wire this up to telemetry more directly
44 Logger.layer([Logger.consolePretty()]),
45 );
46
47 const entryLayer = entrypointLayer(entrypoint).pipe(
48 Layer.provideMerge(stackFromEnv),
49 Layer.provideMerge(Credentials.fromEnv()),
50 Layer.provideMerge(Region.fromEnv()),
51 // AWS_ENDPOINT_URL is the LocalStack-standard override injected by local
52 // emulators (floci) into the Lambda container — without it, runtime
53 // bindings in `alchemy dev` would call REAL AWS with dummy credentials.
54 // Resolves undefined when unset, so live deploys are unaffected.
55 Layer.provideMerge(Endpoint.fromEnv()),
56 Layer.provideMerge(platform),
57 Layer.provideMerge(
58 Layer.succeed(
59 ConfigProvider.ConfigProvider,
60 // Auto-bound `Config` values arrive in the env as
61 // `{"_tag":"Redacted","value":...}` markers; reify them so a `Config`
62 // re-read inside a handler decodes the raw source value.
63 reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
64 ),
65 ),
66 Layer.provideMerge(
67 Layer.succeed(MinimumLogLevel, process.env.DEBUG ? "Debug" : "Info"),
68 ),
69 );
70
71 const handlerEffect: Effect.Effect<unknown, unknown> = Layer.buildWithScope(
72 entryLayer,
73 instanceScope,
74 ).pipe(
75 Effect.flatMap((context) =>
76 entrypointTag.pipe(
77 Effect.flatMap((func) => func.RuntimeContext.exports),
78 Effect.flatMap((exports: any) => exports.handler),
79 Effect.provideContext(context),
80 ),
81 ),
82 Scope.provide(instanceScope),
83 );
84
85 const handler = await Effect.runPromise(handlerEffect);
86
87 // Lambda's Shutdown phase: close the instance scope so init-level
88 // finalizers run, then exit inside the 500 ms budget. SIGKILL follows if
89 // we overstay, so finalizers must be fast and best-effort.
90 process.on("SIGTERM", () => {
91 console.log("[alchemy] SIGTERM — closing instance scope");
92 Effect.runPromise(Scope.close(instanceScope, Exit.void))
93 .catch((error) =>
94 console.error("[alchemy] shutdown finalizers failed", error),
95 )
96 .finally(() => process.exit(0));
97 });
98
99 return handler;
100};