1import type * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Predicate from "effect/Predicate";
6import type { Scope } from "effect/Scope";
7import * as Stream from "effect/Stream";
8import { AlchemyContext } from "../AlchemyContext.ts";
9import { Artifacts, ArtifactStore, makeScopedArtifacts } from "../Artifacts.ts";
10import { InstanceId } from "../InstanceId.ts";
11import type { Platform } from "../Platform.ts";
12import * as Provider from "../Provider.ts";
13import type { ResourceClassLike, ResourceLike } from "../Resource.ts";
14import { Stack } from "../Stack.ts";
15import { Stage } from "../Stage.ts";
16import { RpcProviderProxy } from "./RpcProviderProxy.ts";
17
18/**
19 * Constructs an RpcProvider for a given resource.
20 *
21 * An RpcProvider is a resource provider that can run in a separate process.
22 * This is used for local development so that the resource provider and its state can outlive the user's code when it is hot-reloaded.
23 * If the {@link RpcProviderProxy} is provided, the provider is run in a separate process, which we communicate with here using a websocket.
24 * Otherwise, the given effect is used to construct the provider directly.
25 *
26 * @note If the provider is running in a separate process, you may not need to construct all of its layers in this process.
27 * You can use {@link providerServices} or {@link providerServicesEffect} to construct such layers only when they are needed.
28 *
29 * @example
30 * ```ts
31 * export const MyProvider = RpcProvider.effect(
32 * MyResource,
33 * import.meta.resolve("./my-rpc-provider-server.ts"),
34 * Effect.gen(function* () {
35 * return {
36 * diff: ...,
37 * reconcile: ...,
38 * delete: ...,
39 * }
40 * })
41 * )
42 * ```
43 *
44 * @param cls - The tag of the resource class to construct a provider for.
45 * @param providersUrl - URL of the module whose default export is the provider group's layer (the layer this provider is registered in). When the provider runs in the dev sidecar, the sidecar imports that module on first use — see `Local/Sidecar.ts`. Typically `import.meta.resolve("./Local.ts")`.
46 * @param eff - The Effect to use to construct the provider.
47 * @returns A layer containing the RpcProvider.
48 */
49/**
50 * The provider shape accepted by {@link effect}. It is identical to
51 * {@link Provider.ProviderService} except that `list` is **optional**: local /
52 * dev RPC providers frequently have no cloud enumeration API, so the factory
53 * supplies a safe `() => Effect.succeed([])` default when one is not given. A
54 * provider that *can* enumerate its in-memory/local runtime state (e.g.
55 * `LocalWorkerProvider` enumerating its running instances) just defines `list`
56 * and the factory threads it straight through.
57 */
58type RpcProviderService<
59 R extends ResourceLike,
60 ReadReq = never,
61 DiffReq = never,
62 PrecreateReq = never,
63 ReconcileReq = never,
64 DeleteReq = never,
65 TailReq = never,
66 LogsReq = never,
67 ListReq = never,
68> = Omit<
69 Provider.ProviderService<
70 R,
71 ReadReq,
72 DiffReq,
73 PrecreateReq,
74 ReconcileReq,
75 DeleteReq,
76 TailReq,
77 LogsReq,
78 ListReq
79 >,
80 "list"
81> &
82 Partial<
83 Pick<
84 Provider.ProviderService<
85 R,
86 ReadReq,
87 DiffReq,
88 PrecreateReq,
89 ReconcileReq,
90 DeleteReq,
91 TailReq,
92 LogsReq,
93 ListReq
94 >,
95 "list"
96 >
97 >;
98
99/**
100 * Ensures a provider satisfies the now-required `list()` contract. If the
101 * provider already implements `list` (enumerating its local runtime state) it
102 * is returned untouched; otherwise a safe default that enumerates nothing
103 * (`() => Effect.succeed([])`) is injected. Local/dev providers have no cloud
104 * enumeration API, so `[]` is the correct default — never throw.
105 */
106const withDefaultList = <
107 R extends ResourceLike,
108 ReadReq,
109 DiffReq,
110 PrecreateReq,
111 ReconcileReq,
112 DeleteReq,
113 TailReq,
114 LogsReq,
115 ListReq,
116>(
117 provider: RpcProviderService<
118 R,
119 ReadReq,
120 DiffReq,
121 PrecreateReq,
122 ReconcileReq,
123 DeleteReq,
124 TailReq,
125 LogsReq,
126 ListReq
127 >,
128): Provider.ProviderService<
129 R,
130 ReadReq,
131 DiffReq,
132 PrecreateReq,
133 ReconcileReq,
134 DeleteReq,
135 TailReq,
136 LogsReq,
137 ListReq
138> =>
139 (provider.list
140 ? provider
141 : {
142 ...provider,
143 list: () => Effect.succeed([]),
144 }) as Provider.ProviderService<
145 R,
146 ReadReq,
147 DiffReq,
148 PrecreateReq,
149 ReconcileReq,
150 DeleteReq,
151 TailReq,
152 LogsReq,
153 ListReq
154 >;
155
156export const effect = <
157 R extends ResourceLike,
158 Req = never,
159 ReadReq = never,
160 DiffReq = never,
161 PrecreateReq = never,
162 ReconcileReq = never,
163 DeleteReq = never,
164 TailReq = never,
165 LogsReq = never,
166 ListReq = never,
167>(
168 cls: ResourceClassLike<R> | Platform<R, any, any, any, any, any>,
169 providersUrl: string,
170 eff: Effect.Effect<
171 RpcProviderService<
172 R,
173 ReadReq,
174 DiffReq,
175 PrecreateReq,
176 ReconcileReq,
177 DeleteReq,
178 TailReq,
179 LogsReq,
180 ListReq
181 >,
182 never,
183 Req
184 >,
185) =>
186 Provider.effect(
187 cls,
188 Effect.gen(function* () {
189 const client = yield* Effect.serviceOption(RpcProviderProxy);
190 const context = yield* Effect.context();
191 const stack = yield* Stack;
192 const store = yield* ArtifactStore;
193
194 if (client._tag === "None") {
195 const provider = withDefaultList(yield* eff);
196 return new Proxy(provider, {
197 get: (target, prop) => {
198 const value = (target as any)[prop];
199 if (!Predicate.isFunction(value)) return value;
200 return (...args: any[]) => {
201 const result = value(...args);
202 const services = Layer.mergeAll(
203 Layer.succeedContext(context),
204 layerFallback(Stack, stack),
205 layerFallback(Stage, stack.stage),
206 Predicate.hasProperty(args[0], "instanceId") &&
207 Predicate.isString(args[0].instanceId)
208 ? Layer.merge(
209 layerFallback(InstanceId, args[0].instanceId),
210 Layer.succeed(
211 Artifacts,
212 makeScopedArtifacts(store, args[0].instanceId),
213 ),
214 )
215 : Layer.empty,
216 );
217 return result.pipe(
218 Stream.isStream(result)
219 ? Stream.provide(services)
220 : Effect.provide(services),
221 );
222 };
223 },
224 });
225 }
226 return withDefaultList(yield* client.value.get(providersUrl, cls.Type));
227 }),
228 );
229
230const layerFallback = <I, S>(
231 service: Context.Key<I, S>,
232 defaultValue: NoInfer<S>,
233) =>
234 Layer.effect(
235 service,
236 Effect.serviceOption(service).pipe(
237 Effect.map(Option.getOrElse(() => defaultValue)),
238 ),
239 );
240
241/**
242 * Conditionally constructs a layer for use by an RpcProvider.
243 * If the {@link RpcProviderProxy} is present in context, the layer is empty because it will not be used in this process.
244 * Otherwise, the given layer is returned.
245 * @param self - The layer that is used by the RpcProvider.
246 * @returns A layer that is empty if the {@link RpcProviderProxy} is present in context, otherwise the given layer.
247 */
248export const providerServices = <ROut, E, RIn>(
249 self: Layer.Layer<ROut, E, RIn>,
250): Layer.Layer<ROut, E, RIn | AlchemyContext> =>
251 providerServicesEffect(Effect.succeed(self));
252
253/**
254 * Conditionally constructs a layer for use by an RpcProvider.
255 * If the {@link RpcProviderProxy} is present in context, the layer is empty because it will not be used in this process.
256 * Otherwise, the given layer is returned.
257 *
258 * Note that unlike earlier versions this is no longer gated on
259 * `AlchemyContext.dev`: local providers are registered via
260 * `ProviderLayer.dual` and their dependency layers are composed inside the
261 * *lazily built* local variant, so in a live run these services are only
262 * constructed when the local provider is actually demanded (e.g. deleting a
263 * `providerMode: "local"` state row during `alchemy deploy`).
264 * @param self - An effect which returns a layer that is used by the RpcProvider.
265 */
266export const providerServicesEffect = <A, E1, R1, E, R>(
267 self: Effect.Effect<Layer.Layer<A, E1, R1>, E, R>,
268): Layer.Layer<A, E | E1, R1 | Exclude<R, Scope> | AlchemyContext> =>
269 Effect.serviceOption(RpcProviderProxy).pipe(
270 Effect.flatMap((client) =>
271 client._tag === "None" ? self : (Effect.succeed(Layer.empty) as never),
272 ),
273 Layer.unwrap,
274 );