1/** @effect-diagnostics anyUnknownInErrorContext:off */
2
3import * as Context from "effect/Context";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { AlchemyContext } from "../AlchemyContext.ts";
7import type { Platform } from "../Platform.ts";
8import { Provider, type ProviderService } from "../Provider.ts";
9import { defaultProviderMode, type ProviderMode } from "../ProviderMode.ts";
10import 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 */
55export 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;