@deno/kv
Version:
A Deno KV client library optimized for Node.js.
113 lines (112 loc) • 5.71 kB
JavaScript
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { encodeBinary as encodeWatch } from "./proto/messages/com/deno/kv/datapath/Watch.js";
import { encodeBinary as encodeAtomicWrite } from "./proto/messages/com/deno/kv/datapath/AtomicWrite.js";
import { encodeBinary as encodeSnapshotRead } from "./proto/messages/com/deno/kv/datapath/SnapshotRead.js";
import { decodeBinary as decodeSnapshotReadOutput } from "./proto/messages/com/deno/kv/datapath/SnapshotReadOutput.js";
import { decodeBinary as decodeAtomicWriteOutput } from "./proto/messages/com/deno/kv/datapath/AtomicWriteOutput.js";
import { isDateTime, isRecord } from "./check.js";
import { executeWithRetries, RetryableError } from "./sleep.js";
export { decodeAtomicWriteOutput, decodeSnapshotReadOutput, encodeAtomicWrite, encodeSnapshotRead, };
export async function fetchDatabaseMetadata(url, accessToken, fetcher, maxRetries, supportedVersions) {
return await executeWithRetries("fetchDatabaseMetadata", async () => {
const res = await fetcher(url, {
method: "POST",
headers: {
authorization: `Bearer ${accessToken}`,
"content-type": "application/json",
},
body: JSON.stringify({ supportedVersions }),
});
if (res.status !== 200) {
throw new (res.status >= 500 && res.status < 600
? RetryableError
: Error)(`Unexpected response status: ${res.status} ${await res.text()}`);
}
const contentType = res.headers.get("content-type") ?? undefined;
if (contentType !== "application/json") {
throw new Error(`Unexpected response content-type: ${contentType} ${await res.text()}`);
}
const metadata = await res.json();
if (!isDatabaseMetadata(metadata)) {
throw new Error(`Bad DatabaseMetadata: ${JSON.stringify(metadata)}`);
}
return { metadata, responseUrl: res.url };
}, { maxRetries });
}
export async function fetchSnapshotRead(url, accessToken, databaseId, req, fetcher, maxRetries, version) {
return decodeSnapshotReadOutput(await fetchProtobuf(url, accessToken, databaseId, encodeSnapshotRead(req), fetcher, maxRetries, version, false));
}
export async function fetchAtomicWrite(url, accessToken, databaseId, write, fetcher, maxRetries, version) {
return decodeAtomicWriteOutput(await fetchProtobuf(url, accessToken, databaseId, encodeAtomicWrite(write), fetcher, maxRetries, version, false));
}
export async function fetchWatchStream(url, accessToken, databaseId, watch, fetcher, maxRetries, version) {
return await fetchProtobuf(url, accessToken, databaseId, encodeWatch(watch), fetcher, maxRetries, version, true);
}
async function fetchProtobuf(url, accessToken, databaseId, body, fetcher, maxRetries, version, stream) {
const headers = {
authorization: `Bearer ${accessToken}`,
"content-type": "application/x-protobuf",
...(version === 1 ? { "x-transaction-domain-id": databaseId } : {
"x-denokv-version": version.toString(),
"x-denokv-database-id": databaseId,
}),
};
return await executeWithRetries("fetchProtobuf", async () => {
const res = await fetcher(url, { method: "POST", body, headers });
if (res.status !== 200) {
throw new (res.status >= 500 && res.status < 600
? RetryableError
: Error)(`Unexpected response status: ${res.status} ${await res.text()}`);
}
const contentType = res.headers.get("content-type") ?? undefined;
const expectedContentTypes = stream
? ["application/octet-stream", "" /* TODO remove once fixed upstream */]
: [
"application/x-protobuf",
"application/protobuf", /* allow nonspec, was returned by denokv release 0.2.0 */
];
if (!expectedContentTypes.includes(contentType ?? "")) {
throw new Error(`Unexpected response content-type: ${contentType} ${await res.text()}`);
}
if (stream) {
if (res.body === null) {
throw new Error(`No response body for stream request`);
}
return res.body;
}
else {
return new Uint8Array(await res.arrayBuffer());
}
}, { maxRetries });
}
function isValidEndpointUrl(url) {
try {
const { protocol, pathname, search, hash } = new URL(url, "https://example.com");
return /^https?:$/.test(protocol) &&
(pathname === "/" ||
!pathname.endsWith("/") && search === "" && hash === ""); // must not end in "/" (except no path), no qp/hash implied since the spec simply appends "/action"
}
catch {
return false;
}
}
function isEndpointInfo(obj) {
if (!isRecord(obj))
return false;
const { url, consistency, ...rest } = obj;
return typeof url === "string" && isValidEndpointUrl(url) &&
(consistency === "strong" || consistency === "eventual") &&
Object.keys(rest).length === 0;
}
function isDatabaseMetadata(obj) {
if (!isRecord(obj))
return false;
const { version, databaseId, endpoints, token, expiresAt, ...rest } = obj;
return (version === 1 || version === 2 || version === 3) &&
typeof databaseId === "string" &&
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(databaseId) &&
Array.isArray(endpoints) && endpoints.every(isEndpointInfo) &&
typeof token === "string" &&
typeof expiresAt === "string" && isDateTime(expiresAt) &&
Object.keys(rest).length === 0;
}