"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.RpcInvocation = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const CommonLoggerCategory_1 = require("../../CommonLoggerCategory");
const IModelError_1 = require("../../IModelError");
const RpcInterface_1 = require("../../RpcInterface");
const RpcConfiguration_1 = require("./RpcConfiguration");
const RpcConstants_1 = require("./RpcConstants");
const RpcControl_1 = require("./RpcControl");
const RpcMarshaling_1 = require("./RpcMarshaling");
const RpcOperation_1 = require("./RpcOperation");
const RpcProtocol_1 = require("./RpcProtocol");
const RpcRegistry_1 = require("./RpcRegistry");
/** An RPC operation invocation in response to a request.
 * @internal
 */
class RpcInvocation {
    static runActivity = async (_activity, fn) => fn();
    _threw = false;
    _pending = false;
    _notFound = false;
    _noContent = false;
    _timeIn = 0;
    _timeOut = 0;
    /** The protocol for this invocation. */
    protocol;
    /** The received request. */
    request;
    /** The operation of the request. */
    operation = undefined;
    /** The implementation response. */
    result;
    /** The fulfillment for this request. */
    fulfillment;
    /** The status for this request. */
    get status() {
        return this._threw ? RpcConstants_1.RpcRequestStatus.Rejected :
            this._pending ? RpcConstants_1.RpcRequestStatus.Pending :
                this._notFound ? RpcConstants_1.RpcRequestStatus.NotFound :
                    this._noContent ? RpcConstants_1.RpcRequestStatus.NoContent :
                        RpcConstants_1.RpcRequestStatus.Resolved;
    }
    /** The elapsed time for this invocation. */
    get elapsed() {
        return this._timeOut - this._timeIn;
    }
    /**
     * The invocation for the current RPC operation.
     * @note The return value of this function is only reliable in an RPC impl class member function where program control was received from the RpcInvocation constructor function.
     */
    static current(rpcImpl) {
        return rpcImpl[RpcRegistry_1.CURRENT_INVOCATION];
    }
    /** Constructs an invocation. */
    constructor(protocol, request) {
        this._timeIn = new Date().getTime();
        this.protocol = protocol;
        this.request = request;
        try {
            try {
                this.operation = RpcOperation_1.RpcOperation.lookup(this.request.operation.interfaceDefinition, this.request.operation.operationName);
                const backend = this.operation.interfaceVersion;
                const frontend = this.request.operation.interfaceVersion;
                if (!RpcInterface_1.RpcInterface.isVersionCompatible(backend, frontend)) {
                    throw new IModelError_1.IModelError(core_bentley_1.RpcInterfaceStatus.IncompatibleVersion, `Backend version ${backend} does not match frontend version ${frontend} for RPC interface ${this.operation.operationName}.`);
                }
            }
            catch (error) {
                if (this.handleUnknownOperation(error)) {
                    this.operation = RpcOperation_1.RpcOperation.lookup(this.request.operation.interfaceDefinition, this.request.operation.operationName);
                }
                else {
                    throw error;
                }
            }
            this.result = this.resolve();
        }
        catch (error) {
            this.result = this.reject(error);
        }
        this.fulfillment = this.result.then(async (value) => this._threw ? this.fulfillRejected(value) : this.fulfillResolved(value), async (reason) => this.fulfillRejected(reason));
    }
    handleUnknownOperation(error) {
        RpcControl_1.RpcControlChannel.ensureInitialized();
        return this.protocol.configuration.controlChannel.handleUnknownOperation(this, error);
    }
    static sanitizeForLog(activity) {
        /* eslint-disable @typescript-eslint/naming-convention */
        return activity ? {
            ActivityId: activity.activityId, SessionId: activity.sessionId, ApplicationId: activity.applicationId, ApplicationVersion: activity.applicationVersion, rpcMethod: activity.rpcMethod,
        } : undefined;
        /* eslint-enable @typescript-eslint/naming-convention */
    }
    async resolve() {
        const request = this.request;
        const activity = {
            activityId: request.id,
            applicationId: request.applicationId,
            applicationVersion: request.applicationVersion,
            sessionId: request.sessionId,
            user: request.user,
            accessToken: request.authorization,
            rpcMethod: request.operation.operationName,
        };
        try {
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.RequestReceived, this);
            const parameters = request.parametersOverride || RpcMarshaling_1.RpcMarshaling.deserialize(this.protocol, request.parameters);
            this.applyPolicies(parameters);
            const impl = RpcRegistry_1.RpcRegistry.instance.getImplForInterface(this.operation.interfaceDefinition);
            impl[RpcRegistry_1.CURRENT_INVOCATION] = this;
            const op = this.lookupOperationFunction(impl);
            return await RpcInvocation.runActivity(activity, async () => op.call(impl, ...parameters)
                .catch(async (error) => {
                // this catch block is intentionally placed inside `runActivity` to attach the right logging metadata and use the correct openTelemetry span.
                if (!(error instanceof RpcControl_1.RpcPendingResponse)) {
                    core_bentley_1.Logger.logError(CommonLoggerCategory_1.CommonLoggerCategory.RpcInterfaceBackend, "Error in RPC operation", { error: core_bentley_1.BentleyError.getErrorProps(error) });
                    core_bentley_1.Tracing.recordException(error);
                }
                throw error;
            }));
        }
        catch (error) {
            return this.reject(error);
        }
    }
    applyPolicies(parameters) {
        if (!parameters || !Array.isArray(parameters))
            return;
        for (let i = 0; i !== parameters.length; ++i) {
            const parameter = parameters[i];
            const isToken = typeof (parameter) === "object" && parameter !== null && parameter.hasOwnProperty("iModelId") && parameter.hasOwnProperty("iTwinId");
            if (isToken && this.protocol.checkToken && !this.operation.policy.allowTokenMismatch) {
                const inflated = this.protocol.inflateToken(parameter, this.request);
                parameters[i] = inflated;
                if (!RpcInvocation.compareTokens(parameter, inflated)) {
                    if (RpcConfiguration_1.RpcConfiguration.throwOnTokenMismatch) {
                        throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "IModelRpcProps mismatch detected for this request.");
                    }
                    else {
                        core_bentley_1.Logger.logWarning(CommonLoggerCategory_1.CommonLoggerCategory.RpcInterfaceBackend, "IModelRpcProps mismatch detected for this request.");
                    }
                }
            }
        }
    }
    static compareTokens(a, b) {
        return a.key === b.key &&
            a.iTwinId === b.iTwinId &&
            a.iModelId === b.iModelId &&
            (undefined === a.changeset || (a.changeset.id === b.changeset?.id));
    }
    async reject(error) {
        this._threw = true;
        return error;
    }
    async fulfillResolved(value) {
        this._timeOut = new Date().getTime();
        this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.BackendResponseCreated, this);
        const result = RpcMarshaling_1.RpcMarshaling.serialize(this.protocol, value);
        return this.fulfill(result, value);
    }
    async fulfillRejected(reason) {
        this._timeOut = new Date().getTime();
        if (!RpcConfiguration_1.RpcConfiguration.developmentMode)
            reason.stack = undefined;
        const result = RpcMarshaling_1.RpcMarshaling.serialize(this.protocol, reason);
        if (reason instanceof RpcControl_1.RpcPendingResponse) {
            this._pending = true;
            this._threw = false;
            result.objects = reason.message;
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.BackendReportedPending, this);
        }
        else if (this.supportsNoContent() && reason?.errorNumber === core_bentley_1.IModelStatus.NoContent) {
            this._noContent = true;
            this._threw = false;
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.BackendReportedNoContent, this);
        }
        else if (reason instanceof RpcControl_1.RpcNotFoundResponse) {
            this._notFound = true;
            this._threw = false;
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.BackendReportedNotFound, this);
        }
        else {
            this._threw = true;
            this.protocol.events.raiseEvent(RpcConstants_1.RpcProtocolEvent.BackendErrorOccurred, this);
        }
        return this.fulfill(result, reason);
    }
    supportsNoContent() {
        if (!this.request.protocolVersion) {
            return false;
        }
        return RpcProtocol_1.RpcProtocol.protocolVersion >= RpcProtocol_1.RpcProtocolVersion.IntroducedNoContent && this.request.protocolVersion >= RpcProtocol_1.RpcProtocolVersion.IntroducedNoContent;
    }
    supportsStatusCategory() {
        if (!this.request.protocolVersion) {
            return false;
        }
        if (!this.protocol.supportsStatusCategory) {
            return false;
        }
        return RpcProtocol_1.RpcProtocol.protocolVersion >= RpcProtocol_1.RpcProtocolVersion.IntroducedStatusCategory && this.request.protocolVersion >= RpcProtocol_1.RpcProtocolVersion.IntroducedStatusCategory;
    }
    fulfill(result, rawResult) {
        const fulfillment = {
            result,
            rawResult,
            status: this.protocol.getCode(this.status),
            id: this.request.id,
            interfaceName: (typeof (this.operation) === "undefined") ? "" : this.operation.interfaceDefinition.interfaceName,
            allowCompression: this.operation ? this.operation.policy.allowResponseCompression : true,
        };
        this.transformResponseStatus(fulfillment, rawResult);
        try {
            const impl = RpcRegistry_1.RpcRegistry.instance.getImplForInterface(this.operation.interfaceDefinition);
            if (impl[RpcRegistry_1.CURRENT_INVOCATION] === this) {
                impl[RpcRegistry_1.CURRENT_INVOCATION] = undefined;
            }
        }
        catch { }
        return fulfillment;
    }
    lookupOperationFunction(implementation) {
        const func = implementation[this.operation.operationName];
        if (!func || typeof (func) !== "function")
            throw new IModelError_1.IModelError(core_bentley_1.BentleyStatus.ERROR, `RPC interface class "${implementation.constructor.name}" does not implement operation "${this.operation.operationName}".`);
        return func;
    }
    transformResponseStatus(fulfillment, rawResult) {
        if (!this.supportsStatusCategory()) {
            return;
        }
        let managedStatus;
        if (this._pending) {
            managedStatus = "pending";
        }
        else if (this._notFound) {
            managedStatus = "notFound";
        }
        else if (this._noContent) {
            managedStatus = "noContent";
        }
        if (managedStatus) {
            const responseValue = fulfillment.result.objects;
            const status = { iTwinRpcCoreResponse: true, managedStatus, responseValue };
            fulfillment.result.objects = JSON.stringify(status);
            status.responseValue = rawResult; // for ipc case
            fulfillment.rawResult = status;
        }
        if (rawResult instanceof Error) {
            fulfillment.status = core_bentley_1.StatusCategory.for(rawResult).code;
        }
    }
}
exports.RpcInvocation = RpcInvocation;
//# sourceMappingURL=RpcInvocation.js.map