UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

260 lines • 11.9 kB
/*--------------------------------------------------------------------------------------------- * 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 */ import { BentleyError, BentleyStatus, IModelStatus, Logger, RpcInterfaceStatus, StatusCategory, Tracing } from "@itwin/core-bentley"; import { CommonLoggerCategory } from "../../CommonLoggerCategory"; import { IModelError } from "../../IModelError"; import { RpcInterface } from "../../RpcInterface"; import { RpcConfiguration } from "./RpcConfiguration"; import { RpcProtocolEvent, RpcRequestStatus } from "./RpcConstants"; import { RpcControlChannel, RpcNotFoundResponse, RpcPendingResponse } from "./RpcControl"; import { RpcMarshaling } from "./RpcMarshaling"; import { RpcOperation } from "./RpcOperation"; import { RpcProtocol, RpcProtocolVersion } from "./RpcProtocol"; import { CURRENT_INVOCATION, RpcRegistry } from "./RpcRegistry"; /** An RPC operation invocation in response to a request. * @internal */ export 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 ? RpcRequestStatus.Rejected : this._pending ? RpcRequestStatus.Pending : this._notFound ? RpcRequestStatus.NotFound : this._noContent ? RpcRequestStatus.NoContent : 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[CURRENT_INVOCATION]; } /** Constructs an invocation. */ constructor(protocol, request) { this._timeIn = new Date().getTime(); this.protocol = protocol; this.request = request; try { try { this.operation = RpcOperation.lookup(this.request.operation.interfaceDefinition, this.request.operation.operationName); const backend = this.operation.interfaceVersion; const frontend = this.request.operation.interfaceVersion; if (!RpcInterface.isVersionCompatible(backend, frontend)) { throw new IModelError(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.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) { 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(RpcProtocolEvent.RequestReceived, this); const parameters = request.parametersOverride || RpcMarshaling.deserialize(this.protocol, request.parameters); this.applyPolicies(parameters); const impl = RpcRegistry.instance.getImplForInterface(this.operation.interfaceDefinition); impl[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 RpcPendingResponse)) { Logger.logError(CommonLoggerCategory.RpcInterfaceBackend, "Error in RPC operation", { error: BentleyError.getErrorProps(error) }); 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.throwOnTokenMismatch) { throw new IModelError(BentleyStatus.ERROR, "IModelRpcProps mismatch detected for this request."); } else { Logger.logWarning(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(RpcProtocolEvent.BackendResponseCreated, this); const result = RpcMarshaling.serialize(this.protocol, value); return this.fulfill(result, value); } async fulfillRejected(reason) { this._timeOut = new Date().getTime(); if (!RpcConfiguration.developmentMode) reason.stack = undefined; const result = RpcMarshaling.serialize(this.protocol, reason); if (reason instanceof RpcPendingResponse) { this._pending = true; this._threw = false; result.objects = reason.message; this.protocol.events.raiseEvent(RpcProtocolEvent.BackendReportedPending, this); } else if (this.supportsNoContent() && reason?.errorNumber === IModelStatus.NoContent) { this._noContent = true; this._threw = false; this.protocol.events.raiseEvent(RpcProtocolEvent.BackendReportedNoContent, this); } else if (reason instanceof RpcNotFoundResponse) { this._notFound = true; this._threw = false; this.protocol.events.raiseEvent(RpcProtocolEvent.BackendReportedNotFound, this); } else { this._threw = true; this.protocol.events.raiseEvent(RpcProtocolEvent.BackendErrorOccurred, this); } return this.fulfill(result, reason); } supportsNoContent() { if (!this.request.protocolVersion) { return false; } return RpcProtocol.protocolVersion >= RpcProtocolVersion.IntroducedNoContent && this.request.protocolVersion >= RpcProtocolVersion.IntroducedNoContent; } supportsStatusCategory() { if (!this.request.protocolVersion) { return false; } if (!this.protocol.supportsStatusCategory) { return false; } return RpcProtocol.protocolVersion >= RpcProtocolVersion.IntroducedStatusCategory && this.request.protocolVersion >= 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.instance.getImplForInterface(this.operation.interfaceDefinition); if (impl[CURRENT_INVOCATION] === this) { impl[CURRENT_INVOCATION] = undefined; } } catch { } return fulfillment; } lookupOperationFunction(implementation) { const func = implementation[this.operation.operationName]; if (!func || typeof (func) !== "function") throw new IModelError(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 = StatusCategory.for(rawResult).code; } } } //# sourceMappingURL=RpcInvocation.js.map