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