1import * as kv from "@distilled.cloud/cloudflare/kv";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import {
5 makeHttpKVNamespaceBinding,
6 makeKVAuth,
7 makeKVHttpScope,
8 toKVNamespaceError,
9 type KVAuth,
10} from "./NamespaceHttp.ts";
11import { NamespaceError } from "./NamespaceTypes.ts";
12import { WriteNamespace, type WriteNamespaceClient } from "./WriteNamespace.ts";
13
14/**
15 * HTTP-backed implementation of the {@link WriteNamespace} binding.
16 *
17 * It creates a scoped {@link AccountApiToken} with the `Workers KV Storage
18 * Write` permission and writes values via the Cloudflare KV HTTP API.
19 */
20export const WriteNamespaceHttp = Layer.effect(
21 WriteNamespace,
22 Effect.suspend(() =>
23 makeHttpKVNamespaceBinding({
24 permissionGroups: ["Workers KV Storage Write"],
25 makeClient: (token, namespaceId) =>
26 makeWriteKVHttpClient(makeKVAuth(token), namespaceId),
27 }),
28 ),
29);
30
31export const makeWriteKVHttpClient = (
32 auth: KVAuth,
33 namespaceId: Effect.Effect<string>,
34): WriteNamespaceClient => {
35 const { authorize } = auth;
36 const scope = makeKVHttpScope(auth, namespaceId);
37
38 return {
39 put: ((
40 key: string,
41 value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
42 options?: KVNamespacePutOptions,
43 ) =>
44 scope.pipe(
45 Effect.flatMap(({ accountId, namespaceId }) =>
46 toKVBody(value).pipe(
47 Effect.flatMap((body) =>
48 authorize(
49 kv.putNamespaceValue({
50 accountId,
51 namespaceId,
52 keyName: key,
53 value: body,
54 expiration: options?.expiration,
55 expirationTtl: options?.expirationTtl,
56 metadata: options?.metadata,
57 }),
58 ),
59 ),
60 ),
61 ),
62 Effect.mapError(toKVNamespaceError),
63 Effect.asVoid,
64 )) as any,
65 delete: ((key: string) =>
66 scope.pipe(
67 Effect.flatMap(({ accountId, namespaceId }) =>
68 authorize(
69 kv.deleteNamespaceValue({ accountId, namespaceId, keyName: key }),
70 ),
71 ),
72 Effect.mapError(toKVNamespaceError),
73 Effect.asVoid,
74 )) as any,
75 };
76};
77
78/** Collect a put value into a body accepted by the KV HTTP API. */
79const toKVBody = (
80 value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
81): Effect.Effect<string | Blob, NamespaceError> =>
82 Effect.gen(function* () {
83 if (typeof value === "string") return value;
84 if (value instanceof Blob) return value;
85 if (value instanceof ArrayBuffer) return new Blob([value]);
86 if (value instanceof ReadableStream) {
87 const buffer = yield* Effect.tryPromise({
88 try: () => new Response(value as any).arrayBuffer(),
89 catch: toKVNamespaceError,
90 });
91 return new Blob([buffer]);
92 }
93 const view = value as ArrayBufferView;
94 const bytes = new Uint8Array(view.byteLength);
95 bytes.set(
96 new Uint8Array(
97 view.buffer as ArrayBuffer,
98 view.byteOffset,
99 view.byteLength,
100 ),
101 );
102 return new Blob([bytes]);
103 });