UNPKG

@deno/kv

Version:

A Deno KV client library optimized for Node.js.

252 lines (251 loc) 9.66 kB
// Copyright 2023 the Deno authors. All rights reserved. MIT license. import { decodeHex, encodeHex, equalBytes } from "./bytes.js"; import { packKey, unpackKey } from "./kv_key.js"; import { BaseKv, packCursor, packKvValue, readValue, unpackCursor, } from "./kv_util.js"; export { UnknownV8 } from "./v8.js"; export class ProtoBasedKv extends BaseKv { constructor(debug, decodeV8, encodeV8) { super({ debug }); Object.defineProperty(this, "decodeV8", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "encodeV8", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.decodeV8 = decodeV8; this.encodeV8 = encodeV8; } async get_(key, consistency) { const { decodeV8 } = this; const packedKey = packKey(key); const req = { ranges: [computeReadRangeForKey(packedKey)], }; const res = await this.snapshotRead(req, consistency); for (const range of res.ranges) { for (const item of range.values) { if (equalBytes(item.key, packedKey)) { return { key, value: readValue(item.value, item.encoding, decodeV8), versionstamp: encodeHex(item.versionstamp), }; } } } return { key, value: null, versionstamp: null }; } async getMany_(keys, consistency) { const { decodeV8 } = this; const packedKeys = keys.map(packKey); const packedKeysHex = packedKeys.map(encodeHex); const req = { ranges: packedKeys.map(computeReadRangeForKey), }; const res = await this.snapshotRead(req, consistency); const rowMap = new Map(); for (const range of res.ranges) { for (const { key, value, encoding, versionstamp } of range.values) { rowMap.set(encodeHex(key), { key: unpackKey(key), value: readValue(value, encoding, decodeV8), versionstamp: encodeHex(versionstamp), }); } } return keys.map((key, i) => { const row = rowMap.get(packedKeysHex[i]); return row ? { key, value: row.value, versionstamp: row.versionstamp } : { key, value: null, versionstamp: null }; }); } async commit(checks, mutations, enqueues) { const write = { checks: checks.map(computeKvCheckMessage), mutations: mutations.map((v) => computeKvMutationMessage(v, this.encodeV8)), enqueues: enqueues.map(({ value, opts }) => computeEnqueueMessage(value, this.encodeV8, opts)), }; const { status, versionstamp } = await this.atomicWrite(write); if (status === "AW_CHECK_FAILURE") return { ok: false }; if (status !== "AW_SUCCESS") { throw new Error(`commit failed with status: ${status}`); } return { ok: true, versionstamp: encodeHex(versionstamp) }; } async *listStream(outCursor, selector, { batchSize, consistency, cursor: cursorOpt, limit, reverse = false } = {}) { const { decodeV8 } = this; let yielded = 0; if (typeof limit === "number" && yielded >= limit) return; const cursor = typeof cursorOpt === "string" ? unpackCursor(cursorOpt) : undefined; let lastYieldedKeyBytes = cursor?.lastYieldedKeyBytes; let pass = 0; const prefixBytes = "prefix" in selector ? packKey(selector.prefix) : undefined; while (true) { pass++; // console.log({ pass }); const req = { ranges: [] }; let start; let end; if ("prefix" in selector) { start = "start" in selector ? packKey(selector.start) : prefixBytes; end = "end" in selector ? packKey(selector.end) : new Uint8Array([...prefixBytes, 0xff]); } else { start = packKey(selector.start); end = packKey(selector.end); } if (reverse) { end = lastYieldedKeyBytes ?? end; } else { start = lastYieldedKeyBytes ?? start; } if (start === undefined || end === undefined) throw new Error(); const batchLimit = Math.min(batchSize ?? 100, 500, limit ?? Number.MAX_SAFE_INTEGER) + (lastYieldedKeyBytes ? 1 : 0); req.ranges.push({ start, end, limit: batchLimit, reverse }); const res = await this.snapshotRead(req, consistency); let entries = 0; for (const range of res.ranges) { for (const entry of range.values) { if (entries++ === 0 && (lastYieldedKeyBytes && equalBytes(lastYieldedKeyBytes, entry.key) || prefixBytes && equalBytes(prefixBytes, entry.key))) continue; const key = unpackKey(entry.key); const value = readValue(entry.value, entry.encoding, decodeV8); const versionstamp = encodeHex(entry.versionstamp); lastYieldedKeyBytes = entry.key; outCursor.set(packCursor({ lastYieldedKeyBytes })); // cursor needs to be set before yield yield { key, value, versionstamp }; yielded++; // console.log({ yielded, entries, limit }); if (typeof limit === "number" && yielded >= limit) return; } } if (entries < batchLimit) return; } } } export class WatchCache { constructor(decodeV8, keys) { Object.defineProperty(this, "decodeV8", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "keys", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "lastValues", { enumerable: true, configurable: true, writable: true, value: [] }); this.decodeV8 = decodeV8; this.keys = keys; } processOutputKeys(outputKeys) { const { lastValues, decodeV8, keys } = this; const initial = lastValues.length === 0; outputKeys.forEach((v, i) => { const { changed, entryIfChanged } = v; if (initial && !changed) { throw new Error(`watch: Expect all values in first message: ${JSON.stringify(outputKeys)}`); } if (!changed) return; if (entryIfChanged) { const { value: bytes, encoding } = entryIfChanged; const value = readValue(bytes, encoding, decodeV8); const versionstamp = encodeHex(entryIfChanged.versionstamp); lastValues[i] = [value, versionstamp]; } else { lastValues[i] = undefined; // deleted } }); return keys.map((key, i) => { const lastValue = lastValues[i]; if (lastValue === undefined) { return { key, value: null, versionstamp: null }; } const [value, versionstamp] = lastValue; return { key, value, versionstamp }; }); } } function computeReadRangeForKey(packedKey) { return { start: packedKey, end: new Uint8Array([0xff]), limit: 1, reverse: false, }; } function computeKvCheckMessage({ key, versionstamp }) { return { key: packKey(key), versionstamp: (versionstamp === null || versionstamp === undefined) /* in proto3 all fields are optional, but the generated types don't * like it, so we have to cast it manually */ ? undefined : decodeHex(versionstamp), }; } function computeKvMutationMessage(mut, encodeV8) { const { key, type } = mut; return { key: packKey(key), mutationType: type === "delete" ? "M_DELETE" : type === "max" ? "M_MAX" : type === "min" ? "M_MIN" : type == "set" ? "M_SET" : type === "sum" ? "M_SUM" : "M_UNSPECIFIED", value: mut.type === "delete" ? undefined : packKvValue(mut.value, encodeV8), expireAtMs: mut.type === "set" && typeof mut.expireIn === "number" ? (Date.now() + mut.expireIn).toString() : "0", sumMin: new Uint8Array(), sumMax: new Uint8Array(), sumClamp: false, }; } function computeEnqueueMessage(value, encodeV8, { delay = 0, keysIfUndelivered = [] } = {}) { return { backoffSchedule: [100, 200, 400, 800], deadlineMs: `${Date.now() + delay}`, keysIfUndelivered: keysIfUndelivered.map(packKey), payload: encodeV8(value), }; }