@itwin/core-common
Version:
iTwin.js components common to frontend and backend
162 lines • 7.56 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 { RpcConfiguration } from "./rpc/core/RpcConfiguration";
import { CURRENT_REQUEST } from "./rpc/core/RpcRegistry";
import { aggregateLoad, RpcRequest } from "./rpc/core/RpcRequest";
import { RpcRoutingToken } from "./rpc/core/RpcRoutingToken";
import { IpcSession } from "./ipc/IpcSession";
import { IModelError, NoContentError } from "./IModelError";
import { RpcRequestEvent, RpcRequestStatus } from "./rpc/core/RpcConstants";
import { BeDuration, BentleyStatus } from "@itwin/core-bentley";
/** An RPC interface is a set of operations exposed by a service that a client can call, using configurable protocols,
* in a platform-independent way. TheRpcInterface class is the base class for RPC interface definitions and implementations.
* @public
*/
export class RpcInterface {
static findDiff(backend, frontend) {
return backend.major !== frontend.major ? "major" :
backend.minor !== frontend.minor ? "minor" :
backend.patch !== frontend.patch ? "patch" :
backend.prerelease !== frontend.prerelease ? "prerelease" :
"same";
}
static parseVer(version) {
// separate the version from the prerelease tag
const parts = version.split(/[:-]/);
// Split the major.minor.path into separate components
const prefix = parts[0].split(".");
const ver = { major: Number(prefix[0]), minor: Number(prefix[1]), patch: Number(prefix[2]) };
if (parts.length > 1)
ver.prerelease = parts[1];
return ver;
}
/** Determines whether the backend version of an RPC interface is compatible (according to semantic versioning) with the frontend version of the interface. */
static isVersionCompatible(backend, frontend) {
if (backend === frontend)
return true; // most common case, versions are identical
const backendSemver = this.parseVer(backend);
const frontendSemver = this.parseVer(frontend);
// if either has a prerelease tag, they are not compatible unless version strings are identical
if (backendSemver.prerelease || frontendSemver.prerelease)
return false;
const difference = this.findDiff(backendSemver, frontendSemver);
// If the major versions are different, the versions are not compatible
if (difference === "major")
return false;
// special case for major version 0. If patch difference, backend patch must be greater than frontend patch
if (backendSemver.major === 0)
return (difference === "patch" && frontendSemver.patch < backendSemver.patch);
// patch difference is fine. If minor versions differ, compatible as long as backend minor version is greater
return difference === "patch" || (difference === "minor" && frontendSemver.minor < backendSemver.minor);
}
/** The configuration for the RPC interface.
* @internal
*/
configuration;
/** @internal */
routing;
/** @beta */
constructor(routing = RpcRoutingToken.default) {
this.routing = routing;
this.configuration = RpcConfiguration.supply(this);
}
/** Obtains the implementation result for an RPC operation. */
async forward(parameters) {
const parametersCompat = (arguments.length === 1 && typeof (parameters) === "object") ? parameters : arguments;
const parametersArray = Array.isArray(parametersCompat) ? parametersCompat : Array.prototype.slice.call(parametersCompat);
const operationName = parametersArray.pop();
const session = IpcSession.active;
if (session) {
return intercept(session, this, operationName, parametersArray);
}
else {
const request = new this.configuration.protocol.requestType(this, operationName, parametersArray);
request.submit(); // eslint-disable-line @typescript-eslint/no-floating-promises
this[CURRENT_REQUEST] = request;
return request.response;
}
}
}
RpcInterface.prototype.configurationSupplier = undefined;
class InterceptedRequest extends RpcRequest {
async load() { throw new Error(); }
async send() { throw new Error(); }
setHeader(_name, _value) { throw new Error(); }
}
async function intercept(session, client, operation, parameters) {
const request = new InterceptedRequest(client, operation, []);
client[CURRENT_REQUEST] = request;
const context = await client.configuration.protocol.serialize(request);
request.parameters = parameters;
const info = {
definition: {
interfaceName: context.operation.interfaceDefinition,
interfaceVersion: context.operation.interfaceVersion,
},
operation,
parameters,
context: {
applicationId: context.applicationId,
applicationVersion: context.applicationVersion,
id: context.id,
sessionId: context.sessionId,
protocolVersion: (context.protocolVersion || 0).toString(),
},
};
const dispatch = async () => {
aggregateLoad.lastRequest = new Date().getTime();
const response = await session.handleRpc(info);
aggregateLoad.lastResponse = new Date().getTime();
if (typeof (response) === "object" && response.hasOwnProperty("iTwinRpcCoreResponse") && response.hasOwnProperty("managedStatus")) {
const status = response;
if (status.managedStatus === "pending") {
return handlePending(request, status, dispatch);
}
else if (status.managedStatus === "notFound") {
return handleNotFound(request, status, dispatch);
}
else if (status.managedStatus === "noContent") {
return handleNoContent();
}
}
else {
return response;
}
};
return dispatch();
}
async function handlePending(request, status, dispatch) {
request._status = RpcRequestStatus.Pending;
request._extendedStatus = status.responseValue.message;
RpcRequest.events.raiseEvent(RpcRequestEvent.PendingUpdateReceived, request);
const delay = request.operation.policy.retryInterval(request.client.configuration);
await BeDuration.wait(delay);
return dispatch();
}
async function handleNotFound(request, status, dispatch) {
return new Promise((resolve, reject) => {
let resubmitted = false;
RpcRequest.notFoundHandlers.raiseEvent(request, status.responseValue, async () => {
if (resubmitted) {
throw new IModelError(BentleyStatus.ERROR, `Already resubmitted using this handler.`);
}
resubmitted = true;
try {
const response = await dispatch();
resolve(response);
}
catch (err) {
reject(err); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
}
}, reject);
});
}
async function handleNoContent() {
throw new NoContentError();
}
//# sourceMappingURL=RpcInterface.js.map