| 1 | /** @effect-diagnostics anyUnknownInErrorContext:off */ |
| 2 | |
| 3 | import * as Context from "effect/Context"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import { AlchemyContext } from "../AlchemyContext.ts"; |
| 7 | import type { Platform } from "../Platform.ts"; |
| 8 | import { Provider, type ProviderService } from "../Provider.ts"; |
| 9 | import { defaultProviderMode, type ProviderMode } from "../ProviderMode.ts"; |
| 10 | import type { ResourceClassLike, ResourceLike } from "../Resource.ts"; |
| 11 | |
| 12 | /** |
| 13 | * Registers a resource provider with both a **live** and a **local** |
| 14 | * implementation, making provider mode a first-class, per-run *and* |
| 15 | * per-resource concern: |
| 16 | * |
| 17 | * - The `Provider(type)` service placed in context delegates to the |
| 18 | * variant for the run's default mode (`AlchemyContext.dev ? "local" : |
| 19 | * "live"`). `findProvider` resolves the concrete variant, including its |
| 20 | * optional lifecycle methods and metadata. |
| 21 | * - Both variants are exposed via {@link ProviderService.modes} as lazy, |
| 22 | * memoized builders. NEITHER is constructed at registration: a variant |
| 23 | * (and the mode-specific dependency layers composed inside its thunk) is |
| 24 | * built the first time something demands it — planning a resource of |
| 25 | * this type, deleting a state row stamped with its mode, a nuke scan. A |
| 26 | * dual provider whose resources never appear in a run costs nothing, and |
| 27 | * in dev the sidecar process a local variant spawns only starts once a |
| 28 | * resource of its type is actually planned. |
| 29 | * |
| 30 | * Laziness mechanics: the layer is a {@link Layer.fromBuildMemo}, so the |
| 31 | * build itself is memoized by layer identity (one provider instance — and |
| 32 | * one instance registry for local providers — per memo-map ancestry, i.e. |
| 33 | * shared across every stack build that forks the same root, exactly like |
| 34 | * ordinary layers). The build receives its `MemoMap` and `Scope`; variant |
| 35 | * layers are built with `Layer.buildWithMemoMap` against that same memo |
| 36 | * map and scope, which means: |
| 37 | * |
| 38 | * - dependency layers shared between different providers' local variants |
| 39 | * (e.g. Cloudflare's `localRuntimeServices()`) are constructed exactly |
| 40 | * once, provided the thunks share the layer *reference*; |
| 41 | * - lazily-built services live until the memoized entry's scope closes |
| 42 | * (when every borrowing scope has closed), like any eagerly-built |
| 43 | * provider. |
| 44 | * |
| 45 | * @example |
| 46 | * ```ts |
| 47 | * export const WorkerProvider = () => |
| 48 | * ProviderLayer.dual(Worker, { |
| 49 | * live: () => LiveWorkerProvider(), |
| 50 | * local: () => |
| 51 | * LocalWorkerProvider().pipe(Layer.provide(localRuntimeServices())), |
| 52 | * }); |
| 53 | * ``` |
| 54 | */ |
| 55 | export const dual = < |
| 56 | R extends ResourceLike, |
| 57 | LayerLive extends Layer.Layer<any, any, any>, |
| 58 | LayerLocal extends Layer.Layer<any, any, any>, |
| 59 | >( |
| 60 | // Only the resource type string is needed — a ResourceClass, a Platform, |
| 61 | // or a bare `{ Type }` (useful when importing the class would create a |
| 62 | // module cycle) all satisfy this. |
| 63 | cls: |
| 64 | | ResourceClassLike<R> |
| 65 | | Platform<R, any, any, any, any> |
| 66 | | { Type: R["Type"] }, |
| 67 | input: { |
| 68 | live: () => LayerLive; |
| 69 | local: () => LayerLocal; |
| 70 | /** |
| 71 | * Data-plane override context for the local mode — the layer of cloud |
| 72 | * environment services (endpoint, credentials, region) the local |
| 73 | * lifecycle variant runs under (e.g. AWS's `flociServices()`). Stamped |
| 74 | * onto the registered service as |
| 75 | * {@link ProviderService.localDataPlane} so `Binding.Service` clients |
| 76 | * can route deploy-time data-plane calls (Action bodies, plan-time |
| 77 | * `execute`) to the emulator when the bound resource resolves local. |
| 78 | * Pass a module-memoized layer reference. |
| 79 | */ |
| 80 | dataPlane?: () => Layer.Layer<any, any, never>; |
| 81 | /** |
| 82 | * Data-plane override for **live** mode — the inverse of |
| 83 | * {@link dataPlane}. Stamped as {@link ProviderService.liveDataPlane} so |
| 84 | * `Alchemy.remote()` binding clients in a `dev` run provide the live |
| 85 | * chain closest (ambient is the emulator). Pass a module-memoized |
| 86 | * layer reference. AWS duals also get this from |
| 87 | * `pinCollectionEnvironment`. |
| 88 | */ |
| 89 | liveDataPlane?: () => Layer.Layer<any, any, never>; |
| 90 | }, |
| 91 | ): Layer.Layer< |
| 92 | Layer.Success<LayerLive | LayerLocal>, |
| 93 | Layer.Error<LayerLive | LayerLocal>, |
| 94 | Layer.Services<LayerLive | LayerLocal> | AlchemyContext |
| 95 | > => |
| 96 | Layer.fromBuildMemo((memoMap, scope) => |
| 97 | Effect.gen(function* () { |
| 98 | // The layer-build context contains everything provided to the |
| 99 | // provider stack (API clients, environments, ...). Captured here so |
| 100 | // lazily-built variants see the same services as an eager build. |
| 101 | const context = yield* Effect.context<never>(); |
| 102 | const defaultMode = yield* defaultProviderMode; |
| 103 | |
| 104 | const buildVariant = (mode: ProviderMode) => |
| 105 | Layer.buildWithMemoMap( |
| 106 | (mode === "live" ? input.live() : input.local()).pipe( |
| 107 | Layer.provide(Layer.succeedContext(context)), |
| 108 | ) as Layer.Layer<any, any, never>, |
| 109 | memoMap, |
| 110 | scope, |
| 111 | ).pipe( |
| 112 | Effect.map((built): ProviderService<R> => ({ |
| 113 | ...(built.mapUnsafe.get(cls.Type) as ProviderService<R>), |
| 114 | mode, |
| 115 | })), |
| 116 | ); |
| 117 | |
| 118 | // Memoized so each variant is constructed at most once per stack |
| 119 | // build, no matter how many resources/deletes resolve it. |
| 120 | const cached = { |
| 121 | live: yield* Effect.cached(buildVariant("live")), |
| 122 | local: yield* Effect.cached(buildVariant("local")), |
| 123 | }; |
| 124 | |
| 125 | // Lazy accessors die on construction failure: a provider layer that |
| 126 | // cannot be built is fatal (Providers.ts pipes `Layer.orDie` around |
| 127 | // the eager path for the same reason). |
| 128 | const modes: { |
| 129 | readonly [M in ProviderMode]: Effect.Effect<ProviderService<R>>; |
| 130 | } = { |
| 131 | live: Effect.orDie(cached.live), |
| 132 | local: Effect.orDie(cached.local), |
| 133 | }; |
| 134 | |
| 135 | // Every engine path resolves a concrete variant through |
| 136 | // `providerForMode` and reads optional-method presence (`read`, |
| 137 | // `precreate`, `tail`, `logs`), `version`, `stables` and `nuke` |
| 138 | // there. The registered service therefore only needs the required |
| 139 | // lifecycle methods for structural provider checks and direct |
| 140 | // registration access — each forwarding to the default |
| 141 | // variant, built on first call. |
| 142 | const variant = modes[defaultMode]; |
| 143 | |
| 144 | return Context.make(Provider(cls.Type) as any, { |
| 145 | aliases: "Aliases" in cls ? cls.Aliases : undefined, |
| 146 | diff: (input) => |
| 147 | Effect.flatMap(variant, (service) => |
| 148 | service.diff === undefined ? Effect.void : service.diff(input), |
| 149 | ), |
| 150 | reconcile: (input) => |
| 151 | Effect.flatMap(variant, (service) => service.reconcile(input)), |
| 152 | delete: (input) => |
| 153 | Effect.flatMap(variant, (service) => service.delete(input)), |
| 154 | list: (...args) => |
| 155 | Effect.flatMap(variant, (service) => service.list(...args)), |
| 156 | mode: defaultMode, |
| 157 | modes, |
| 158 | localDataPlane: input.dataPlane, |
| 159 | liveDataPlane: input.liveDataPlane, |
| 160 | } satisfies ProviderService<R>); |
| 161 | }), |
| 162 | ) as any; |