UNPKG

arangojs

Version:

The official ArangoDB JavaScript driver.

503 lines 17.6 kB
/** * ```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