@deno/kv
Version:
A Deno KV client library optimized for Node.js.
252 lines (251 loc) • 9.66 kB
JavaScript
// 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),
};
}