1/**
2 * Node-side path into the local workerd KV simulator, built on the
3 * runtime's platform proxy (`PlatformProxy.open` — our `getPlatformProxy`).
4 *
5 * For a `dev:` namespace there is no cloud namespace to speak REST to.
6 * Instead, each operation opens a scoped platform proxy hosting the native
7 * KV binding and runs against `proxy.env.KV` — the same
8 * `runtime.KVNamespace` surface the Worker-binding client builders
9 * (`makeReadKVClient` / `makeWriteKVClient`) already consume, so the
10 * `*Local` layers reuse those builders verbatim:
11 *
12 * Node ── proxy.env.KV.get(...) ──▶ kv service ──▶ KVNamespaceObject
13 *
14 * One proxy boot per operation — slow but correct; the `*Local` layers run
15 * in deploy-time Actions, not on a request path. Data lands in the same
16 * `{storage}/kv` directory every local worker binding reads.
17 *
18 * NOT exported from `index.ts` — capability-internal scaffolding.
19 */
20import { KvNamespace } from "@alchemy.run/cloudflare-runtime/core/bindings";
21import { open } from "@alchemy.run/cloudflare-runtime/core/platform-proxy";
22import type * as runtime from "@cloudflare/workers-types";
23import type * as Context from "effect/Context";
24import * as Effect from "effect/Effect";
25import { gatewayName, localGatewayRuntime } from "../LocalGateway.ts";
26import type { makeKVNamespaceHelpers } from "./NamespaceBinding.ts";
27import { NamespaceError } from "./NamespaceTypes.ts";
28
29const tryPromise = <T>(
30 fn: () => Promise<T>,
31): Effect.Effect<T, NamespaceError> =>
32 Effect.tryPromise({
33 try: fn,
34 catch: (error: any) =>
35 new NamespaceError({
36 message: error?.message ?? "Unknown error",
37 cause: error,
38 }),
39 });
40
41/**
42 * Binding-client helpers backed by a per-operation platform proxy instead
43 * of a Worker's `env`. `raw` cannot be satisfied — a native namespace only
44 * lives as long as its proxy's scope — so it dies with guidance.
45 */
46export const makeProxyKVNamespaceHelpers = (
47 namespaceId: string,
48 /**
49 * The FULL ambient stack-eval context: platform services for booting
50 * workerd plus `CloudflareEnvironment`/`AlchemyContext` for the runtime
51 * layer.
52 */
53 ambient: Context.Context<never>,
54): ReturnType<typeof makeKVNamespaceHelpers> => {
55 const use = <T>(
56 fn: (raw: runtime.KVNamespace<string>) => Promise<T>,
57 ): Effect.Effect<T, NamespaceError> =>
58 Effect.scoped(
59 Effect.gen(function* () {
60 const proxy = yield* open({
61 name: gatewayName("alchemy-kv-gateway", namespaceId),
62 bindings: [KvNamespace.local({ binding: "KV", id: namespaceId })],
63 });
64 const kv = (proxy.env as Record<string, unknown>)
65 .KV as runtime.KVNamespace<string>;
66 return yield* tryPromise(() => fn(kv));
67 }),
68 ).pipe(
69 Effect.provide(localGatewayRuntime),
70 // The gateway layer's platform requirements are satisfied by the
71 // ambient stack-eval context; `Context<never>` can't prove that
72 // statically, so erase the leftover R (and the proxy's own infra
73 // error channel) with a cast — mirroring D1's gateway.
74 Effect.provideContext(ambient),
75 ) as Effect.Effect<T, NamespaceError>;
76
77 const raw = Effect.die(
78 new NamespaceError({
79 message:
80 "A locally-emulated KV namespace has no long-lived native binding outside a Worker; use get/put/delete/list/getWithMetadata (each runs through a scoped local gateway).",
81 cause: new Error("unsupported"),
82 }),
83 ) as Effect.Effect<runtime.KVNamespace<string>>;
84
85 return { raw, use, tryPromise };
86};