UNPKG

@deno/kv

Version:

A Deno KV client library optimized for Node.js.

411 lines (410 loc) 13.7 kB
// Copyright 2023 the Deno authors. All rights reserved. MIT license. import { decodeHex, encodeHex } from "./bytes.js"; import { checkExpireIn, checkKeyNotEmpty, checkMatches, isRecord, } from "./check.js"; import { _KvU64 } from "./kv_u64.js"; import { defer } from "./proto/runtime/async/observer.js"; import { decode as decodeBase64, encode as encodeBase64, } from "./proto/runtime/base64.js"; export function unpackKvu(bytes) { if (bytes.length !== 8) throw new Error(); if (bytes.buffer.byteLength !== 8) bytes = new Uint8Array(bytes); const rt = new DataView(bytes.buffer).getBigUint64(0, true); return new _KvU64(rt); } export function packKvu(value) { const rt = new Uint8Array(8); new DataView(rt.buffer).setBigUint64(0, value.value, true); return rt; } export function readValue(bytes, encoding, decodeV8) { if (encoding === "VE_V8") return decodeV8(bytes); if (encoding === "VE_LE64") return unpackKvu(bytes); if (encoding === "VE_BYTES") return bytes; throw new Error(`Unsupported encoding: ${encoding} [${[...bytes].join(", ")}]`); } export function packKvValue(value, encodeV8) { if (value instanceof _KvU64) { return { encoding: "VE_LE64", data: packKvu(value) }; } if (value instanceof Uint8Array) return { encoding: "VE_BYTES", data: value }; return { encoding: "VE_V8", data: encodeV8(value) }; } export function packCursor({ lastYieldedKeyBytes }) { return encodeBase64(JSON.stringify({ lastYieldedKeyBytes: encodeHex(lastYieldedKeyBytes) })); } export function unpackCursor(str) { try { const { lastYieldedKeyBytes } = JSON.parse(new TextDecoder().decode(decodeBase64(str))); if (typeof lastYieldedKeyBytes === "string") { return { lastYieldedKeyBytes: decodeHex(lastYieldedKeyBytes) }; } } catch { // noop } throw new Error(`Invalid cursor`); } export function checkListSelector(selector) { if (!isRecord(selector)) { throw new TypeError(`Bad selector: ${JSON.stringify(selector)}`); } if ("prefix" in selector && "start" in selector && "end" in selector) { throw new TypeError(`Selector can not specify both 'start' and 'end' key when specifying 'prefix'`); } } export function checkListOptions(options) { if (!isRecord(options)) { throw new TypeError(`Bad options: ${JSON.stringify(options)}`); } const { limit, cursor, consistency, batchSize } = options; if (!(limit === undefined || typeof limit === "number" && limit > 0 && Number.isSafeInteger(limit))) throw new TypeError(`Bad 'limit': ${limit}`); if (!(cursor === undefined || typeof cursor === "string")) { throw new TypeError(`Bad 'cursor': ${limit}`); } const reverse = options.reverse === true; // follow native logic if (!(consistency === undefined || consistency === "strong" || consistency === "eventual")) throw new TypeError(`Bad 'consistency': ${consistency}`); if (!(batchSize === undefined || typeof batchSize === "number" && batchSize > 0 && Number.isSafeInteger(batchSize) && batchSize <= 1000)) throw new TypeError(`Bad 'batchSize': ${batchSize}`); return { limit, cursor, reverse, consistency, batchSize }; } export const packVersionstamp = (version) => `${version.toString().padStart(16, "0")}0000`; export const unpackVersionstamp = (versionstamp) => parseInt(checkMatches("versionstamp", versionstamp, /^(\d{16})0000$/)[1]); export const isValidVersionstamp = (versionstamp) => /^(\d{16})0000$/.test(versionstamp); export const replacer = (_this, v) => typeof v === "bigint" ? v.toString() : v; export class CursorHolder { constructor() { Object.defineProperty(this, "cursor", { enumerable: true, configurable: true, writable: true, value: void 0 }); } get() { const { cursor } = this; if (cursor === undefined) { throw new Error(`Cannot get cursor before first iteration`); } return cursor; } set(cursor) { this.cursor = cursor; } } export class GenericKvListIterator { constructor(generator, cursor) { Object.defineProperty(this, "generator", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_cursor", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.generator = generator; this._cursor = cursor; } get cursor() { return this._cursor(); } next() { return this.generator.next(); } [Symbol.asyncIterator]() { return this.generator[Symbol.asyncIterator](); } // deno-lint-ignore no-explicit-any return(value) { return this.generator.return(value); } // deno-lint-ignore no-explicit-any throw(e) { return this.generator.throw(e); } } export class GenericAtomicOperation { constructor(commit) { Object.defineProperty(this, "commitFn", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "checks", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "mutations", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "enqueues", { enumerable: true, configurable: true, writable: true, value: [] }); this.commitFn = commit; } check(...checks) { this.checks.push(...checks); return this; } mutate(...mutations) { mutations.map((v) => v.key).forEach(checkKeyNotEmpty); mutations.forEach((v) => v.type === "set" && checkExpireIn(v.expireIn)); this.mutations.push(...mutations); return this; } sum(key, n) { checkKeyNotEmpty(key); return this.mutate({ type: "sum", key, value: new _KvU64(n) }); } min(key, n) { checkKeyNotEmpty(key); return this.mutate({ type: "min", key, value: new _KvU64(n) }); } max(key, n) { checkKeyNotEmpty(key); return this.mutate({ type: "max", key, value: new _KvU64(n) }); } set(key, value, { expireIn } = {}) { checkExpireIn(expireIn); checkKeyNotEmpty(key); return this.mutate({ type: "set", key, value, expireIn }); } delete(key) { checkKeyNotEmpty(key); return this.mutate({ type: "delete", key }); } enqueue(value, opts) { this.enqueues.push({ value, opts }); return this; } async commit() { return await this.commitFn(this.checks, this.mutations, this.enqueues); } } export class BaseKv { constructor({ debug }) { Object.defineProperty(this, "debug", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "closed", { enumerable: true, configurable: true, writable: true, value: false }); this.debug = debug; } async get(key, { consistency } = {}) { this.checkOpen("get"); checkKeyNotEmpty(key); return await this.get_(key, consistency); } // deno-lint-ignore no-explicit-any async getMany(keys, { consistency } = {}) { this.checkOpen("getMany"); keys.forEach(checkKeyNotEmpty); if (keys.length === 0) return []; return await this.getMany_(keys, consistency); } async set(key, value, { expireIn } = {}) { this.checkOpen("set"); const result = await this.atomic().set(key, value, { expireIn }).commit(); if (!result.ok) throw new Error(`set failed`); // should never happen, there are no checks return result; } async delete(key) { this.checkOpen("delete"); const result = await this.atomic().delete(key).commit(); if (!result.ok) throw new Error(`delete failed`); // should never happen, there are no checks } async enqueue(value, opts) { this.checkOpen("enqueue"); const result = await this.atomic().enqueue(value, opts).commit(); if (!result.ok) throw new Error(`enqueue failed`); // should never happen, there are no checks return result; } list(selector, options = {}) { this.checkOpen("list"); checkListSelector(selector); options = checkListOptions(options); const outCursor = new CursorHolder(); const generator = this.listStream(outCursor, selector, options); return new GenericKvListIterator(generator, () => outCursor.get()); } async listenQueue(handler) { this.checkOpen("listenQueue"); return await this.listenQueue_(handler); } atomic(additionalWork) { return new GenericAtomicOperation(async (checks, mutations, enqueues) => { this.checkOpen("commit"); return await this.commit(checks, mutations, enqueues, additionalWork); }); } // deno-lint-ignore no-explicit-any watch(keys, options) { this.checkOpen("watch"); if (keys.length === 0) throw new Error("Provide at least one key to watch"); keys.forEach(checkKeyNotEmpty); if (!(options === undefined || isRecord(options) && (options.raw === undefined || typeof options.raw === "boolean"))) throw new Error(`Unexpected options: ${JSON.stringify(options)}`); const { raw } = options ?? {}; return this.watch_(keys, raw); } close() { this.checkOpen("close"); this.closed = true; this.close_(); } [Symbol.dispose]() { this.close(); } // checkOpen(method) { if (this.closed) { throw new Error(`Cannot call '.${method}' after '.close' is called`); } } } export class Expirer { constructor(debug, expireFn) { Object.defineProperty(this, "debug", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "expireFn", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "minExpires", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "expirerTimeout", { enumerable: true, configurable: true, writable: true, value: 0 }); this.debug = debug; this.expireFn = expireFn; } init(minExpires) { this.minExpires = minExpires; if (this.minExpires !== undefined) this.rescheduleExpirer(this.minExpires); } rescheduleExpirer(expires) { const { minExpires, debug, expirerTimeout } = this; if (minExpires !== undefined && minExpires < expires) return; this.minExpires = expires; clearTimeout(expirerTimeout); const delay = expires - Date.now(); if (debug) console.log(`rescheduleExpirer: run in ${delay}ms`); this.expirerTimeout = setTimeout(() => this.runExpirer(), delay); } finalize() { clearTimeout(this.expirerTimeout); } // runExpirer() { const { expireFn } = this; const newMinExpires = expireFn(); this.minExpires = newMinExpires; if (newMinExpires !== undefined) { this.rescheduleExpirer(newMinExpires); } else { clearTimeout(this.expirerTimeout); } } } export class QueueWorker { constructor(workerFn) { Object.defineProperty(this, "workerFn", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "workerTimeout", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "queueHandler", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "queueHandlerPromise", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.workerFn = workerFn; } listen(handler) { if (this.queueHandler) throw new Error(`Already called 'listenQueue'`); // for now this.queueHandler = handler; const rt = defer(); this.queueHandlerPromise = rt; this.rescheduleWorker(); return rt; } rescheduleWorker(delay = 0) { clearTimeout(this.workerTimeout); if (this.queueHandler) { this.workerTimeout = setTimeout(() => this.workerFn(this.queueHandler), delay); } } finalize() { clearTimeout(this.workerTimeout); this.queueHandlerPromise?.resolve(); } }