1// A KVNamespace for tests, in memory: get, getWithMetadata, put, delete and 2// list (by prefix, in key order, paged by `limit` and `cursor`), with 3// values kept as bytes and each put's options recorded (`puts`), so a test 4// can check what was stored and for how long. Expiry is recorded, not 5// 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 }; 8 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}