UNPKG

@deno/kv

Version:

A Deno KV client library optimized for Node.js.

113 lines (112 loc) 5.71 kB
// 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; }