@deno/kv
Version:
A Deno KV client library optimized for Node.js.
255 lines (254 loc) • 10.3 kB
JavaScript
;
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.WatchCache = exports.ProtoBasedKv = exports.UnknownV8 = void 0;
const bytes_js_1 = require("./bytes.js");
const kv_key_js_1 = require("./kv_key.js");
const kv_util_js_1 = require("./kv_util.js");
var v8_js_1 = require("./v8.js");
Object.defineProperty(exports, "UnknownV8", { enumerable: true, get: function () { return v8_js_1.UnknownV8; } });
class ProtoBasedKv extends kv_util_js_1.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 = (0, kv_key_js_1.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 ((0, bytes_js_1.equalBytes)(item.key, packedKey)) {
return {
key,
value: (0, kv_util_js_1.readValue)(item.value, item.encoding, decodeV8),
versionstamp: (0, bytes_js_1.encodeHex)(item.versionstamp),
};
}
}
}
return { key, value: null, versionstamp: null };
}
async getMany_(keys, consistency) {
const { decodeV8 } = this;
const packedKeys = keys.map(kv_key_js_1.packKey);
const packedKeysHex = packedKeys.map(bytes_js_1.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((0, bytes_js_1.encodeHex)(key), {
key: (0, kv_key_js_1.unpackKey)(key),
value: (0, kv_util_js_1.readValue)(value, encoding, decodeV8),
versionstamp: (0, bytes_js_1.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: (0, bytes_js_1.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"
? (0, kv_util_js_1.unpackCursor)(cursorOpt)
: undefined;
let lastYieldedKeyBytes = cursor?.lastYieldedKeyBytes;
let pass = 0;
const prefixBytes = "prefix" in selector
? (0, kv_key_js_1.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 ? (0, kv_key_js_1.packKey)(selector.start) : prefixBytes;
end = "end" in selector
? (0, kv_key_js_1.packKey)(selector.end)
: new Uint8Array([...prefixBytes, 0xff]);
}
else {
start = (0, kv_key_js_1.packKey)(selector.start);
end = (0, kv_key_js_1.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 &&
(0, bytes_js_1.equalBytes)(lastYieldedKeyBytes, entry.key) ||
prefixBytes && (0, bytes_js_1.equalBytes)(prefixBytes, entry.key)))
continue;
const key = (0, kv_key_js_1.unpackKey)(entry.key);
const value = (0, kv_util_js_1.readValue)(entry.value, entry.encoding, decodeV8);
const versionstamp = (0, bytes_js_1.encodeHex)(entry.versionstamp);
lastYieldedKeyBytes = entry.key;
outCursor.set((0, kv_util_js_1.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;
}
}
}
exports.ProtoBasedKv = ProtoBasedKv;
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 = (0, kv_util_js_1.readValue)(bytes, encoding, decodeV8);
const versionstamp = (0, bytes_js_1.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 };
});
}
}
exports.WatchCache = WatchCache;
function computeReadRangeForKey(packedKey) {
return {
start: packedKey,
end: new Uint8Array([0xff]),
limit: 1,
reverse: false,
};
}
function computeKvCheckMessage({ key, versionstamp }) {
return {
key: (0, kv_key_js_1.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
: (0, bytes_js_1.decodeHex)(versionstamp),
};
}
function computeKvMutationMessage(mut, encodeV8) {
const { key, type } = mut;
return {
key: (0, kv_key_js_1.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 : (0, kv_util_js_1.packKvValue)(mut.value, encodeV8),
expireAtMs: mut.type === "set" && typeof mut.expireIn === "number"
? (Date.now() + mut.expireIn).toString()
: "0",
};
}
function computeEnqueueMessage(value, encodeV8, { delay = 0, keysIfUndelivered = [] } = {}) {
return {
backoffSchedule: [100, 200, 400, 800],
deadlineMs: `${Date.now() + delay}`,
keysIfUndelivered: keysIfUndelivered.map(kv_key_js_1.packKey),
payload: encodeV8(value),
};
}