UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

497 lines • 19.8 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 { BeEvent, BentleyStatus, Guid } from "@itwin/core-bentley"; import { BackendError, IModelError, NoContentError } from "../../IModelError"; import { RpcConfiguration } from "./RpcConfiguration"; import { RpcProtocolEvent, RpcRequestEvent, RpcRequestStatus } from "./RpcConstants"; import { RpcMarshaling } from "./RpcMarshaling"; import { RpcOperation } from "./RpcOperation"; import { RpcProtocol, RpcProtocolVersion } from "./RpcProtocol"; import { CURRENT_REQUEST } from "./RpcRegistry"; /* eslint-disable @typescript-eslint/naming-convention */ // cspell:ignore csrf /* eslint-disable @typescript-eslint/no-deprecated */ /** @internal */ export const aggregateLoad = { lastRequest: 0, lastResponse: 0 }; /** @internal */ export class ResponseLike { _data; get body() { return null; } async arrayBuffer() { return this._data; } async blob() { throw new IModelError(BentleyStatus.ERROR, "Not implemented."); } async formData() { throw new IModelError(BentleyStatus.ERROR, "Not implemented."); } async json() { return this._data; } async text() { return this._data; } get bodyUsed() { return false; } get headers() { throw new IModelError(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(BentleyStatus.ERROR, "Not implemented."); } get type() { return "basic"; } get url() { return ""; } clone() { return { ...this }; } constructor(data) { this._data = Promise.resolve(data); } } 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 */ export class RpcRequest { static _activeRequests = new Map(); _resolve = () => undefined; _resolveRaw = () => undefined; _reject = () => undefined; _rejectRaw = () => undefined; _created = 0; _lastSubmitted = 0; _lastUpdated = 0; /** @internal */ _status = RpcRequestStatus.Unknown; /** @internal */ _extendedStatus = ""; _connecting = false; _active = true; _hasRawListener = false; _raw = undefined; _sending; _attempts = 0; _retryAfter = null; _transientFaults = 0; _response = undefined; _rawPromise; responseProtocolVersion = 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 BeEvent(); /** Resolvers for "not found" requests. See [[RpcRequestNotFoundHandler]] */ static notFoundHandlers = new BeEvent(); /** The aggregate operations profile of all active RPC interfaces. */ static get aggregateLoad() { return 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[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 RpcRequestStatus.Submitted: case 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.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.requestContext.getId(this) || Guid.createValue(); this.setStatus(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.protocolVersion >= RpcProtocolVersion.IntroducedStatusCategory && this.responseProtocolVersion >= 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(RpcRequestStatus.Cancelled); } /* @internal */ async submit() { if (!this._active) return; this._lastSubmitted = new Date().getTime(); this._retryAfter = null; ++this._attempts; if (this.status === RpcRequestStatus.Created || this.status === RpcRequestStatus.NotFound || this.status === RpcRequestStatus.Cancelled) { this.setStatus(RpcRequestStatus.Submitted); } try { this._connecting = true; RpcRequest._activeRequests.set(this.id, this); this.protocol.events.raiseEvent(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 === RpcRequestStatus.Resolved && typeof (this._response) !== "undefined") { this._connecting = false; RpcRequest._activeRequests.delete(this.id); this.resolveRaw(); } else { this.protocol.events.raiseEvent(RpcProtocolEvent.ResponseLoading, this); if (status === RpcRequestStatus.Unknown) { this._connecting = false; RpcRequest._activeRequests.delete(this.id); this.handleUnknownResponse(response); return; } const value = await this.load(); this.protocol.events.raiseEvent(RpcProtocolEvent.ResponseLoaded, this); RpcRequest._activeRequests.delete(this.id); this._connecting = false; this.handleResponse(response, value); } } catch (err) { this.protocol.events.raiseEvent(RpcProtocolEvent.ConnectionErrorReceived, this, err); RpcRequest._activeRequests.delete(this.id); this._connecting = false; this.reject(err); } } handleUnknownResponse(code) { this.reject(new IModelError(BentleyStatus.ERROR, `Unknown response ${code}.`)); } handleResponse(code, value) { const protocolStatus = this.protocol.getStatus(code); const status = this.transformResponseStatus(protocolStatus, value); if (RpcRequestStatus.isTransientError(status)) { return this.handleTransientError(status); } switch (status) { case RpcRequestStatus.Resolved: { return this.handleResolved(value); } case RpcRequestStatus.Rejected: { return this.handleRejected(value); } case RpcRequestStatus.Pending: { return this.setPending(status, value.objects); } case RpcRequestStatus.NotFound: { return this.handleNotFound(status, value); } case RpcRequestStatus.NoContent: { return this.handleNoContent(); } } } transformResponseStatus(protocolStatus, value) { if (!this.supportsStatusCategory()) { return protocolStatus; } let status = protocolStatus; if (protocolStatus === RpcRequestStatus.Pending) { status = RpcRequestStatus.Rejected; } else if (protocolStatus === RpcRequestStatus.NotFound) { status = RpcRequestStatus.Rejected; } else if (protocolStatus === RpcRequestStatus.Unknown) { status = 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 = RpcRequestStatus.Pending; } else if (managedStatus.managedStatus === "notFound") { status = RpcRequestStatus.NotFound; } } return status; } handleResolved(value) { try { this._raw = value.objects; const result = 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(RpcProtocolEvent.BackendErrorReceived, this); try { const error = 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 : BentleyStatus.ERROR; return this.reject(new BackendError(errorNumber, name, message, () => error)); } catch (err) { return this.reject(err); } } handleNoContent() { return this.reject(new NoContentError()); } handleNotFound(status, value) { if (RpcRequest.notFoundHandlers.numberOfListeners === 0) this.handleRejected(value); const response = RpcMarshaling.deserialize(this.protocol, value); this.setStatus(status); let resubmitted = false; RpcRequest.notFoundHandlers.raiseEvent(this, response, () => { if (resubmitted) throw new IModelError(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(BentleyStatus.ERROR, "Cannot access raw response."); } this._resolveRaw(new ResponseLike(this._raw)); } this.setStatus(RpcRequestStatus.Resolved); this[Symbol.dispose](); } resolveRaw() { if (typeof (this._response) === "undefined") { throw new IModelError(BentleyStatus.ERROR, "Cannot access raw response."); } this._active = false; this.setLastUpdatedTime(); this._resolveRaw(this._response); this.setStatus(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(RpcRequestStatus.Rejected); this[Symbol.dispose](); } /** @internal */ [Symbol.dispose]() { this.setStatus(RpcRequestStatus.Disposed); this._raw = undefined; this._response = undefined; const client = this.client; if (client[CURRENT_REQUEST] === this) { client[CURRENT_REQUEST] = undefined; } } setPending(status, extendedStatus) { if (!this._active) return; this.setLastUpdatedTime(); this._extendedStatus = extendedStatus; this.setStatus(status); RpcRequest.events.raiseEvent(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(BentleyStatus.ERROR, `Exceeded transient fault limit.`)); } else { this.setStatus(status); RpcRequest.events.raiseEvent(RpcRequestEvent.TransientErrorReceived, this); } } async setHeaders() { const versionHeader = this.protocol.protocolVersionHeaderName; if (versionHeader && RpcProtocol.protocolVersion) this.setHeader(versionHeader, RpcProtocol.protocolVersion.toString()); const headerNames = this.protocol.serializedClientRequestContextHeaderNames; const headerValues = await 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(RpcRequestEvent.StatusChanged, this); } } /** @internal */ export const initializeRpcRequest = (() => { let initialized = false; return () => { if (initialized) { return; } initialized = true; RpcRequest.events.addListener((type, request) => { if (type !== RpcRequestEvent.StatusChanged) return; switch (request.status) { case RpcRequestStatus.Submitted: { aggregateLoad.lastRequest = request.lastSubmitted; break; } case RpcRequestStatus.Pending: case RpcRequestStatus.Resolved: case RpcRequestStatus.Rejected: { aggregateLoad.lastResponse = request.lastUpdated; break; } } }); RpcProtocol.events.addListener((type) => { const now = new Date().getTime(); switch (type) { case RpcProtocolEvent.RequestReceived: { aggregateLoad.lastRequest = now; break; } case RpcProtocolEvent.BackendReportedPending: case RpcProtocolEvent.BackendErrorOccurred: case RpcProtocolEvent.BackendResponseCreated: { aggregateLoad.lastResponse = now; break; } } }); }; })(); //# sourceMappingURL=RpcRequest.js.map