1import * as Config from "effect/Config";
2import * as ConfigProvider from "effect/ConfigProvider";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import { AlchemyContext } from "../AlchemyContext.ts";
7import { AuthProviders } from "../Auth/AuthProvider.ts";
8import { CredentialsStoreLive } from "../Auth/Credentials.ts";
9import { ProfileStoreLive } from "../Auth/Profile.ts";
10import { withProfileOverride } from "../Auth/Resolve.ts";
11import { Stack } from "../Stack.ts";
12import { Stage } from "../Stage.ts";
13import { loadConfigProvider } from "../Util/ConfigProvider.ts";
14
15/**
16 * The stack-specific half of the sidecar environment. One sidecar process
17 * serves many stacks (the test harness shares a single child across all
18 * test files), so this part travels per RPC session — as a query parameter
19 * on the session websocket — rather than being baked into the child's env
20 * at spawn time.
21 */
22export interface SessionEnvironment {
23 alchemyContext: AlchemyContext["Service"];
24 stack: {
25 name: string;
26 stage: string;
27 };
28}
29
30export interface RpcServerEnvironment {
31 profile: string | undefined;
32 envFile: string | undefined;
33 /**
34 * Default session environment for sessions that do not carry their own
35 * (legacy single-stack boots). Sessions created by {@link RpcProviderProxy}
36 * always send an explicit {@link SessionEnvironment}.
37 */
38 alchemyContext?: AlchemyContext["Service"];
39 stack?: {
40 name: string;
41 stage: string;
42 };
43}
44
45/** Query parameter carrying the JSON {@link SessionEnvironment} on session websockets. */
46export const SESSION_ENV_PARAM = "alchemy-session-env";
47
48export const encodeSessionEnvironment = (env: SessionEnvironment): string =>
49 JSON.stringify(env);
50
51export const decodeSessionEnvironment = (raw: string): SessionEnvironment =>
52 JSON.parse(raw) as SessionEnvironment;
53
54export type RpcEnvironmentServices = Layer.Success<ReturnType<typeof layer>>;
55
56export const layer = (
57 environment: Pick<RpcServerEnvironment, "profile" | "envFile"> &
58 SessionEnvironment,
59) =>
60 Layer.mergeAll(
61 ProfileStoreLive,
62 CredentialsStoreLive,
63 Layer.succeed(AuthProviders, {}),
64 ConfigProvider.layer(
65 loadConfigProvider(Option.fromNullishOr(environment.envFile)).pipe(
66 Effect.map((base) => withProfileOverride(base, environment.profile)),
67 ),
68 ),
69 Layer.succeed(AlchemyContext, environment.alchemyContext),
70 Layer.succeed(Stack, {
71 name: environment.stack.name,
72 stage: environment.stack.stage,
73 resources: {},
74 bindings: {},
75 actions: {},
76 }),
77 Layer.succeed(Stage, environment.stack.stage),
78 );
79
80export const RPC_SERVER_ENVIRONMENT_KEY =
81 "ALCHEMY_RPC_SERVER_ENVIRONMENT" as const;
82
83/** The spawn-time environment the parent baked into the child's process env. */
84export const fromProcessEnv: Effect.Effect<RpcServerEnvironment, unknown> =
85 Config.String(RPC_SERVER_ENVIRONMENT_KEY).pipe(
86 Config.map((raw) => JSON.parse(raw) as RpcServerEnvironment),
87 );
88
89/**
90 * Legacy single-stack boot: the full environment (including the stack) baked
91 * into the process env at spawn time. Used by children that serve exactly one
92 * stack for their whole lifetime (e.g. the Vite dev-server child, which gets
93 * an explicit environment from `startViteChild`). RPC sidecars do NOT use
94 * this — their sessions each carry a {@link SessionEnvironment}.
95 */
96export const fromEnv = () =>
97 Layer.unwrap(
98 fromProcessEnv.pipe(
99 Effect.flatMap(
100 (
101 environment,
102 ): Effect.Effect<ReturnType<typeof layer>, unknown, never> =>
103 environment.stack === undefined ||
104 environment.alchemyContext === undefined
105 ? Effect.die(
106 new Error(
107 `${RPC_SERVER_ENVIRONMENT_KEY} carries no stack/alchemyContext — this child requires the legacy single-stack environment`,
108 ),
109 )
110 : Effect.succeed(
111 layer({
112 profile: environment.profile,
113 envFile: environment.envFile,
114 stack: environment.stack,
115 alchemyContext: environment.alchemyContext,
116 }),
117 ),
118 ),
119 ),
120 );