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 */
13import { layerRuntime } from "@alchemy.run/cloudflare-runtime/core";
14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";
16import * as Path from "effect/Path";
17import { AlchemyContext } from "../AlchemyContext.ts";
18import { CloudflareEnvironment } from "./CloudflareEnvironment.ts";
19import { 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 */
28export 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. */
45export 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 */
56export 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};