import {got} from "@e280/stz" import {Cell} from "./cell.js" import {consts} from "./consts.js" import {Subtree} from "./subtree.js" import {Operator} from "./utils/operator.js" import {Prefixer} from "./utils/prefixer.js" import {MemoryMagazine} from "./magazines/memory.js" import {validateScan} from "./utils/validate-scan.js" import {validateScopes} from "./utils/validate-scopes.js" import {Magazine, Op, Options, Scan, Pair, Value} from "./types.js" export class Kv { /** facility for writing operations that could be committed. */ readonly op /** methods that can access all entries under this scope, including child scopes. */ readonly subtree #magazine #prefixer #preprocess #options: Options constructor( magazine: Magazine = new MemoryMagazine(), options: Partial = {}, ) { this.#magazine = magazine this.#options = { strict: false, scopes: [], ...options, } validateScopes(this.#options.scopes) this.#prefixer = new Prefixer(this.#options) this.op = new Operator(this.#prefixer) this.subtree = new Subtree(this.#magazine, this.#options.scopes) this.#preprocess = this.#options.strict ? (value: Value) => JSON.parse(JSON.stringify(value)) : (value: Value) => value } /** create a cell which can set or get on a single key */ cell(key: string) { return new Cell(this, key) } /** create a sub kv where all keys inherit a prefix */ scope(...scopes: string[]) { return new Kv(this.#magazine, { ...this.#options, scopes: [...this.#options.scopes, ...scopes], }) } async commit(ops: Op[]) { await this.#magazine.commit( ops.map(([key, value]) => [key, ( (value === undefined) ? undefined : this.#preprocess(value) )]) ) } async set(key: string, value: X | undefined) { return this.commit([this.op.set(key, value)]) } async setMany(changes: [key: string, value: X | undefined][]) { return this.commit(changes.map(([key, value]) => this.op.set(key, value))) } async delete(...keys: string[]) { return this.commit(keys.map(key => this.op.delete(key))) } async getMany(keys: string[]) { keys = keys.map(key => this.#prefixer.prefix(key)) return (await this.#magazine.getMany(keys)).map(value => (value === undefined) ? undefined : value as X ) } async get(key: string) { const [value] = await this.getMany([key]) return value } async has(key: string) { return (await this.get(key)) !== undefined } /** throw if the value is undefined or null */ async need(key: string) { return got(await this.get(key), `key not found "${key}"`) } /** throw if the value is undefined or null */ async needMany(keys: string[]) { const values = await this.getMany(keys) for (const [index, key] of keys.entries()) got(values[index], `key not found "${key}"`) return values as X[] } async* entries(scan: Scan = {}) { scan = validateScan(this.#prefixer.scan(scan)) for await (const [key, value] of this.#magazine.entries(scan)) { const key2 = this.#prefixer.unprefix(key) if (value !== undefined) yield [key2, value] as Pair } } [Symbol.asyncIterator]() { return this.entries() } async* keys(scan: Scan = {}) { for await (const [key] of this.entries(scan)) yield key } async* values(scan: Scan = {}) { for await (const [, value] of this.entries(scan)) yield value as X } async count(scan: Scan = {}) { let count = 0 for await (const _ of this.keys(scan)) count++ return count } async clear(scan: Scan = {}) { let changes: Op[] = [] for await (const [key] of this.entries(scan)) { changes.push(this.op.delete(key)) if (changes.length >= consts.chunkSize) { await this.commit(changes) changes = [] } } if (changes.length) await this.commit(changes) } }