edge-mock
Version:
types for testing an developer edge applications
176 lines (154 loc) • 5.11 kB
text/typescript
// 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)
}
}