1/** @effect-diagnostics anyUnknownInErrorContext:off */
2
3import { Credentials } from "@distilled.cloud/aws/Credentials";
4import { Endpoint } from "@distilled.cloud/aws";
5import { Region } from "@distilled.cloud/aws/Region";
6import * as Context from "effect/Context";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Predicate from "effect/Predicate";
11import * as Stream from "effect/Stream";
12import type { ProviderService } from "../../Provider.ts";
13import type { ResourceLike } from "../../Resource.ts";
14import { AWSEnvironment } from "../Environment.ts";
15
16/**
17 * Wraps a {@link ProviderService} so that EVERY lifecycle method
18 * (`reconcile`, `diff`, `read`, `delete`, `list`, `precreate`, `tail`,
19 * `logs`, ...) runs with the given services provided. The services are
20 * provided *closest* to the lifecycle effect, so they override anything the
21 * engine's ambient context supplies (credentials, region, endpoint,
22 * environment).
23 *
24 * Method identity/shape is preserved exactly — the Provider interface is
25 * structural, and the Proxy forwards non-function members (`version`,
26 * `stables`, `aliases`, `nuke`, ...) untouched. Modeled on the lifecycle
27 * Proxy in [Local/RpcProvider.ts](../../Local/RpcProvider.ts).
28 */
29export const withProviderContext = <R extends ResourceLike>(
30 provider: ProviderService<R>,
31 services: Layer.Layer<any, never, never>,
32): ProviderService<R> =>
33 new Proxy(provider, {
34 get: (target, prop) => {
35 const value = (target as any)[prop];
36 if (prop === "modes" && value !== undefined) {
37 // Mode-resolved variants (`findProviderByType(type, mode)`, used for
38 // stamped-mode deletes and `Alchemy.remote()`) bypass this Proxy —
39 // wrap the services THEY resolve to as well, so a live variant's
40 // ops carry the same context. A local variant's own data-plane
41 // override is provided closer and still wins.
42 return Object.fromEntries(
43 Object.entries(value as Record<string, Effect.Effect<any>>).map(
44 ([mode, build]) => [
45 mode,
46 Effect.map(build, (resolved) =>
47 withProviderContext(resolved as ProviderService<R>, services),
48 ),
49 ],
50 ),
51 );
52 }
53 if (!Predicate.isFunction(value)) return value;
54 return (...args: any[]) => {
55 const result: unknown = value(...args);
56 if (Stream.isStream(result)) {
57 return Stream.provide(result, services);
58 }
59 if (Effect.isEffect(result)) {
60 return Effect.provide(result, services);
61 }
62 return result;
63 };
64 },
65 });
66
67/**
68 * Capture the ambient AWS environment — the exact tag set a local data
69 * plane overrides: {@link Endpoint}, {@link Region}, {@link Credentials},
70 * {@link AWSEnvironment} — as a layer that reproduces it verbatim. An
71 * absent Endpoint is pinned as `undefined` (the SDK default resolver), so
72 * a later ambient override cannot leak in.
73 */
74export const captureAwsEnvironment: Effect.Effect<
75 Layer.Layer<any, never, never>
76> = Effect.gen(function* () {
77 let ctx = Context.empty();
78 const endpoint = yield* Effect.serviceOption(Endpoint.Endpoint);
79 ctx = Context.add(
80 ctx,
81 Endpoint.Endpoint,
82 Option.getOrElse(endpoint, () => Effect.succeed(undefined)),
83 );
84 const region = yield* Effect.serviceOption(Region);
85 if (Option.isSome(region)) ctx = Context.add(ctx, Region, region.value);
86 const credentials = yield* Effect.serviceOption(Credentials);
87 if (Option.isSome(credentials)) {
88 ctx = Context.add(ctx, Credentials, credentials.value);
89 }
90 const environment = yield* Effect.serviceOption(AWSEnvironment);
91 if (Option.isSome(environment)) {
92 ctx = Context.add(ctx, AWSEnvironment, environment.value);
93 }
94 return Layer.succeedContext(ctx) as Layer.Layer<any, never, never>;
95});
96
97/**
98 * Pin every provider in a collection (and every mode variant it lazily
99 * resolves) to the AWS environment captured at REGISTRATION, provided
100 * closest around each lifecycle effect. Providers therefore always run
101 * against the environment they were registered with, regardless of the
102 * caller's ambient context — which in an `alchemy dev` run is the emulator
103 * (see AWS/Providers.ts): composition-time lookups get the emulator for
104 * free while live-mode and mode-agnostic providers keep hitting the real
105 * cloud. Local variants still win — their own data-plane override is
106 * provided closer.
107 */
108export const pinCollectionEnvironment = <
109 A extends {
110 readonly kind: "ProviderCollection";
111 get(service: string): ProviderService<any> | undefined;
112 readonly providers: Record<string, ProviderService>;
113 },
114>(
115 collection: A,
116 environment: Layer.Layer<any, never, never>,
117): A => {
118 const wrap = (provider: ProviderService | undefined) => {
119 if (provider === undefined) return undefined;
120 // Stamp the registration-captured live environment so Binding clients
121 // for `Alchemy.remote()` resources can provide it closest — in a
122 // `dev` run ambient is the emulator, and an unwrapped live client
123 // would miss the real cloud.
124 Object.assign(provider, { liveDataPlane: () => environment });
125 return withProviderContext(provider, environment);
126 };
127 const wrapped: Record<string, ProviderService> = {};
128 for (const [type, provider] of Object.entries(collection.providers)) {
129 wrapped[type] = wrap(provider)!;
130 }
131 return {
132 ...collection,
133 get: (service: string) => wrapped[service] ?? wrap(collection.get(service)),
134 providers: wrapped,
135 } as A;
136};
137
138const isProviderService = (value: unknown): value is ProviderService<any> =>
139 Predicate.hasProperty(value, "reconcile") &&
140 Predicate.isFunction(value.reconcile);
141
142/**
143 * Layer-level companion to {@link withProviderContext}: given a provider
144 * layer (e.g. `S3.BucketProvider()`) and a layer of override services,
145 * returns a layer that builds both and re-registers every provider service
146 * with its lifecycle methods wrapped in the override context.
147 *
148 * Built with `Layer.fromBuildMemo` (like `ProviderLayer.dual`) so the
149 * `services` layer is built through the ambient `MemoMap`: pass a
150 * **module-memoized layer reference** and it is constructed exactly once per
151 * stack build no matter how many providers are wrapped with it.
152 *
153 * The services are also provided to the provider layer's *build* (winning
154 * over the ambient context), so providers that resolve environment services
155 * at layer construction see the override too.
156 */
157export const provideProviderContext = <ROut, E, RIn, ServicesE>(
158 providerLayer: Layer.Layer<ROut, E, RIn>,
159 services: Layer.Layer<any, ServicesE, never>,
160): Layer.Layer<ROut, E | ServicesE, RIn> =>
161 Layer.fromBuildMemo((memoMap, scope) =>
162 Effect.gen(function* () {
163 const ambient = yield* Effect.context<never>();
164 // Built via the shared MemoMap: a module-memoized `services` reference
165 // is deduped to a single instance across every wrapped provider.
166 const servicesCtx = yield* Layer.buildWithMemoMap(
167 services,
168 memoMap,
169 scope,
170 );
171 const servicesLayer = Layer.succeedContext(servicesCtx);
172 const built = yield* Layer.buildWithMemoMap(
173 providerLayer.pipe(
174 // Closest wins: overrides first, then the ambient build context.
175 Layer.provide(servicesLayer),
176 Layer.provide(Layer.succeedContext(ambient)),
177 ) as Layer.Layer<any, any, never>,
178 memoMap,
179 scope,
180 );
181 const wrapped = new Map<string, any>();
182 for (const [key, value] of built.mapUnsafe) {
183 wrapped.set(
184 key,
185 isProviderService(value)
186 ? withProviderContext(value, servicesLayer)
187 : value,
188 );
189 }
190 return Context.makeUnsafe(wrapped) as Context.Context<ROut>;
191 }),
192 ) as Layer.Layer<ROut, E | ServicesE, RIn>;