| 1 | /** |
| 2 | * Shared scaffolding for the local-simulator gateways. |
| 3 | * |
| 4 | * The local binding simulators (KV, R2, D1, ...) live inside workerd — only |
| 5 | * a Worker with the native binding can reach them. Deploy-time code (Action |
| 6 | * capability clients, provider reconciles) runs in Node, so the `*Local` |
| 7 | * layers bridge the two with the runtime's platform proxy |
| 8 | * (`PlatformProxy.open` — our `getPlatformProxy`): a scoped workerd |
| 9 | * instance hosts the binding, and Node drives it through proxied clients. |
| 10 | * |
| 11 | * NOT exported from `index.ts` — provider/capability-internal scaffolding. |
| 12 | */ |
| 13 | import { layerRuntime } from "@alchemy.run/cloudflare-runtime/core"; |
| 14 | import * as Effect from "effect/Effect"; |
| 15 | import * as Layer from "effect/Layer"; |
| 16 | import * as Path from "effect/Path"; |
| 17 | import { AlchemyContext } from "../AlchemyContext.ts"; |
| 18 | import { CloudflareEnvironment } from "./CloudflareEnvironment.ts"; |
| 19 | import { isLocalId } from "./LocalRuntime.ts"; |
| 20 | |
| 21 | /** |
| 22 | * A standalone local-runtime layer for gateway consumers OUTSIDE the |
| 23 | * provider stack (e.g. capability `*Local` layers running in an Action, |
| 24 | * whose ambient context has no workerd `Runtime`). Configured identically |
| 25 | * to the providers' shared runtime — same `.alchemy/local` storage |
| 26 | * directory — so it reads and writes the same simulator data. |
| 27 | */ |
| 28 | export const localGatewayRuntime = Layer.unwrap( |
| 29 | Effect.gen(function* () { |
| 30 | const getEnv = yield* CloudflareEnvironment; |
| 31 | const { dotAlchemy } = yield* AlchemyContext; |
| 32 | const path = yield* Path.Path; |
| 33 | return layerRuntime({ |
| 34 | api: { |
| 35 | accountId: getEnv.pipe(Effect.map((env) => env.accountId)), |
| 36 | }, |
| 37 | storage: { |
| 38 | directory: path.join(dotAlchemy, "local"), |
| 39 | }, |
| 40 | }); |
| 41 | }), |
| 42 | ); |
| 43 | |
| 44 | /** Sanitize an id into a workerd instance-name-safe suffix. */ |
| 45 | export const gatewayName = (prefix: string, id: string) => |
| 46 | `${prefix}-${id.replace(/[^a-zA-Z0-9_-]/g, "-")}`; |
| 47 | |
| 48 | /** |
| 49 | * Build a client whose every member resolves the resource id first and then |
| 50 | * delegates: `dev:` ids to the (memoized) native-over-gateway client, real |
| 51 | * ids to the HTTP client. Function members dispatch per call; Effect-valued |
| 52 | * members (`raw`) dispatch on evaluation. Used by the `*Local` capability |
| 53 | * scaffolding (KV, R2) where the id is a deferred accessor resolved at |
| 54 | * apply time. |
| 55 | */ |
| 56 | export const dispatchByMode = <Client extends object>( |
| 57 | resourceId: Effect.Effect<string>, |
| 58 | httpClient: Client, |
| 59 | makeNative: (id: string) => Client, |
| 60 | ): Client => { |
| 61 | let native: Client | undefined; |
| 62 | let nativeId: string | undefined; |
| 63 | const nativeFor = (id: string): Client => { |
| 64 | if (nativeId !== id) { |
| 65 | nativeId = id; |
| 66 | native = makeNative(id); |
| 67 | } |
| 68 | return native!; |
| 69 | }; |
| 70 | const pick = (id: string): Client => |
| 71 | isLocalId(id) ? nativeFor(id) : httpClient; |
| 72 | const out: Record<string, unknown> = {}; |
| 73 | for (const [key, value] of Object.entries(httpClient)) { |
| 74 | out[key] = |
| 75 | typeof value === "function" |
| 76 | ? (...args: unknown[]) => |
| 77 | Effect.flatMap(resourceId, (id) => |
| 78 | (pick(id) as Record<string, any>)[key](...args), |
| 79 | ) |
| 80 | : Effect.flatMap( |
| 81 | resourceId, |
| 82 | (id) => (pick(id) as Record<string, any>)[key], |
| 83 | ); |
| 84 | } |
| 85 | return out as Client; |
| 86 | }; |