UNPKG

edge-mock

Version:

types for testing an developer edge applications

176 lines (154 loc) 5.11 kB
// https://developers.cloudflare.com/workers/runtime-apis/kv // TODO expiration import fs from 'fs' import path from 'path' import {encode, decode, escape_regex, rsToArrayBufferView, rsFromArray} from './utils' type InputValueValue = string | ArrayBuffer | ReadableStream | Buffer interface InputObject { value: InputValueValue metadata?: Record<string, string> expiration?: number } type InputValue = InputValueValue | InputObject interface InternalValue { value: ArrayBuffer metadata?: unknown expiration?: number } interface OutputValue { value: any metadata: unknown | null } interface ListValue { name: string expiration?: number metadata?: unknown } type ValueTypeNames = 'text' | 'json' | 'arrayBuffer' | 'stream' export class EdgeKVNamespace implements KVNamespace { protected kv: Map<string, InternalValue> constructor() { this.kv = new Map() } async get(key: string, options?: {type?: ValueTypeNames; cacheTtl?: number} | ValueTypeNames): Promise<any> { options = options || {} if (typeof options == 'string') { options = {type: options} } const v = await this.getWithMetadata(key, options.type) return v.value || null } async getWithMetadata(key: string, type?: ValueTypeNames): Promise<OutputValue> { const v = this.kv.get(key) if (v == undefined) { return {value: null, metadata: null} } return {value: prepare_value(v.value, type), metadata: v.metadata || {}} } async put(key: string, value: InputValueValue, {metadata}: {metadata?: Record<string, string>} = {}): Promise<void> { let _value: ArrayBuffer if (typeof value == 'string') { _value = encode(value).buffer } else if (Buffer.isBuffer(value)) { _value = value.buffer } else if ('getReader' in value) { const view = await rsToArrayBufferView(value) _value = view.buffer } else { _value = value } this.kv.set(key, {value: _value, metadata}) } async delete(key: string): Promise<void> { this.kv.delete(key) } async list(options?: {prefix?: string; limit?: number; cursor?: string}): Promise<{ keys: ListValue[] list_complete: boolean cursor?: string }> { options = options || {} if (options.cursor) { throw new Error('list cursors not yet implemented') } const prefix = options.prefix const limit = options.limit || 1000 const keys: ListValue[] = [] for (const [name, value] of this.kv) { if (!prefix || name.startsWith(prefix)) { if (keys.length == limit) { return {keys, list_complete: false, cursor: 'not-fully-implemented'} } // const {expiration, metadata} = value const {metadata} = value const list_value: ListValue = {name} // if (expiration != undefined) { // list_value.expiration = expiration // } if (metadata != undefined) { list_value.metadata = metadata } keys.push(list_value) } } return {keys, list_complete: true} } async _add_files(directory: string, prepare_key?: (file_name: string) => string): Promise<number> { this._clear() if (!prepare_key) { const clean_dir = directory.replace(/\/+$/, '') const replace_prefix = new RegExp(`^${escape_regex(clean_dir)}\\/`) prepare_key = (file_name: string) => file_name.replace(replace_prefix, '') } return await this._add_directory(directory, prepare_key) } protected async _add_directory(directory: string, prepare_key: (file_name: string) => string): Promise<number> { if (!(await fs.promises.stat(directory)).isDirectory()) { throw new Error(`"${directory}" is not a directory`) } const files = await fs.promises.readdir(directory) let count = 0 for (const file of files) { const file_path = path.join(directory, file) const stat = await fs.promises.stat(file_path) if (stat.isFile()) { const content = await fs.promises.readFile(file_path) await this.put(prepare_key(file_path), content) count += 1 } else if (stat.isDirectory()) { count += await this._add_directory(file_path, prepare_key) } } return count } _manifestJson(): string { const manifest = Object.fromEntries([...this.kv.keys()].map(k => [k, k])) return JSON.stringify(manifest) } _clear() { this.kv.clear() } async _putMany(kv: Record<string, InputValue>): Promise<void> { const promises: Promise<void>[] = [] for (const [k, v] of Object.entries(kv)) { if (typeof v != 'string' && 'value' in v) { promises.push(this.put(k, v.value, {metadata: v.metadata})) } else { promises.push(this.put(k, v, undefined)) } } await Promise.all(promises) } } function prepare_value(v: ArrayBuffer, type: ValueTypeNames | undefined): any { switch (type) { case 'arrayBuffer': return v case 'json': return JSON.parse(decode(v)) case 'stream': return rsFromArray([new Uint8Array(v)]) default: return decode(v) } }