arangojs
Version:
The official ArangoDB JavaScript driver.
503 lines • 17.6 kB
JavaScript
/**
* ```ts
* import type { Config } from "arangojs/connection.js";
* ```
*
* The "connection" module provides connection and configuration related types
* for TypeScript.
*
* @packageDocumentation
*/
import { LinkedList } from "./lib/linkedList.js";
import { ArangoError, HttpError, isArangoError, isArangoErrorResponse, isSystemError, } from "./error.js";
import { ERROR_ARANGO_CONFLICT, ERROR_ARANGO_MAINTENANCE_MODE, } from "./lib/codes.js";
import { normalizeUrl } from "./lib/normalizeUrl.js";
import { createRequest, } from "./lib/request.js";
import { joinPath } from "./lib/joinPath.js";
import { mergeHeaders } from "./lib/mergeHeaders.js";
const MIME_JSON = /\/(json|javascript)(\W|$)/;
const LEADER_ENDPOINT_HEADER = "x-arango-endpoint";
function isBearerAuth(auth) {
return auth.hasOwnProperty("token");
}
/**
* @internal
*/
function generateStackTrace() {
let err = new Error();
if (!err.stack) {
try {
throw err;
}
catch (e) {
err = e;
}
}
return err;
}
/**
* Indicates whether the given value represents a {@link Connection}.
*
* @param connection - A value that might be a connection.
*
* @internal
*/
export function isArangoConnection(connection) {
return Boolean(connection && connection.isArangoConnection);
}
/**
* Represents a connection pool shared by one or more databases.
*
* @internal
*/
export class Connection {
_activeTasks = 0;
_arangoVersion = 31100;
_headers;
_loadBalancingStrategy;
_maxRetries;
_taskPoolSize;
_requestConfig;
_retryOnConflict;
_queue = new LinkedList();
_databases = new Map();
_hosts = [];
_hostUrls = [];
_activeHostUrl;
_activeDirtyHostUrl;
_transactionId = null;
_precaptureStackTraces;
_queueTimes = new LinkedList();
_responseQueueTimeSamples;
/**
* @internal
*
* Creates a new `Connection` instance.
*
* @param config - An object with configuration options.
*
*/
constructor(config = {}) {
const URLS = config.url
? Array.isArray(config.url)
? config.url
: [config.url]
: ["http://127.0.0.1:8529"];
const DEFAULT_POOL_SIZE = 3 * (config.loadBalancingStrategy === "ROUND_ROBIN" ? URLS.length : 1);
if (config.arangoVersion !== undefined) {
this._arangoVersion = config.arangoVersion;
}
this._taskPoolSize = config.poolSize ?? DEFAULT_POOL_SIZE;
this._requestConfig = {
credentials: config.credentials ?? "same-origin",
keepalive: config.keepalive ?? true,
beforeRequest: config.beforeRequest,
afterResponse: config.afterResponse,
};
this._headers = new Headers(config.headers);
this._headers.set("x-arango-version", String(this._arangoVersion));
this._headers.set("x-arango-driver", `arangojs/9.0.0 (cloud)`);
this._loadBalancingStrategy = config.loadBalancingStrategy ?? "NONE";
this._precaptureStackTraces = Boolean(config.precaptureStackTraces);
this._responseQueueTimeSamples = config.responseQueueTimeSamples ?? 10;
this._retryOnConflict = config.retryOnConflict ?? 0;
if (this._responseQueueTimeSamples < 0) {
this._responseQueueTimeSamples = Infinity;
}
if (config.maxRetries === false) {
this._maxRetries = false;
}
else {
this._maxRetries = Number(config.maxRetries ?? 0);
}
this.addToHostList(URLS);
if (config.auth) {
if (isBearerAuth(config.auth)) {
this.setBearerAuth(config.auth);
}
else {
this.setBasicAuth(config.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._queue.length || this._activeTasks >= this._taskPoolSize)
return;
const task = this._queue.shift();
let hostUrl = this._activeHostUrl;
if (task.hostUrl !== undefined) {
hostUrl = task.hostUrl;
}
else if (task.allowDirtyRead) {
hostUrl = this._activeDirtyHostUrl;
this._activeDirtyHostUrl =
this._hostUrls[(this._hostUrls.indexOf(this._activeDirtyHostUrl) + 1) %
this._hostUrls.length];
task.options.headers.set("x-arango-allow-dirty-read", "true");
}
else if (this._loadBalancingStrategy === "ROUND_ROBIN") {
this._activeHostUrl =
this._hostUrls[(this._hostUrls.indexOf(this._activeHostUrl) + 1) %
this._hostUrls.length];
}
this._activeTasks += 1;
try {
const res = await this._hosts[this._hostUrls.indexOf(hostUrl)](task.options);
const leaderEndpoint = res.headers.get(LEADER_ENDPOINT_HEADER);
if (res.status === 503 && leaderEndpoint) {
const [cleanUrl] = this.addToHostList(leaderEndpoint);
task.hostUrl = cleanUrl;
if (this._activeHostUrl === hostUrl) {
this._activeHostUrl = cleanUrl;
}
this._queue.push(task);
}
else {
res.arangojsHostUrl = hostUrl;
const contentType = res.headers.get("content-type");
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();
}
}
if (res.status >= 400) {
try {
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 new ArangoError(res);
}
}
throw new HttpError(res);
}
catch (err) {
if (task.stack) {
err.stack += task.stack();
}
throw err;
}
}
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();
}
}
task.resolve(task.transform ? task.transform(res) : res);
}
}
catch (err) {
if (!task.allowDirtyRead &&
this._hosts.length > 1 &&
this._activeHostUrl === hostUrl &&
this._loadBalancingStrategy !== "ROUND_ROBIN") {
this._activeHostUrl =
this._hostUrls[(this._hostUrls.indexOf(this._activeHostUrl) + 1) %
this._hostUrls.length];
}
if (isArangoError(err) &&
err.errorNum === ERROR_ARANGO_CONFLICT &&
task.retryOnConflict > 0) {
task.retryOnConflict -= 1;
this._queue.push(task);
}
else if (((isSystemError(err) &&
err.syscall === "connect" &&
err.code === "ECONNREFUSED") ||
(isArangoError(err) &&
err.errorNum === ERROR_ARANGO_MAINTENANCE_MODE)) &&
task.hostUrl === undefined &&
this._maxRetries !== false &&
task.retries < (this._maxRetries || this._hosts.length - 1)) {
task.retries += 1;
this._queue.push(task);
}
else {
if (task.stack) {
err.stack += task.stack();
}
task.reject(err);
}
}
finally {
this._activeTasks -= 1;
}
this._runQueue();
}
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) => 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];
const parsedUrl = new URL(url);
if (!parsedUrl.pathname.endsWith("/")) {
parsedUrl.pathname += "/";
}
return createRequest(parsedUrl, this._requestConfig);
}));
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) => normalizeUrl(url));
const newUrls = cleanUrls.filter((url) => this._hostUrls.indexOf(url) === -1);
this._hostUrls.push(...newUrls);
this._hosts.push(...newUrls.map((url) => {
const parsedUrl = new URL(url);
if (!parsedUrl.pathname.endsWith("/")) {
parsedUrl.pathname += "/";
}
return createRequest(parsedUrl, this._requestConfig);
}));
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._headers.delete(headerName);
}
else {
this._headers.set(headerName, value);
}
}
/**
* @internal
*
* Closes all open connections.
*
* See {@link database.Database#close}.
*/
close() {
for (const host of this._hosts) {
if (host.close)
host.close();
}
}
/**
* @internal
*
* Waits for propagation.
*
* See {@link database.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();
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 });
}
catch (e) {
if (started + timeout < Date.now()) {
throw 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.
*/
request({ hostUrl, method = "GET", body, expectBinary = false, isBinary = false, allowDirtyRead = false, retryOnConflict = this._retryOnConflict, timeout = 0, headers: requestHeaders, basePath, path, search: params, }, transform) {
return new Promise((resolve, reject) => {
const headers = mergeHeaders(this._headers, requestHeaders ?? {});
if (body && !(body instanceof FormData)) {
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);
}
const task = {
retries: 0,
hostUrl,
allowDirtyRead,
retryOnConflict,
options: {
pathname: joinPath(basePath, path) ?? "",
search: params &&
(params instanceof URLSearchParams
? params
: new URLSearchParams(params)),
headers,
timeout,
method,
expectBinary,
body,
},
reject,
resolve,
transform,
};
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 = 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();
});
}
}
//# sourceMappingURL=connection.js.map