@itwin/core-common
Version:
iTwin.js components common to frontend and backend
260 lines • 11.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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