@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
403 lines • 20.4 kB
JavaScript
"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 RPC
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PresentationRpcImpl = exports.MAX_ALLOWED_KEYS_PAGE_SIZE = exports.MAX_ALLOWED_PAGE_SIZE = void 0;
const core_backend_1 = require("@itwin/core-backend");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const presentation_common_1 = require("@itwin/presentation-common");
const internal_1 = require("@itwin/presentation-common/internal");
// @ts-expect-error TS complains about `with` in CJS builds; The path is fine at runtime, but not at compile time
// eslint-disable-next-line @itwin/import-within-package
const package_json_1 = __importDefault(require("../../../package.json"));
const BackendLoggerCategory_js_1 = require("./BackendLoggerCategory.js");
const InternalSymbols_js_1 = require("./InternalSymbols.js");
const Presentation_js_1 = require("./Presentation.js");
const PresentationManagerDetail_js_1 = require("./PresentationManagerDetail.js");
const TemporaryStorage_js_1 = require("./TemporaryStorage.js");
const packageJsonVersion = package_json_1.default.version;
/** @internal */
exports.MAX_ALLOWED_PAGE_SIZE = 1000;
/** @internal */
exports.MAX_ALLOWED_KEYS_PAGE_SIZE = 10000;
const DEFAULT_REQUEST_TIMEOUT = 5000;
/**
* The backend implementation of PresentationRpcInterface. All it's basically
* responsible for is forwarding calls to [[Presentation.manager]].
*
* @internal
*/
class PresentationRpcImpl extends presentation_common_1.PresentationRpcInterface {
_requestTimeout;
_pendingRequests;
_cancelEvents;
_statusHandler;
constructor(props) {
super();
this._requestTimeout = props?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
this._pendingRequests = new TemporaryStorage_js_1.TemporaryStorage({
// remove the pending request after request timeout + 10 seconds - this gives
// frontend 10 seconds to re-send the request until it's removed from requests' cache
unusedValueLifetime: this._requestTimeout > 0 ? this._requestTimeout + 10 * 1000 : undefined,
// attempt to clean up every second
cleanupInterval: 1000,
cleanupHandler: (id, _, reason) => {
if (reason !== "request") {
core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Cleaning up request without frontend retrieving it: ${id}.`);
/* c8 ignore next */
this._cancelEvents.get(id)?.raiseEvent();
}
this._cancelEvents.delete(id);
},
});
this._cancelEvents = new Map();
this._statusHandler = createStatusCategoryHandler();
core_bentley_1.StatusCategory.handlers.add(this._statusHandler);
}
[Symbol.dispose]() {
this._pendingRequests[Symbol.dispose]();
core_bentley_1.StatusCategory.handlers.delete(this._statusHandler);
}
get requestTimeout() {
return this._requestTimeout;
}
get pendingRequests() {
return this._pendingRequests;
}
/** Returns an ok response with result inside */
successResponse(result, diagnostics) {
return {
statusCode: presentation_common_1.PresentationStatus.Success,
result,
diagnostics,
};
}
/** Returns a bad request response with empty result and an error code */
errorResponse(errorCode, errorMessage, diagnostics) {
return {
statusCode: errorCode,
result: undefined,
errorMessage,
diagnostics,
};
}
/**
* Get the [[PresentationManager]] used by this RPC impl.
*/
getManager(clientId) {
return Presentation_js_1.Presentation.getManager(clientId);
}
async getIModel(token) {
const imodel = core_backend_1.IModelDb.findByKey(token.key);
// call refreshContainer, just in case this is a V2 checkpoint whose sasToken is about to expire, or its default transaction is about to be restarted.
await imodel.refreshContainerForRpc(core_backend_1.RpcTrace.expectCurrentActivity.accessToken);
return imodel;
}
async makeRequest(token, requestId, requestOptions, request) {
const serializedRequestOptionsForLogging = JSON.stringify({
...(0, core_bentley_1.omit)(requestOptions, ["rulesetOrId"]),
...(requestOptions.rulesetOrId ? { rulesetId: (0, PresentationManagerDetail_js_1.getRulesetIdObject)(requestOptions.rulesetOrId).uniqueId } : undefined),
});
core_bentley_1.Logger.logInfo(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Received '${requestId}' request. Params: ${serializedRequestOptionsForLogging}`);
const imodel = await this.getIModel(token);
const requestKey = JSON.stringify({ iModelKey: token.key, requestId, requestOptions });
let resultPromise = this._pendingRequests.getValue(requestKey);
if (resultPromise) {
core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request already pending`);
}
else {
core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request not found, creating a new one`);
const { clientId: _, diagnostics: diagnosticsOptions, rulesetVariables, ...options } = requestOptions;
const managerRequestOptions = {
...options,
imodel,
cancelEvent: new core_bentley_1.BeEvent(),
};
// set up ruleset variables
if (rulesetVariables) {
managerRequestOptions.rulesetVariables = rulesetVariables.map(presentation_common_1.RulesetVariable.fromJSON);
}
// set up diagnostics listener
let diagnostics;
const getDiagnostics = () => {
if (!diagnostics) {
diagnostics = {};
}
return diagnostics;
};
if (diagnosticsOptions) {
if (diagnosticsOptions.backendVersion) {
getDiagnostics().backendVersion = packageJsonVersion;
}
managerRequestOptions.diagnostics = {
...diagnosticsOptions,
handler: (d) => {
if (d.logs) {
const target = getDiagnostics();
if (target.logs) {
target.logs.push(...d.logs);
}
else {
target.logs = [...d.logs];
}
}
},
};
}
// initiate request
resultPromise = request(managerRequestOptions).then((result) => this.successResponse(result, diagnostics));
// store the request promise
this._pendingRequests.addValue(requestKey, resultPromise);
this._cancelEvents.set(requestKey, managerRequestOptions.cancelEvent);
}
if (this._requestTimeout === 0) {
core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request timeout not configured, returning promise without a timeout.`);
void resultPromise.finally(() => {
this._pendingRequests.deleteValue(requestKey);
});
return resultPromise;
}
core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Returning a promise with a timeout of ${this._requestTimeout}.`);
const timeout = (0, internal_1.createCancellableTimeoutPromise)(this._requestTimeout);
return Promise.race([
resultPromise,
timeout.promise.then(() => {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw new core_common_1.RpcPendingResponse("Timeout");
}),
])
.then((response) => {
core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request completed, returning result.`);
this._pendingRequests.deleteValue(requestKey);
return response;
})
.finally(() => {
timeout.cancel();
});
}
async getNodesCount(token, requestOptions) {
return this.makeRequest(token, "getNodesCount", requestOptions, async (options) => {
return this.getManager(requestOptions.clientId).getNodesCount(options);
});
}
async getPagedNodes(token, requestOptions) {
return this.makeRequest(token, "getPagedNodes", requestOptions, async (options) => {
options = enforceValidPageSize(options);
const [serializedHierarchyLevel, count] = await Promise.all([
this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getNodes(options),
this.getManager(requestOptions.clientId).getNodesCount(options),
]);
const hierarchyLevel = (0, internal_1.deepReplaceNullsToUndefined)(JSON.parse(serializedHierarchyLevel));
return {
total: count,
items: hierarchyLevel.nodes,
};
});
}
async getNodesDescriptor(token, requestOptions) {
return this.makeRequest(token, "getNodesDescriptor", requestOptions, async (options) => {
return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getNodesDescriptor(options);
});
}
async getNodePaths(token, requestOptions) {
return this.makeRequest(token, "getNodePaths", requestOptions, async (options) => {
return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getNodePaths(options);
});
}
async getFilteredNodePaths(token, requestOptions) {
return this.makeRequest(token, "getFilteredNodePaths", requestOptions, async (options) => {
return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getFilteredNodePaths(options);
});
}
async getContentSources(token, requestOptions) {
return this.makeRequest(token, "getContentSources", requestOptions, async (options) => {
const result = await this.getManager(requestOptions.clientId).getContentSources(options);
const classesMap = {};
const selectClasses = result.map((sci) => presentation_common_1.SelectClassInfo.toCompressedJSON(sci, classesMap));
return { sources: selectClasses, classesMap };
});
}
async getContentDescriptor(token, requestOptions) {
return this.makeRequest(token, "getContentDescriptor", requestOptions, async (options) => {
options = {
...options,
contentFlags: (options.contentFlags ?? 0) | PresentationManagerDetail_js_1.DESCRIPTOR_ONLY_CONTENT_FLAG, // always append the "descriptor only" flag when handling request from the frontend
keys: presentation_common_1.KeySet.fromJSON(options.keys),
};
// Here we send a plain JSON string but we will parse it to DescriptorJSON on the frontend. This way we are
// bypassing unnecessary deserialization and serialization.
return Presentation_js_1.Presentation.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getContentDescriptor(options);
});
}
async getContentSetSize(token, requestOptions) {
return this.makeRequest(token, "getContentSetSize", requestOptions, async (options) => {
options = {
...options,
keys: presentation_common_1.KeySet.fromJSON(options.keys),
};
return this.getManager(requestOptions.clientId).getContentSetSize(options);
});
}
async getPagedContent(token, requestOptions) {
return this.makeRequest(token, "getPagedContent", requestOptions, async (options) => {
options = enforceValidPageSize({
...options,
keys: presentation_common_1.KeySet.fromJSON(options.keys),
});
const [size, content] = await Promise.all([
this.getManager(requestOptions.clientId).getContentSetSize(options),
this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getContent(options),
]);
if (!content) {
return undefined;
}
return {
descriptor: content.descriptor.toJSON(),
contentSet: {
total: size,
items: content.contentSet.map((i) => i.toJSON()),
},
};
});
}
async getPagedContentSet(token, requestOptions) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const response = await this.getPagedContent(token, requestOptions);
if (response.statusCode !== presentation_common_1.PresentationStatus.Success) {
return this.errorResponse(response.statusCode, response.errorMessage, response.diagnostics);
}
if (!response.result) {
return this.errorResponse(presentation_common_1.PresentationStatus.Error, `Failed to get content set (received a success response with empty result)`, response.diagnostics);
}
return {
...response,
result: response.result.contentSet,
};
}
async getElementProperties(token, requestOptions) {
return this.makeRequest(token, "getElementProperties", { ...requestOptions }, async (options) => {
const { clientId, ...restOptions } = options;
return this.getManager(clientId).getElementProperties(restOptions);
});
}
async getPagedDistinctValues(token, requestOptions) {
return this.makeRequest(token, "getPagedDistinctValues", requestOptions, async (options) => {
options = enforceValidPageSize({
...options,
keys: presentation_common_1.KeySet.fromJSON(options.keys),
});
return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getPagedDistinctValues(options);
});
}
async getContentInstanceKeys(token, requestOptions) {
return this.makeRequest(token, "getContentInstanceKeys", requestOptions, async (options) => {
const { displayType, ...optionsNoDisplayType } = options;
options = enforceValidPageSize({
...optionsNoDisplayType,
keys: presentation_common_1.KeySet.fromJSON(optionsNoDisplayType.keys),
descriptor: {
displayType,
contentFlags: presentation_common_1.ContentFlags.KeysOnly,
},
}, exports.MAX_ALLOWED_KEYS_PAGE_SIZE);
const [size, content] = await Promise.all([
this.getManager(requestOptions.clientId).getContentSetSize(options),
this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getContent(options),
]);
if (size === 0 || !content) {
return { total: 0, items: new presentation_common_1.KeySet().toJSON() };
}
return {
total: size,
items: content.contentSet.reduce((keys, item) => keys.add(item.primaryKeys), new presentation_common_1.KeySet()).toJSON(),
};
});
}
async getDisplayLabelDefinition(token, requestOptions) {
return this.makeRequest(token, "getDisplayLabelDefinition", requestOptions, async (options) => {
const label = await this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getDisplayLabelDefinition(options);
return label;
});
}
async getPagedDisplayLabelDefinitions(token, requestOptions) {
const pageOpts = enforceValidPageSize({ paging: { start: 0, size: requestOptions.keys.length } });
if (pageOpts.paging.size < requestOptions.keys.length) {
requestOptions.keys.splice(pageOpts.paging.size);
}
return this.makeRequest(token, "getPagedDisplayLabelDefinitions", requestOptions, async (options) => {
const labels = await this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getDisplayLabelDefinitions({
...options,
keys: options.keys,
});
return {
total: options.keys.length,
items: labels,
};
});
}
/* eslint-disable @typescript-eslint/no-deprecated */
async getSelectionScopes(token, requestOptions) {
return this.makeRequest(token, "getSelectionScopes", requestOptions, async (options) => this.getManager(requestOptions.clientId).getSelectionScopes(options));
}
async computeSelection(token, requestOptions) {
return this.makeRequest(token, "computeSelection", requestOptions, async (options) => {
const keys = await this.getManager(requestOptions.clientId).computeSelection(options);
return keys.toJSON();
});
}
}
exports.PresentationRpcImpl = PresentationRpcImpl;
const enforceValidPageSize = (requestOptions, maxPageSize = exports.MAX_ALLOWED_PAGE_SIZE) => {
const validPageSize = getValidPageSize(requestOptions.paging?.size, maxPageSize);
if (!requestOptions.paging || requestOptions.paging.size !== validPageSize) {
return { ...requestOptions, paging: { ...requestOptions.paging, size: validPageSize } };
}
return requestOptions;
};
const getValidPageSize = (size, maxPageSize) => {
const requestedSize = size ?? 0;
return requestedSize === 0 || requestedSize > maxPageSize ? maxPageSize : requestedSize;
};
// not testing temporary solution
/* c8 ignore start */
function createStatusCategoryHandler() {
return (e) => {
if (e instanceof presentation_common_1.PresentationError) {
switch (e.errorNumber) {
case presentation_common_1.PresentationStatus.NotInitialized:
return new (class extends core_bentley_1.ErrorCategory {
name = "Internal server error";
code = 500;
})();
case presentation_common_1.PresentationStatus.Canceled:
return new (class extends core_bentley_1.SuccessCategory {
name = "Cancelled";
code = 204;
})();
case presentation_common_1.PresentationStatus.ResultSetTooLarge:
return new (class extends core_bentley_1.ErrorCategory {
name = "Result set is too large";
code = 413;
})();
case presentation_common_1.PresentationStatus.Error:
case presentation_common_1.PresentationStatus.InvalidArgument:
return new (class extends core_bentley_1.ErrorCategory {
name = "Invalid request props";
code = 422;
})();
}
}
return undefined;
};
}
/* c8 ignore end */
//# sourceMappingURL=PresentationRpcImpl.js.map