83 lines · 3.1 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Effect KeyValueStore over a Cloudflare KV namespace — the durable backend// for `executor.cache` on the Cloudflare hosts. The SDK owns the cache seam// (`ExecutorConfig.cache`, with an in-memory fallback); this binding lives// here so the SDK stays platform-agnostic, mirroring blob-store.ts for R2.//// KV is eventually consistent across edge locations — fine for a cache// (consumers must treat entries as best-effort), wrong for anything needing// read-after-write. `size`/`clear` paginate the whole namespace and exist for// interface completeness; production consumers should not call them on a// namespace of any size.// --------------------------------------------------------------------------- import { Effect, Layer } from "effect";import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore";import type { KVNamespace } from "@cloudflare/workers-types"; const KV_DELETE_CONCURRENCY = 50; const storeError = (method: string, key: string | undefined) => (cause: unknown) =>  new KeyValueStore.KeyValueStoreError({    method,    ...(key !== undefined ? { key } : {}),    message: `Cloudflare KV ${method} failed`,    cause,  }); const listAllKeys = async (kv: KVNamespace): Promise<readonly string[]> => {  const keys: string[] = [];  let cursor: string | undefined;  do {    const page = await kv.list(cursor === undefined ? {} : { cursor });    keys.push(...page.keys.map((key) => key.name));    cursor = page.list_complete ? undefined : page.cursor;  } while (cursor !== undefined);  return keys;}; const deleteAllKeys = async (kv: KVNamespace, keys: readonly string[]): Promise<void> => {  for (let index = 0; index < keys.length; index += KV_DELETE_CONCURRENCY) {    await Promise.all(      keys.slice(index, index + KV_DELETE_CONCURRENCY).map((key) => kv.delete(key)),    );  }}; export const makeCloudflareKeyValueStore = (kv: KVNamespace): KeyValueStore.KeyValueStore =>  KeyValueStore.makeStringOnly({    get: (key) =>      Effect.tryPromise({        try: async () => (await kv.get(key)) ?? undefined,        catch: storeError("get", key),      }),    set: (key, value) =>      Effect.tryPromise({        try: () => kv.put(key, value),        catch: storeError("set", key),      }),    remove: (key) =>      Effect.tryPromise({        try: () => kv.delete(key),        catch: storeError("remove", key),      }),    clear: Effect.tryPromise({      try: async (signal) => {        void signal;        await deleteAllKeys(kv, await listAllKeys(kv));      },      catch: storeError("clear", undefined),    }),    size: Effect.tryPromise({      try: async () => (await listAllKeys(kv)).length,      catch: storeError("size", undefined),    }),  }); /** Boot-layer form: provide the KV namespace as the ambient *  `KeyValueStore.KeyValueStore` service `makeScopedExecutor` reads *  optionally and threads into `createExecutor({ cache })`. */export const layerCloudflareKeyValueStore = (  kv: KVNamespace,): Layer.Layer<KeyValueStore.KeyValueStore> =>  Layer.succeed(KeyValueStore.KeyValueStore, makeCloudflareKeyValueStore(kv));