arangojs
Version:
The official ArangoDB JavaScript driver.
760 lines • 27 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Connection = exports.isArangoConnection = exports.isArangoErrorResponse = exports.getStatusMessage = void 0;
const configuration = __importStar(require("./configuration.js"));
const errors = __importStar(require("./errors.js"));
const codes_js_1 = require("./lib/codes.js");
const util = __importStar(require("./lib/util.js"));
const x3_linkedlist_js_1 = require("./lib/x3-linkedlist.js");
const MIME_JSON = /\/(json|javascript)(\W|$)/;
const LEADER_ENDPOINT_HEADER = "x-arango-endpoint";
const REASON_TIMEOUT = "timeout";
/**
* @internal
*
* Create a function for performing fetch requests against a given host.
*
* @param arangojsHostUrl - Base URL of the host, i.e. protocol, port and domain name.
* @param options - Options to use for all fetch requests.
*/
function createHost(arangojsHostUrl, agentOptions) {
const baseUrl = new URL(arangojsHostUrl);
let fetch = globalThis.fetch;
let createDispatcher;
let dispatcher;
let socketPath;
if (arangojsHostUrl.match(/^\w+:\/\/unix:\//)) {
socketPath = baseUrl.pathname;
baseUrl.hostname = "localhost";
baseUrl.pathname = "/";
agentOptions = {
...agentOptions,
connect: {
...agentOptions?.connect,
socketPath,
},
};
}
if (agentOptions) {
createDispatcher = async () => {
let undici;
try {
// Prevent overzealous bundlers from attempting to bundle undici
const undiciName = "undici";
undici = await Promise.resolve(`${undiciName}`).then(s => __importStar(require(s)));
}
catch (cause) {
if (socketPath) {
throw new Error("Undici is required for Unix domain sockets", {
cause,
});
}
throw new Error("Undici is required when using config.agentOptions", {
cause,
});
}
fetch = undici.fetch;
return new undici.Agent(agentOptions);
};
}
const pending = new Map();
return {
async fetch({ method, pathname, search, headers: requestHeaders, body, timeout, fetchOptions, beforeRequest, afterResponse, }) {
const url = new URL(pathname + baseUrl.search, baseUrl);
if (search) {
const searchParams = search instanceof URLSearchParams
? search
: new URLSearchParams(search);
for (const [key, value] of searchParams) {
url.searchParams.append(key, value);
}
}
const headers = new Headers(requestHeaders);
if (!headers.has("authorization")) {
headers.set("authorization", `Basic ${btoa(`${baseUrl.username || "root"}:${baseUrl.password || ""}`)}`);
}
const abortController = new AbortController();
const signal = abortController.signal;
if (createDispatcher) {
dispatcher = await createDispatcher();
createDispatcher = undefined;
}
const request = new Request(url, {
...fetchOptions,
dispatcher,
method,
headers,
body,
signal,
});
if (beforeRequest) {
const p = beforeRequest(request);
if (p instanceof Promise)
await p;
}
const requestId = util.generateRequestId();
pending.set(requestId, abortController);
let clearTimer;
if (timeout) {
clearTimer = util.createTimer(timeout, () => {
clearTimer = undefined;
abortController.abort(REASON_TIMEOUT);
});
}
let response;
try {
response = Object.assign(await fetch(request), {
request,
arangojsHostUrl,
});
if (fetchOptions?.redirect === "manual" && isRedirect(response)) {
throw new errors.HttpError(response);
}
}
catch (e) {
const cause = e instanceof Error ? e : new Error(String(e));
let error;
if (cause instanceof errors.NetworkError) {
error = cause;
}
else if (signal.aborted) {
const reason = typeof signal.reason == "string" ? signal.reason : undefined;
if (reason === REASON_TIMEOUT) {
error = new errors.ResponseTimeoutError(undefined, request, {
cause,
});
}
else {
error = new errors.RequestAbortedError(reason, request, { cause });
}
}
else if (cause instanceof TypeError) {
error = new errors.FetchFailedError(undefined, request, { cause });
}
else {
error = new errors.NetworkError(cause.message, request, { cause });
}
if (afterResponse) {
const p = afterResponse(error);
if (p instanceof Promise)
await p;
}
throw error;
}
finally {
clearTimer?.();
pending.delete(requestId);
}
if (afterResponse) {
const p = afterResponse(null, response);
if (p instanceof Promise)
await p;
}
return response;
},
close() {
if (!pending.size)
return;
const controllers = [...pending.values()];
pending.clear();
for (const controller of controllers) {
try {
controller.abort();
}
catch (e) {
// noop
}
}
},
};
}
//#endregion
//#region Response types
const STATUS_CODE_DEFAULT_MESSAGES = {
0: "Network Error",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
444: "Connection Closed Without Response",
451: "Unavailable For Legal Reasons",
499: "Client Closed Request",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
599: "Network Connect Timeout Error",
};
const KNOWN_STATUS_CODES = Object.keys(STATUS_CODE_DEFAULT_MESSAGES).map((k) => Number(k));
const REDIRECT_CODES = [301, 302, 303, 307, 308];
/**
* @internal
*
* Indicates whether the given status code can be translated to a known status
* message.
*/
function isKnownStatusCode(code) {
return KNOWN_STATUS_CODES.includes(code);
}
/**
* @internal
*
* Indicates whether the given status code represents a redirect.
*/
function isRedirect(response) {
return REDIRECT_CODES.includes(response.status);
}
/**
* Returns the status message for the given response's status code or the
* status text of the response.
*/
function getStatusMessage(response) {
if (isKnownStatusCode(response.status)) {
return STATUS_CODE_DEFAULT_MESSAGES[response.status];
}
if (response.statusText)
return response.statusText;
return "Unknown response status";
}
exports.getStatusMessage = getStatusMessage;
/**
* Indicates whether the given value represents an ArangoDB error response.
*/
function isArangoErrorResponse(body) {
if (!body || typeof body !== "object")
return false;
const obj = body;
return (obj.error === true &&
typeof obj.errorMessage === "string" &&
typeof obj.errorNum === "number" &&
(obj.code === undefined || typeof obj.code === "number"));
}
exports.isArangoErrorResponse = isArangoErrorResponse;
/**
* Indicates whether the given value represents a {@link Connection}.
*
* @param connection - A value that might be a connection.
*
* @internal
*/
function isArangoConnection(connection) {
return Boolean(connection && connection.isArangoConnection);
}
exports.isArangoConnection = isArangoConnection;
/**
* Represents a connection pool shared by one or more databases.
*
* @internal
*/
class Connection {
_activeTasks = 0;
_arangoVersion;
_loadBalancingStrategy;
_taskPoolSize;
_commonRequestOptions;
_commonFetchOptions;
_queue = new x3_linkedlist_js_1.LinkedList();
_databases = new Map();
_hosts = [];
_hostUrls = [];
_activeHostUrl;
_activeDirtyHostUrl;
_transactionId = null;
_onError;
_precaptureStackTraces;
_queueTimes = new x3_linkedlist_js_1.LinkedList();
_responseQueueTimeSamples;
/**
* @internal
*
* Creates a new `Connection` instance.
*
* @param config - An object with configuration options.
*
*/
constructor(config = {}) {
const { url = "http://127.0.0.1:8529", auth, arangoVersion = 31100, loadBalancingStrategy = "NONE", maxRetries = 0, poolSize = 3 *
(loadBalancingStrategy === "ROUND_ROBIN" && Array.isArray(url)
? url.length
: 1), fetchOptions: { headers, ...commonFetchOptions } = {}, onError, precaptureStackTraces = false, responseQueueTimeSamples = 10, ...commonRequestOptions } = config;
const URLS = Array.isArray(url) ? url : [url];
this._loadBalancingStrategy = loadBalancingStrategy;
this._precaptureStackTraces = precaptureStackTraces;
this._responseQueueTimeSamples =
responseQueueTimeSamples < 0 ? Infinity : responseQueueTimeSamples;
this._arangoVersion = arangoVersion;
this._taskPoolSize = poolSize;
this._onError = onError;
this._commonRequestOptions = commonRequestOptions;
this._commonFetchOptions = {
headers: new Headers(headers),
...commonFetchOptions,
};
this._commonFetchOptions.headers.set("x-arango-version", String(arangoVersion));
this._commonFetchOptions.headers.set("x-arango-driver", `arangojs/10.1.1 (cloud)`);
this.addToHostList(URLS);
if (auth) {
if (configuration.isBearerAuth(auth)) {
this.setBearerAuth(auth);
}
else {
this.setBasicAuth(auth);
}
}
if (this._loadBalancingStrategy === "ONE_RANDOM") {
this._activeHostUrl =
this._hostUrls[Math.floor(Math.random() * this._hostUrls.length)];
this._activeDirtyHostUrl =
this._hostUrls[Math.floor(Math.random() * this._hostUrls.length)];
}
else {
this._activeHostUrl = this._hostUrls[0];
this._activeDirtyHostUrl = this._hostUrls[0];
}
}
/**
* @internal
*
* Indicates that this object represents an ArangoDB connection.
*/
get isArangoConnection() {
return true;
}
get queueTime() {
return {
getLatest: () => this._queueTimes.last?.value[1],
getValues: () => Array.from(this._queueTimes.values()),
getAvg: () => {
let avg = 0;
for (const [, [, value]] of this._queueTimes) {
avg += value / this._queueTimes.length;
}
return avg;
},
};
}
async _runQueue() {
if (this._activeTasks >= this._taskPoolSize)
return;
const task = this._queue.shift();
if (!task)
return;
let hostUrl = this._activeHostUrl;
try {
this._activeTasks += 1;
if (task.options.hostUrl !== undefined) {
hostUrl = task.options.hostUrl;
}
else if (task.options.allowDirtyRead) {
hostUrl = this._activeDirtyHostUrl;
const i = this._hostUrls.indexOf(this._activeDirtyHostUrl) + 1;
this._activeDirtyHostUrl = this._hostUrls[i % this._hostUrls.length];
}
else if (this._loadBalancingStrategy === "ROUND_ROBIN") {
const i = this._hostUrls.indexOf(this._activeHostUrl) + 1;
this._activeHostUrl = this._hostUrls[i % this._hostUrls.length];
}
const host = this._hosts[this._hostUrls.indexOf(hostUrl)];
const res = Object.assign(await host.fetch(task.options), {
arangojsHostUrl: hostUrl,
});
const leaderEndpoint = res.headers.get(LEADER_ENDPOINT_HEADER);
if (res.status === 503 && leaderEndpoint) {
const [cleanUrl] = this.addToHostList(leaderEndpoint);
task.options.hostUrl = cleanUrl;
if (this._activeHostUrl === hostUrl) {
this._activeHostUrl = cleanUrl;
}
this._queue.push(task);
return;
}
const queueTime = res.headers.get("x-arango-queue-time-seconds");
if (queueTime) {
this._queueTimes.push([Date.now(), Number(queueTime)]);
while (this._responseQueueTimeSamples < this._queueTimes.length) {
this._queueTimes.shift();
}
}
const contentType = res.headers.get("content-type");
if (res.status >= 400) {
if (contentType?.match(MIME_JSON)) {
const errorResponse = res.clone();
let errorBody;
try {
errorBody = await errorResponse.json();
}
catch {
// noop
}
if (isArangoErrorResponse(errorBody)) {
res.parsedBody = errorBody;
throw errors.ArangoError.from(res);
}
}
throw new errors.HttpError(res);
}
if (res.body) {
if (task.options.expectBinary) {
res.parsedBody = await res.blob();
}
else if (contentType?.match(MIME_JSON)) {
res.parsedBody = await res.json();
}
else {
res.parsedBody = await res.text();
}
}
let result = res;
if (task.transform)
result = task.transform(res);
task.resolve(result);
}
catch (e) {
const err = e;
if (!task.options.allowDirtyRead &&
this._hosts.length > 1 &&
this._activeHostUrl === hostUrl &&
this._loadBalancingStrategy !== "ROUND_ROBIN") {
const i = this._hostUrls.indexOf(this._activeHostUrl) + 1;
this._activeHostUrl = this._hostUrls[i % this._hostUrls.length];
}
if (errors.isArangoError(err) &&
err.errorNum === codes_js_1.ERROR_ARANGO_CONFLICT &&
task.options.retryOnConflict &&
task.conflicts < task.options.retryOnConflict) {
task.conflicts += 1;
this._queue.push(task);
return;
}
if ((errors.isNetworkError(err) || errors.isArangoError(err)) &&
err.isSafeToRetry &&
task.options.hostUrl === undefined &&
this._commonRequestOptions.maxRetries !== false &&
task.retries <
(this._commonRequestOptions.maxRetries || this._hosts.length - 1)) {
task.retries += 1;
this._queue.push(task);
return;
}
if (task.stack) {
err.stack += task.stack();
}
if (this._onError) {
try {
const p = this._onError(err);
if (p instanceof Promise)
await p;
}
catch (e) {
e.cause = err;
task.reject(e);
return;
}
}
task.reject(err);
}
finally {
this._activeTasks -= 1;
setTimeout(() => this._runQueue(), 0);
}
}
setBearerAuth(auth) {
this.setHeader("authorization", `Bearer ${auth.token}`);
}
setBasicAuth(auth) {
this.setHeader("authorization", `Basic ${btoa(`${auth.username}:${auth.password}`)}`);
}
setResponseQueueTimeSamples(responseQueueTimeSamples) {
if (responseQueueTimeSamples < 0) {
responseQueueTimeSamples = Infinity;
}
this._responseQueueTimeSamples = responseQueueTimeSamples;
while (this._responseQueueTimeSamples < this._queueTimes.length) {
this._queueTimes.shift();
}
}
database(databaseName, database) {
if (database === null) {
this._databases.delete(databaseName);
return undefined;
}
if (!database) {
return this._databases.get(databaseName);
}
this._databases.set(databaseName, database);
return database;
}
/**
* @internal
*
* Replaces the host list with the given URLs.
*
* See {@link Connection#acquireHostList}.
*
* @param urls - URLs to use as host list.
*/
setHostList(urls) {
const cleanUrls = urls.map((url) => util.normalizeUrl(url));
this._hosts.splice(0, this._hosts.length, ...cleanUrls.map((url) => {
const i = this._hostUrls.indexOf(url);
if (i !== -1)
return this._hosts[i];
return createHost(url);
}));
this._hostUrls.splice(0, this._hostUrls.length, ...cleanUrls);
}
/**
* @internal
*
* Adds the given URL or URLs to the host list.
*
* See {@link Connection#acquireHostList}.
*
* @param urls - URL or URLs to add.
*/
addToHostList(urls) {
const cleanUrls = (Array.isArray(urls) ? urls : [urls]).map((url) => util.normalizeUrl(url));
const newUrls = cleanUrls.filter((url) => this._hostUrls.indexOf(url) === -1);
this._hostUrls.push(...newUrls);
this._hosts.push(...newUrls.map((url) => createHost(url)));
return cleanUrls;
}
/**
* @internal
*
* Sets the connection's active `transactionId`.
*
* While set, all requests will use this ID, ensuring the requests are executed
* within the transaction if possible. Setting the ID manually may cause
* unexpected behavior.
*
* See also {@link Connection#clearTransactionId}.
*
* @param transactionId - ID of the active transaction.
*/
setTransactionId(transactionId) {
this._transactionId = transactionId;
}
/**
* @internal
*
* Clears the connection's active `transactionId`.
*/
clearTransactionId() {
this._transactionId = null;
}
/**
* @internal
*
* Sets the header `headerName` with the given `value` or clears the header if
* `value` is `null`.
*
* @param headerName - Name of the header to set.
* @param value - Value of the header.
*/
setHeader(headerName, value) {
if (value === null) {
this._commonFetchOptions.headers.delete(headerName);
}
else {
this._commonFetchOptions.headers.set(headerName, value);
}
}
/**
* @internal
*
* Closes all open connections.
*
* See {@link databases.Database#close}.
*/
close() {
for (const host of this._hosts) {
if (host.close)
host.close();
}
}
/**
* @internal
*
* Waits for propagation.
*
* See {@link databases.Database#waitForPropagation}.
*
* @param request - Request to perform against each coordinator.
* @param timeout - Maximum number of milliseconds to wait for propagation.
*/
async waitForPropagation(request, timeout = Infinity) {
const numHosts = this._hosts.length;
const propagated = [];
const started = Date.now();
const endOfTime = started + timeout;
let index = 0;
while (true) {
if (propagated.length === numHosts) {
return;
}
while (propagated.includes(this._hostUrls[index])) {
index = (index + 1) % numHosts;
}
const hostUrl = this._hostUrls[index];
try {
await this.request({
...request,
hostUrl,
timeout: endOfTime - Date.now(),
});
}
catch (e) {
if (endOfTime < Date.now()) {
throw new errors.PropagationTimeoutError(undefined, {
cause: e,
});
}
await new Promise((resolve) => setTimeout(resolve, 1000));
continue;
}
if (!propagated.includes(hostUrl)) {
propagated.push(hostUrl);
}
}
}
/**
* @internal
*
* Performs a request using the arangojs connection pool.
*/
async request(requestOptions, transform) {
const { hostUrl, allowDirtyRead = false, isBinary = false, maxRetries = 0, method = "GET", retryOnConflict = 0, timeout = 0, headers: requestHeaders, body: requestBody, fetchOptions, ...taskOptions } = { ...this._commonRequestOptions, ...requestOptions };
const headers = util.mergeHeaders(this._commonFetchOptions.headers, requestHeaders);
let body = requestBody;
if (body instanceof FormData) {
const res = new Response(body);
const blob = await res.blob();
// Workaround for ArangoDB 3.12.0-rc1 and earlier:
// Omitting the final CRLF results in "bad request body" fatal error
body = new Blob([blob, "\r\n"], { type: blob.type });
}
else if (body) {
let contentType;
if (isBinary) {
contentType = "application/octet-stream";
}
else if (typeof body === "object") {
body = JSON.stringify(body);
contentType = "application/json";
}
else {
body = String(body);
contentType = "text/plain";
}
if (!headers.has("content-type")) {
headers.set("content-type", contentType);
}
}
if (this._transactionId) {
headers.set("x-arango-trx-id", this._transactionId);
}
if (allowDirtyRead) {
headers.set("x-arango-allow-dirty-read", "true");
}
return new Promise((resolve, reject) => {
const task = {
resolve,
reject,
transform,
retries: 0,
conflicts: 0,
options: {
...taskOptions,
hostUrl,
method,
headers,
body,
allowDirtyRead,
retryOnConflict,
maxRetries,
fetchOptions,
timeout,
},
};
if (this._precaptureStackTraces) {
if (typeof Error.captureStackTrace === "function") {
const capture = {};
Error.captureStackTrace(capture);
task.stack = () => `\n${capture.stack.split("\n").slice(3).join("\n")}`;
}
else {
const capture = util.generateStackTrace();
if (Object.prototype.hasOwnProperty.call(capture, "stack")) {
task.stack = () => `\n${capture.stack.split("\n").slice(4).join("\n")}`;
}
}
}
this._queue.push(task);
this._runQueue();
});
}
}
exports.Connection = Connection;
//#endregion
//# sourceMappingURL=connection.js.map