| 1 | import type * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Option from "effect/Option"; |
| 5 | import * as Predicate from "effect/Predicate"; |
| 6 | import type { Scope } from "effect/Scope"; |
| 7 | import * as Stream from "effect/Stream"; |
| 8 | import { AlchemyContext } from "../AlchemyContext.ts"; |
| 9 | import { Artifacts, ArtifactStore, makeScopedArtifacts } from "../Artifacts.ts"; |
| 10 | import { InstanceId } from "../InstanceId.ts"; |
| 11 | import type { Platform } from "../Platform.ts"; |
| 12 | import * as Provider from "../Provider.ts"; |
| 13 | import type { ResourceClassLike, ResourceLike } from "../Resource.ts"; |
| 14 | import { Stack } from "../Stack.ts"; |
| 15 | import { Stage } from "../Stage.ts"; |
| 16 | import { 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 | */ |
| 58 | type 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 | */ |
| 106 | const 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 | |
| 156 | export 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 | |
| 230 | const 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 | */ |
| 248 | export 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 | */ |
| 266 | export 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 | ); |