| 1 | /** @effect-diagnostics anyUnknownInErrorContext:off */ |
| 2 | |
| 3 | import * as Floci from "@alchemy.run/floci"; |
| 4 | import type { FlociError } from "@alchemy.run/floci"; |
| 5 | import { Credentials } from "@distilled.cloud/aws/Credentials"; |
| 6 | import type { RegionName } from "@distilled.cloud/aws/Region"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Redacted from "effect/Redacted"; |
| 10 | import * as ProviderLayer from "../../Local/ProviderLayer.ts"; |
| 11 | import type { Platform } from "../../Platform.ts"; |
| 12 | import type { ResourceClassLike, ResourceLike } from "../../Resource.ts"; |
| 13 | import { DEFAULT_LOCAL_ENDPOINT, LOCAL_ACCOUNT_ID } from "../AuthProvider.ts"; |
| 14 | import * as Endpoint from "../Endpoint.ts"; |
| 15 | import { AWSEnvironment } from "../Environment.ts"; |
| 16 | import * as Region from "../Region.ts"; |
| 17 | import { provideProviderContext } from "./ProviderContext.ts"; |
| 18 | |
| 19 | /** |
| 20 | * Fixed dummy account used for every floci-emulated resource. Attributes |
| 21 | * computed from it (ARNs, queue URLs) double as proof that no real AWS |
| 22 | * account was involved. |
| 23 | */ |
| 24 | export const FLOCI_ACCOUNT_ID = LOCAL_ACCOUNT_ID; |
| 25 | |
| 26 | /** Region every floci-emulated resource lives in. */ |
| 27 | export const FLOCI_REGION = "us-east-1"; |
| 28 | |
| 29 | // Annotated (not inferred): the inferred union names distilled's Endpoint |
| 30 | // through a non-portable relative path (TS2883), and consumers only ever |
| 31 | // hand this to `provideProviderContext`, which takes `Layer<any, any, never>`. |
| 32 | const makeFlociServices = (): Layer.Layer<any, FlociError, never> => { |
| 33 | const region = FLOCI_REGION as RegionName; |
| 34 | const resolved = { |
| 35 | accessKeyId: Redacted.make("test"), |
| 36 | secretAccessKey: Redacted.make("test"), |
| 37 | sessionToken: undefined, |
| 38 | region, |
| 39 | }; |
| 40 | const credentials = Effect.succeed(resolved); |
| 41 | return Layer.mergeAll( |
| 42 | // Pin every distilled SDK call made by a wrapped lifecycle method to the |
| 43 | // emulator gateway with dummy credentials in the emulator's region. |
| 44 | Endpoint.of(DEFAULT_LOCAL_ENDPOINT), |
| 45 | Region.of(FLOCI_REGION), |
| 46 | Layer.succeed(Credentials, credentials), |
| 47 | // Providers read `AWSEnvironment.current` inside lifecycle operations to |
| 48 | // compute ARNs/attrs (accountId, region) — override it so computed |
| 49 | // identities carry the dummy account and the emulator endpoint. |
| 50 | Layer.succeed( |
| 51 | AWSEnvironment, |
| 52 | Effect.succeed({ |
| 53 | accountId: FLOCI_ACCOUNT_ID, |
| 54 | region: FLOCI_REGION, |
| 55 | credentials, |
| 56 | endpoint: DEFAULT_LOCAL_ENDPOINT, |
| 57 | }), |
| 58 | ), |
| 59 | // Building the services guarantees the emulator is serving: reuses |
| 60 | // anything already listening on the endpoint, otherwise starts (or |
| 61 | // revives) the managed `alchemy-floci` container and waits for health. |
| 62 | Layer.effectDiscard(Floci.ensureFloci({ port: Floci.DEFAULT_FLOCI_PORT })), |
| 63 | ); |
| 64 | }; |
| 65 | |
| 66 | let flociServicesLayer: ReturnType<typeof makeFlociServices> | undefined; |
| 67 | |
| 68 | /** |
| 69 | * The floci-scoped override context for local-mode AWS providers, as a |
| 70 | * **module-memoized layer reference** (see the note on |
| 71 | * [Local/ProviderLayer.ts](../../Local/ProviderLayer.ts)): every |
| 72 | * {@link flociDual} registration shares this one reference, so the stack |
| 73 | * build's MemoMap constructs it — and runs `ensureFloci()` — exactly once |
| 74 | * per stack build, and only when a local-mode provider is actually demanded. |
| 75 | */ |
| 76 | export const flociServices = () => (flociServicesLayer ??= makeFlociServices()); |
| 77 | |
| 78 | /** |
| 79 | * Registers an AWS resource provider with both a **live** and a **local** |
| 80 | * (floci-emulated) implementation via `ProviderLayer.dual`. The local |
| 81 | * variant is the SAME live provider code with every lifecycle method |
| 82 | * endpoint-wrapped to the floci emulator ({@link flociServices}), so |
| 83 | * `alchemy dev` routes the resource to the emulator while `alchemy deploy` |
| 84 | * (and `Alchemy.remote()` in dev) keeps hitting the real cloud. |
| 85 | * |
| 86 | * @example |
| 87 | * ```ts |
| 88 | * // in Providers.ts, replacing `S3.BucketProvider(),`: |
| 89 | * flociDual(S3.Bucket, () => S3.BucketProvider()), |
| 90 | * ``` |
| 91 | */ |
| 92 | export const flociDual = <R extends ResourceLike, ROut, E, RIn>( |
| 93 | cls: |
| 94 | | ResourceClassLike<R> |
| 95 | | Platform<R, any, any, any, any> |
| 96 | | { Type: R["Type"] }, |
| 97 | live: () => Layer.Layer<ROut, E, RIn>, |
| 98 | ) => |
| 99 | ProviderLayer.dual(cls, { |
| 100 | live, |
| 101 | local: () => provideProviderContext(live(), flociServices()), |
| 102 | // Registered as the resource's local data plane so deploy-time binding |
| 103 | // clients (Action bodies, plan-time `execute`) route their API calls to |
| 104 | // the emulator whenever the bound resource resolves to local mode. |
| 105 | dataPlane: flociServices, |
| 106 | }); |