| 1 | /** @effect-diagnostics anyUnknownInErrorContext:off */ |
| 2 | |
| 3 | import { Credentials } from "@distilled.cloud/aws/Credentials"; |
| 4 | import { Endpoint } from "@distilled.cloud/aws"; |
| 5 | import { Region } from "@distilled.cloud/aws/Region"; |
| 6 | import * as Context from "effect/Context"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; |
| 10 | import * as Predicate from "effect/Predicate"; |
| 11 | import * as Stream from "effect/Stream"; |
| 12 | import type { ProviderService } from "../../Provider.ts"; |
| 13 | import type { ResourceLike } from "../../Resource.ts"; |
| 14 | import { 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 | */ |
| 29 | export 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 | */ |
| 74 | export 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 | */ |
| 108 | export 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 | |
| 138 | const 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 | */ |
| 157 | export 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>; |