kv.tsannotatedkv.tssource56 lines · 2.5 KB · raw

A KVNamespace for tests, in memory: get, getWithMetadata, put, delete and list (by prefix, in key order, paged by limit and cursor), with values kept as bytes and each put's options recorded (puts), so a test can check what was stored and for how long. Expiry is recorded, not enforced. Add a method when the Worker starts using one.

6type Entry = { value: Uint8Array; metadata: unknown; expirationTtl?: number };
7type Put = { key: string; expirationTtl?: number; metadata?: unknown };
9const bytes = (value: string | ArrayBuffer | ArrayBufferView): Uint8Array =>
10  typeof value === 'string'
11    ? new TextEncoder().encode(value)
12    : value instanceof ArrayBuffer
13      ? new Uint8Array(value.slice(0))
14      : new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
15
16export function memoryKV() {
17  const store = new Map<string, Entry>();
18  const puts: Put[] = [];
19  const read = (entry: Entry | undefined, as: string | { type?: string } = 'text') => {
20    const type = typeof as === 'string' ? as : (as?.type ?? 'text');
21    if (!entry) return null;
22    if (type === 'arrayBuffer') return entry.value.slice().buffer;
23    const text = new TextDecoder().decode(entry.value);
24    return type === 'json' ? JSON.parse(text) : text;
25  };
26  const kv = {
27    store,
28    puts,
29    async get(key: string, type?: string | { type?: string }) {
30      return read(store.get(key), type);
31    },
32    async getWithMetadata(key: string, type?: string | { type?: string }) {
33      const entry = store.get(key);
34      return { value: read(entry, type), metadata: entry?.metadata ?? null };
35    },
36    async put(key: string, value: string | ArrayBuffer | ArrayBufferView, options: Omit<Put, 'key'> = {}) {
37      store.set(key, { value: bytes(value), metadata: options.metadata ?? null, expirationTtl: options.expirationTtl });
38      puts.push({ key, ...options });
39    },
40    async delete(key: string) {
41      store.delete(key);
42    },
43    async list({ prefix = '', limit = 1000, cursor }: { prefix?: string; limit?: number; cursor?: string } = {}) {
44      const names = [...store.keys()].filter((k) => k.startsWith(prefix)).sort();
45      const from = cursor ? Number(cursor) : 0;
46      const page = names.slice(from, from + limit);
47      const done = from + limit >= names.length;
48      return {
49        keys: page.map((name) => ({ name, metadata: store.get(name)!.metadata ?? undefined })),
50        list_complete: done,
51        ...(done ? {} : { cursor: String(from + limit) }),
52      };
53    },
54  };
55  return kv as typeof kv & KVNamespace;
56}