1/**
2 * Node-side path into the local workerd R2 simulator, built on the
3 * runtime's platform proxy (`PlatformProxy.open` — our `getPlatformProxy`).
4 *
5 * For a `dev:` bucket there is no cloud bucket to speak REST to. Instead,
6 * each operation opens a scoped platform proxy hosting the native R2
7 * binding and runs against `proxy.env.R2` — the proxy protocol rehydrates
8 * `R2Object`/`R2ObjectBody` on the Node side, matching the surface the
9 * Worker-binding client builders (`makeRead` / `makeWrite`) consume, so the
10 * `*Local` layers reuse those builders verbatim:
11 *
12 * Node ── proxy.env.R2.get(...) ──▶ r2 service ──▶ R2BucketObject
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}/r2` directory every local worker binding reads.
17 *
18 * NOT exported from `index.ts` — capability-internal scaffolding.
19 */
20import { R2Bucket } 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 { makeR2ObjectWrappers, type makeHelpers } from "./BucketBinding.ts";
27import { R2Error } from "./BucketTypes.ts";
28
29const tryPromise = <T>(fn: () => Promise<T>): Effect.Effect<T, R2Error> =>
30 Effect.tryPromise({
31 try: fn,
32 catch: (error: any) =>
33 new R2Error({
34 message: error?.message ?? "Unknown error",
35 cause: error,
36 }),
37 });
38
39/**
40 * Binding-client helpers backed by a per-operation platform proxy instead
41 * of a Worker's `env`. `raw` cannot be satisfied — a native bucket only
42 * lives as long as its proxy's scope — so it dies with guidance.
43 */
44export const makeProxyBucketHelpers = (
45 bucketName: string,
46 /**
47 * The FULL ambient stack-eval context: platform services for booting
48 * workerd plus `CloudflareEnvironment`/`AlchemyContext` for the runtime
49 * layer.
50 */
51 ambient: Context.Context<never>,
52): ReturnType<typeof makeHelpers> => {
53 const use = <T>(
54 fn: (raw: runtime.R2Bucket) => Promise<T>,
55 ): Effect.Effect<T, R2Error> =>
56 Effect.scoped(
57 Effect.gen(function* () {
58 const proxy = yield* open({
59 name: gatewayName("alchemy-r2-gateway", bucketName),
60 bindings: [R2Bucket.local({ binding: "R2", id: bucketName })],
61 });
62 const bucket = (proxy.env as Record<string, unknown>)
63 .R2 as runtime.R2Bucket;
64 return yield* tryPromise(() => fn(bucket));
65 }),
66 ).pipe(
67 Effect.provide(localGatewayRuntime),
68 // The gateway layer's platform requirements are satisfied by the
69 // ambient stack-eval context; `Context<never>` can't prove that
70 // statically, so erase the leftover R (and the proxy's own infra
71 // error channel) with a cast — mirroring D1's gateway.
72 Effect.provideContext(ambient),
73 ) as Effect.Effect<T, R2Error>;
74
75 const raw = Effect.die(
76 new R2Error({
77 message:
78 "A locally-emulated R2 bucket has no long-lived native binding outside a Worker; use put/get/head/list/delete (each runs through a scoped local gateway).",
79 cause: new Error("unsupported"),
80 }),
81 ) as Effect.Effect<runtime.R2Bucket>;
82
83 return { raw, use, tryPromise, ...makeR2ObjectWrappers(tryPromise) };
84};