"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module RpcInterface
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initializeRpcRequest = exports.RpcRequest = exports.ResponseLike = exports.aggregateLoad = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const IModelError_1 = require("../../IModelError");
const RpcConfiguration_1 = require("./RpcConfiguration");
const RpcConstants_1 = require("./RpcConstants");
const RpcMarshaling_1 = require("./RpcMarshaling");
const RpcOperation_1 = require("./RpcOperation");
const RpcProtocol_1 = require("./RpcProtocol");
const RpcRegistry_1 = require("./RpcRegistry");
/* eslint-disable @typescript-eslint/naming-convention */
// cspell:ignore csrf
/* eslint-disable @typescript-eslint/no-deprecated */
/** @internal */
exports.aggregateLoad = { lastRequest: 0, lastResponse: 0 };
/** @internal */
class ResponseLike {
    _data;
    get body() { return null; }
    async arrayBuffer() { return this._data; }
    async blob() { throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Not implemented."); }
    async formData() { throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Not implemented."); }
    async json() { return this._data; }
    async text() { return this._data; }
    get bodyUsed() { return false; }
    get headers() { throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Not implemented."); }
    get ok() { return this.status >= 200 && this.status <= 299; }
    get redirected() { return false; }
    get status() { return 200; }
    get statusText() { return ""; }
    get trailer() { throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Not implemented."); }
    get type() { return "basic"; }
    get url() { return ""; }
    clone() { return { ...this }; }
    constructor(data) {
        this._data = Promise.resolve(data);
    }
}
exports.ResponseLike = ResponseLike;
class Cancellable {
    promise;
    cancel() { }
    constructor(task) {
        this.promise = new Promise((resolve, reject) => {
            this.cancel = () => resolve(undefined);
            task.then(resolve, reject);
        });
    }
}
/** A RPC operation request.
 * @internal
 */
class RpcRequest {
    static _activeRequests = new Map();
    _resolve = () => undefined;
    _resolveRaw = () => undefined;
    _reject = () => undefined;
    _rejectRaw = () => undefined;
    _created = 0;
    _lastSubmitted = 0;
    _lastUpdated = 0;
    /** @internal */
    _status = RpcConstants_1.RpcRequestStatus.Unknown;
    /** @internal */
    _extendedStatus = "";
    _connecting = false;
    _active = true;
    _hasRawListener = false;
    _raw = undefined;
    _sending;
    _attempts = 0;
    _retryAfter = null;
    _transientFaults = 0;
    _response = undefined;
    _rawPromise;
    responseProtocolVersion = RpcProtocol_1.RpcProtocolVersion.None;
    /** All RPC requests that are currently in flight. */
    static get activeRequests() { return this._activeRequests; }
    /** Events raised by RpcRequest. See [[RpcRequestEvent]] */
    static events = new core_bentley_1.BeEvent();
    /** Resolvers for "not found" requests. See [[RpcRequestNotFoundHandler]] */
    static notFoundHandlers = new core_bentley_1.BeEvent();
    /** The aggregate operations profile of all active RPC interfaces. */
    static get aggregateLoad() { return exports.aggregateLoad; }
    /**
     * The request for the current RPC operation.
     * @note The return value of this function is only reliable if program control was received from a RPC interface class member function that directly returns the result of calling RpcInterface.forward.
     */
    static current(context) {
        return context[RpcRegistry_1.CURRENT_REQUEST];
    }
    /** The unique identifier of this request. */
    id;
    /** The operation for this request. */
    operation;
    /** The parameters for this request. */
    parameters;
    /** The RPC client instance for this request. */
    client;
    /** Convenience access to the protocol of this request. */
    protocol;
    /** The implementation response for this request. */
    response;
    /** The status of this request. */
    get status() { return this._status; }
    /** Extended status information for this request (if available). */
    get extendedStatus() { return this._extendedStatus; }
    /** The last submission for this request. */
    get lastSubmitted() { return this._lastSubmitted; }
    /** The last status update received for this request. */
    get lastUpdated() { return this._lastUpdated; }
    /** The target interval (in milliseconds) between submission attempts for this request. */
    retryInterval;
    /** Whether a connection is active for this request. */
    get connecting() { return this._connecting; }
    /** Whether this request is pending. */
    get pending() {
        switch (this.status) {
            case RpcConstants_1.RpcRequestStatus.Submitted:
            case RpcConstants_1.RpcRequestStatus.Pending: {
                return true;
            }
            default: {
                return false;
            }
        }
    }
    /** The elapsed time for this request. */
    get elapsed() {
        return this._lastUpdated - this._created;
    }
    /** A protocol-specific path identifier for this request. */
    path;
    /** A protocol-specific method identifier for this request. */
    method;
    /** An attempt-specific value for when to next retry this request. */
    get retryAfter() { return this._retryAfter; }
    /** Finds the first parameter of a given structural type if present. */
    findParameterOfType(requiredProperties) {
        for (const param of this.parameters) {
            if (typeof (param) === "object" && param !== null) {
                for (const prop of Object.getOwnPropertyNames(requiredProperties)) {
                    if (prop in param && typeof (param[prop]) === requiredProperties[prop]) {
                        return param;
                    }
                }
            }
        }
        return undefined;
    }
    /** Finds the first IModelRpcProps parameter if present. */
    findTokenPropsParameter() {
        return this.findParameterOfType({ iModelId: "string" });
    }
    /** The raw implementation response for this request. */
    get rawResponse() {
        this._hasRawListener = true;
        return this._rawPromise;
    }
    /** Constructs an RPC request. */
    constructor(client, operation, parameters) {
        this._created = new Date().getTime();
        this.path = "";
        this.method = "";
        this.client = client;
        this.protocol = client.configuration.protocol;
        this.operation = RpcOperation_1.RpcOperation.lookup(client.constructor, operation);
        this.parameters = parameters;
        this.retryInterval = this.operation.policy.retryInterval(client.configuration);
        this.response = new Promise((resolve, reject) => {
            this._resolve = resolve;
            this._reject = reject;
        });
        this._rawPromise = new Promise((resolve, reject) => {
            this._resolveRaw = resolve;
            this._rejectRaw = reject;
        });
        this.id = RpcConfiguration_1.RpcConfiguration.requestContext.getId(this) || core_bentley_1.Guid.createValue();
        this.setStatus(RpcConstants_1.RpcRequestStatus.Created);
        this.operation.policy.requestCallback(this);
    }
    /** Sets the last updated time for the request. */
    setLastUpdatedTime() {
        this._lastUpdated = new Date().getTime();
    }
    computeRetryAfter(attempts) {
        return (((Math.pow(2, attempts) - 1) / 2) * 500) + 500;
    }
    recordTransientFault() {
        ++this._transientFaults;
    }
    resetTransientFaultCount() {
        this._transientFaults = 0;
    }
    supportsStatusCategory() {
        if (!this.protocol.supportsStatusCategory) {
            return false;
        }
        return RpcProtocol_1.RpcProtocol.protocolVersion >= RpcProtocol_1.RpcProtocolVersion.IntroducedStatusCategory && this.responseProtocolVersion >= RpcProtocol_1.RpcProtocolVersion.IntroducedStatusCategory;
    }
    /* @internal */
    cancel() {
        if (typeof (this._sending) === "undefined") {
            return;
        }
        this._sending.cancel();
        this._sending = undefined;
        this._connecting = false;
        RpcRequest._activeRequests.delete(this.id);
        this.setStatus(RpcConstants_1.RpcRequestStatus.Cancelled);
    }
    /* @internal */
    async submit() {
        if (!this._active)
            return;
        this._lastSubmitted = new Date().getTime();
        this._retryAfter = null;
        ++this._attempts;
        if (this.status === RpcConstants_1.RpcRequestStatus.Created || this.status === RpcConstants_1.RpcRequestStatus.NotFound || this.status === RpcConstants_1.RpcRequestStatus.Cancelled) {
            this.setStatus(RpcConstants_1.RpcRequestStatus.Submitted);
        }
        try {
            this._connecting = true;
            RpcRequest._activeRequests.set(this.id, this);
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.RequestCreated, this);
            this._sending = new Cancellable(this.setHeaders().then(async () => this.send()));
            this.operation.policy.sentCallback(this);
            const response = await this._sending.promise;
            if (typeof (response) === "undefined") {
                return;
            }
            this._sending = undefined;
            const status = this.protocol.getStatus(response);
            if (this._hasRawListener && status === RpcConstants_1.RpcRequestStatus.Resolved && typeof (this._response) !== "undefined") {
                this._connecting = false;
                RpcRequest._activeRequests.delete(this.id);
                this.resolveRaw();
            }
            else {
                this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.ResponseLoading, this);
                if (status === RpcConstants_1.RpcRequestStatus.Unknown) {
                    this._connecting = false;
                    RpcRequest._activeRequests.delete(this.id);
                    this.handleUnknownResponse(response);
                    return;
                }
                const value = await this.load();
                this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.ResponseLoaded, this);
                RpcRequest._activeRequests.delete(this.id);
                this._connecting = false;
                this.handleResponse(response, value);
            }
        }
        catch (err) {
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.ConnectionErrorReceived, this, err);
            RpcRequest._activeRequests.delete(this.id);
            this._connecting = false;
            this.reject(err);
        }
    }
    handleUnknownResponse(code) {
        this.reject(new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, `Unknown response ${code}.`));
    }
    handleResponse(code, value) {
        const protocolStatus = this.protocol.getStatus(code);
        const status = this.transformResponseStatus(protocolStatus, value);
        if (RpcConstants_1.RpcRequestStatus.isTransientError(status)) {
            return this.handleTransientError(status);
        }
        switch (status) {
            case RpcConstants_1.RpcRequestStatus.Resolved: {
                return this.handleResolved(value);
            }
            case RpcConstants_1.RpcRequestStatus.Rejected: {
                return this.handleRejected(value);
            }
            case RpcConstants_1.RpcRequestStatus.Pending: {
                return this.setPending(status, value.objects);
            }
            case RpcConstants_1.RpcRequestStatus.NotFound: {
                return this.handleNotFound(status, value);
            }
            case RpcConstants_1.RpcRequestStatus.NoContent: {
                return this.handleNoContent();
            }
        }
    }
    transformResponseStatus(protocolStatus, value) {
        if (!this.supportsStatusCategory()) {
            return protocolStatus;
        }
        let status = protocolStatus;
        if (protocolStatus === RpcConstants_1.RpcRequestStatus.Pending) {
            status = RpcConstants_1.RpcRequestStatus.Rejected;
        }
        else if (protocolStatus === RpcConstants_1.RpcRequestStatus.NotFound) {
            status = RpcConstants_1.RpcRequestStatus.Rejected;
        }
        else if (protocolStatus === RpcConstants_1.RpcRequestStatus.Unknown) {
            status = RpcConstants_1.RpcRequestStatus.Rejected;
        }
        if (value.objects.indexOf("iTwinRpcCoreResponse") !== -1 && value.objects.indexOf("managedStatus") !== -1) {
            const managedStatus = JSON.parse(value.objects);
            value.objects = managedStatus.responseValue;
            if (managedStatus.managedStatus === "pending") {
                status = RpcConstants_1.RpcRequestStatus.Pending;
            }
            else if (managedStatus.managedStatus === "notFound") {
                status = RpcConstants_1.RpcRequestStatus.NotFound;
            }
        }
        return status;
    }
    handleResolved(value) {
        try {
            this._raw = value.objects;
            const result = RpcMarshaling_1.RpcMarshaling.deserialize(this.protocol, value);
            if (ArrayBuffer.isView(result)) {
                this._raw = result.buffer;
            }
            return this.resolve(result);
        }
        catch (err) {
            return this.reject(err);
        }
    }
    handleRejected(value) {
        this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.BackendErrorReceived, this);
        try {
            const error = RpcMarshaling_1.RpcMarshaling.deserialize(this.protocol, value);
            const hasInfo = error && typeof (error) === "object" && error.hasOwnProperty("name") && error.hasOwnProperty("message");
            const name = hasInfo ? error.name : "";
            const message = hasInfo ? error.message : "";
            const errorNumber = (hasInfo && error.hasOwnProperty("errorNumber")) ? error.errorNumber : core_bentley_1.BentleyStatus.ERROR;
            return this.reject(new IModelError_1.BackendError(errorNumber, name, message, () => error));
        }
        catch (err) {
            return this.reject(err);
        }
    }
    handleNoContent() {
        return this.reject(new IModelError_1.NoContentError());
    }
    handleNotFound(status, value) {
        if (RpcRequest.notFoundHandlers.numberOfListeners === 0)
            this.handleRejected(value);
        const response = RpcMarshaling_1.RpcMarshaling.deserialize(this.protocol, value);
        this.setStatus(status);
        let resubmitted = false;
        RpcRequest.notFoundHandlers.raiseEvent(this, response, () => {
            if (resubmitted)
                throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, `Already resubmitted using this handler.`);
            resubmitted = true;
            void this.submit();
        }, (reason) => reason ? this.reject(reason) : this.handleRejected(value));
        return;
    }
    resolve(result) {
        if (!this._active)
            return;
        this._active = false;
        this.setLastUpdatedTime();
        this._resolve(result);
        if (this._hasRawListener) {
            if (typeof (this._raw) === "undefined") {
                throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Cannot access raw response.");
            }
            this._resolveRaw(new ResponseLike(this._raw));
        }
        this.setStatus(RpcConstants_1.RpcRequestStatus.Resolved);
        this[Symbol.dispose]();
    }
    resolveRaw() {
        if (typeof (this._response) === "undefined") {
            throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Cannot access raw response.");
        }
        this._active = false;
        this.setLastUpdatedTime();
        this._resolveRaw(this._response);
        this.setStatus(RpcConstants_1.RpcRequestStatus.Resolved);
        this[Symbol.dispose]();
    }
    reject(reason) {
        if (!this._active)
            return;
        this._active = false;
        this.setLastUpdatedTime();
        this._reject(reason);
        if (this._hasRawListener) {
            this._rejectRaw(reason);
        }
        this.setStatus(RpcConstants_1.RpcRequestStatus.Rejected);
        this[Symbol.dispose]();
    }
    /** @internal */
    [Symbol.dispose]() {
        this.setStatus(RpcConstants_1.RpcRequestStatus.Disposed);
        this._raw = undefined;
        this._response = undefined;
        const client = this.client;
        if (client[RpcRegistry_1.CURRENT_REQUEST] === this) {
            client[RpcRegistry_1.CURRENT_REQUEST] = undefined;
        }
    }
    setPending(status, extendedStatus) {
        if (!this._active)
            return;
        this.setLastUpdatedTime();
        this._extendedStatus = extendedStatus;
        this.setStatus(status);
        RpcRequest.events.raiseEvent(RpcConstants_1.RpcRequestEvent.PendingUpdateReceived, this);
    }
    handleTransientError(status) {
        if (!this._active)
            return;
        this.setLastUpdatedTime();
        this._retryAfter = this.computeRetryAfter(this._attempts - 1);
        if (this._transientFaults > this.protocol.configuration.transientFaultLimit) {
            this.reject(new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, `Exceeded transient fault limit.`));
        }
        else {
            this.setStatus(status);
            RpcRequest.events.raiseEvent(RpcConstants_1.RpcRequestEvent.TransientErrorReceived, this);
        }
    }
    async setHeaders() {
        const versionHeader = this.protocol.protocolVersionHeaderName;
        if (versionHeader && RpcProtocol_1.RpcProtocol.protocolVersion)
            this.setHeader(versionHeader, RpcProtocol_1.RpcProtocol.protocolVersion.toString());
        const headerNames = this.protocol.serializedClientRequestContextHeaderNames;
        const headerValues = await RpcConfiguration_1.RpcConfiguration.requestContext.serialize(this);
        if (headerNames.id)
            this.setHeader(headerNames.id, headerValues.id || this.id); // Cannot be empty
        if (headerNames.applicationVersion)
            this.setHeader(headerNames.applicationVersion, headerValues.applicationVersion);
        if (headerNames.applicationId)
            this.setHeader(headerNames.applicationId, headerValues.applicationId);
        if (headerNames.sessionId)
            this.setHeader(headerNames.sessionId, headerValues.sessionId);
        if (headerNames.authorization && headerValues.authorization)
            this.setHeader(headerNames.authorization, headerValues.authorization);
        if (headerValues.csrfToken)
            this.setHeader(headerValues.csrfToken.headerName, headerValues.csrfToken.headerValue);
    }
    setStatus(status) {
        if (this._status === status)
            return;
        this._status = status;
        RpcRequest.events.raiseEvent(RpcConstants_1.RpcRequestEvent.StatusChanged, this);
    }
}
exports.RpcRequest = RpcRequest;
/** @internal */
exports.initializeRpcRequest = (() => {
    let initialized = false;
    return () => {
        if (initialized) {
            return;
        }
        initialized = true;
        RpcRequest.events.addListener((type, request) => {
            if (type !== RpcConstants_1.RpcRequestEvent.StatusChanged)
                return;
            switch (request.status) {
                case RpcConstants_1.RpcRequestStatus.Submitted: {
                    exports.aggregateLoad.lastRequest = request.lastSubmitted;
                    break;
                }
                case RpcConstants_1.RpcRequestStatus.Pending:
                case RpcConstants_1.RpcRequestStatus.Resolved:
                case RpcConstants_1.RpcRequestStatus.Rejected: {
                    exports.aggregateLoad.lastResponse = request.lastUpdated;
                    break;
                }
            }
        });
        RpcProtocol_1.RpcProtocol.events.addListener((type) => {
            const now = new Date().getTime();
            switch (type) {
                case RpcConstants_1.RpcProtocolEvent.RequestReceived: {
                    exports.aggregateLoad.lastRequest = now;
                    break;
                }
                case RpcConstants_1.RpcProtocolEvent.BackendReportedPending:
                case RpcConstants_1.RpcProtocolEvent.BackendErrorOccurred:
                case RpcConstants_1.RpcProtocolEvent.BackendResponseCreated: {
                    exports.aggregateLoad.lastResponse = now;
                    break;
                }
            }
        });
    };
})();
//# sourceMappingURL=RpcRequest.js.map