@deno/kv
Version:
A Deno KV client library optimized for Node.js.
154 lines (153 loc) • 6.09 kB
JavaScript
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { checkOptionalBoolean, checkOptionalFunction, checkOptionalObject, checkOptionalString, checkRecord, isRecord, } from "./check.js";
import { decodeAtomicWriteOutput, decodeSnapshotReadOutput, encodeAtomicWrite, encodeSnapshotRead, } from "./kv_connect_api.js";
import { packKey } from "./kv_key.js";
import { encodeBinary as encodeWatch } from "./proto/messages/com/deno/kv/datapath/Watch.js";
import { decodeBinary as decodeWatchOutput } from "./proto/messages/com/deno/kv/datapath/WatchOutput.js";
import { ProtoBasedKv, WatchCache } from "./proto_based.js";
import { makeUnrawWatchStream } from "./unraw_watch_stream.js";
/**
* Return a KVService that creates KV instances backed by a native Node NAPI interface.
*/
export function makeNapiBasedService(opts) {
return {
openKv: (v) => Promise.resolve(NapiBasedKv.of(v, opts)),
};
}
export const NAPI_FUNCTIONS = [
"open",
"close",
"snapshotRead",
"atomicWrite",
"dequeueNextMessage",
"finishMessage",
"startWatch",
"dequeueNextWatchMessage",
"endWatch",
];
export function isNapiInterface(obj) {
return isRecord(obj) &&
NAPI_FUNCTIONS.every((v) => typeof obj[v] === "function");
}
//
// deno-lint-ignore no-explicit-any
const DEFAULT_NAPI_INTERFACE = await import('./_napi_index.cjs');
class NapiBasedKv extends ProtoBasedKv {
constructor(debug, napi, dbId, decodeV8, encodeV8) {
super(debug, decodeV8, encodeV8);
Object.defineProperty(this, "napi", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "dbId", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.napi = napi;
this.dbId = dbId;
}
static of(url, opts) {
checkOptionalString("url", url);
checkRecord("opts", opts);
checkOptionalBoolean("opts.debug", opts.debug);
checkOptionalObject("opts.napi", opts.napi);
checkOptionalFunction("opts.decodeV8", opts.decodeV8);
checkOptionalFunction("opts.encodeV8", opts.encodeV8);
checkOptionalBoolean("opts.inMemory", opts.inMemory);
const { debug = false, napi = DEFAULT_NAPI_INTERFACE, decodeV8, encodeV8, inMemory, } = opts;
if (typeof url !== "string" || /^https?:\/\//i.test(url)) {
throw new Error(`Invalid path: ${url}`);
}
if (napi === undefined) {
throw new Error(`No default napi interface, provide one via the 'napi' option.`);
}
const dbId = napi.open(url, inMemory, debug);
return new NapiBasedKv(debug, napi, dbId, decodeV8, encodeV8);
}
async listenQueue_(handler) {
const { napi, dbId, decodeV8, debug } = this;
while (true) {
if (debug)
console.log(`listenQueue_: before dequeueNextMessage`);
const result = await napi.dequeueNextMessage(dbId, debug);
if (result === undefined)
return;
const { bytes, messageId } = result;
const value = decodeV8(bytes);
if (debug)
console.log(`listenQueue_: after value ${value}`);
try {
await Promise.resolve(handler(value));
await napi.finishMessage(dbId, messageId, true, debug);
}
catch (e) {
if (debug)
console.log(`listenQueue_: handler failed ${e.stack || e}`);
await napi.finishMessage(dbId, messageId, false, debug);
}
}
}
close_() {
const { napi, dbId, debug } = this;
napi.close(dbId, debug);
}
async snapshotRead(req, _consistency) {
const { napi, dbId, debug } = this;
const res = await napi.snapshotRead(dbId, encodeSnapshotRead(req), debug);
return decodeSnapshotReadOutput(res);
}
async atomicWrite(req) {
const { napi, dbId, debug } = this;
const res = await napi.atomicWrite(dbId, encodeAtomicWrite(req), debug);
return decodeAtomicWriteOutput(res);
}
watch_(keys, raw) {
const { napi, dbId, debug, decodeV8 } = this;
const { startWatch, dequeueNextWatchMessage, endWatch } = napi;
if (startWatch === undefined || dequeueNextWatchMessage === undefined ||
endWatch === undefined) {
throw new Error("watch: not implemented");
}
const watch = {
keys: keys.map((v) => ({ key: packKey(v) })),
};
let watchId;
let ended = false;
const endWatchIfNecessary = () => {
if (watchId !== undefined && !ended)
endWatch(dbId, watchId, debug);
ended = true;
};
const cache = new WatchCache(decodeV8, keys);
const rawStream = new ReadableStream({
async pull(controller) {
if (watchId === undefined) {
watchId = await startWatch(dbId, encodeWatch(watch), debug);
}
const watchOutputBytes = await dequeueNextWatchMessage(dbId, watchId, debug);
if (watchOutputBytes === undefined) {
endWatchIfNecessary();
controller.close();
return;
}
const watchOutput = decodeWatchOutput(watchOutputBytes);
const { status, keys: outputKeys } = watchOutput;
if (status !== "SR_SUCCESS") {
throw new Error(`Unexpected status: ${status}`);
}
const entries = cache.processOutputKeys(outputKeys);
controller.enqueue(entries);
},
cancel() {
endWatchIfNecessary();
},
});
return raw
? rawStream
: makeUnrawWatchStream(rawStream, endWatchIfNecessary);
}
}