@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
356 lines • 17.8 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 Core
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PresentationManager = exports.HierarchyCacheMode = void 0;
const rxjs_1 = require("rxjs");
const rxjs_for_await_1 = require("rxjs-for-await");
const presentation_common_1 = require("@itwin/presentation-common");
const internal_1 = require("@itwin/presentation-common/internal");
const ElementPropertiesHelper_js_1 = require("./ElementPropertiesHelper.js");
const NativePlatform_js_1 = require("./NativePlatform.js");
const PresentationManagerDetail_js_1 = require("./PresentationManagerDetail.js");
const RulesetVariablesManager_js_1 = require("./RulesetVariablesManager.js");
const SelectionScopesHelper_js_1 = require("./SelectionScopesHelper.js");
const Utils_js_1 = require("./Utils.js");
const InternalSymbols_js_1 = require("./InternalSymbols.js");
/**
* Presentation hierarchy cache mode.
* @public
*/
var HierarchyCacheMode;
(function (HierarchyCacheMode) {
/**
* Hierarchy cache is created in memory.
*/
HierarchyCacheMode["Memory"] = "memory";
/**
* Hierarchy cache is created on disk. In this mode hierarchy cache is persisted between iModel
* openings.
*/
HierarchyCacheMode["Disk"] = "disk";
/**
* Hierarchy cache is created on disk. In this mode everything is cached in memory while creating hierarchy level
* and persisted in disk cache when whole hierarchy level is created.
*
* **Note:** This mode is still experimental.
*/
HierarchyCacheMode["Hybrid"] = "hybrid";
})(HierarchyCacheMode || (exports.HierarchyCacheMode = HierarchyCacheMode = {}));
/**
* Backend Presentation manager which pulls the presentation data from
* an iModel using native platform.
*
* @public
*/
class PresentationManager {
_props;
_detail;
_localizationHelper;
/**
* Creates an instance of PresentationManager.
* @param props Optional configuration properties.
*/
constructor(props) {
this._props = props ?? {};
this._detail = new PresentationManagerDetail_js_1.PresentationManagerDetail(this._props);
this._localizationHelper = new internal_1.LocalizationHelper({ getLocalizedString: props?.getLocalizedString ?? Utils_js_1.getLocalizedStringEN });
}
/** Get / set active unit system used to format property values with units */
get activeUnitSystem() {
return this._detail.activeUnitSystem;
}
/* c8 ignore next 3 */
set activeUnitSystem(value) {
this._detail.activeUnitSystem = value;
}
/** Dispose the presentation manager. Must be called to clean up native resources. */
[Symbol.dispose]() {
this._detail[Symbol.dispose]();
}
/** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
/* c8 ignore next 3 */
dispose() {
this[Symbol.dispose]();
}
/** An event, that this manager raises whenever any request is made on it. */
get onUsed() {
return this._detail.onUsed;
}
/** Properties used to initialize the manager */
get props() {
return this._props;
}
/** Get rulesets manager */
rulesets() {
return this._detail.rulesets;
}
/**
* Get ruleset variables manager for specific ruleset
* @param rulesetId Id of the ruleset to get variables manager for
*/
vars(rulesetId) {
return new RulesetVariablesManager_js_1.RulesetVariablesManagerImpl(() => this._detail.getNativePlatform(), rulesetId);
}
/** @internal */
/* c8 ignore next 3 */
get [InternalSymbols_js_1._presentation_manager_detail]() {
return this._detail;
}
getRulesetId(rulesetOrId) {
return this._detail.getRulesetId(rulesetOrId);
}
/**
* Retrieves nodes
* @public
*/
async getNodes(requestOptions) {
const serializedHierarchyLevel = await this._detail.getNodes(requestOptions);
const hierarchyLevel = (0, internal_1.deepReplaceNullsToUndefined)(JSON.parse(serializedHierarchyLevel));
return this._localizationHelper.getLocalizedNodes(hierarchyLevel.nodes);
}
/**
* Retrieves nodes count
* @public
*/
async getNodesCount(requestOptions) {
return this._detail.getNodesCount(requestOptions);
}
/**
* Retrieves hierarchy level descriptor
* @public
*/
async getNodesDescriptor(requestOptions) {
const response = await this._detail.getNodesDescriptor(requestOptions);
const descriptor = presentation_common_1.Descriptor.fromJSON(JSON.parse(response));
return descriptor ? this._localizationHelper.getLocalizedContentDescriptor(descriptor) : undefined;
}
/**
* Retrieves paths from root nodes to children nodes according to specified instance key paths. Intersecting paths will be merged.
* TODO: Return results in pages
* @public
*/
async getNodePaths(requestOptions) {
const result = await this._detail.getNodePaths(requestOptions);
return result.map((npe) => this._localizationHelper.getLocalizedNodePathElement(npe));
}
/**
* Retrieves paths from root nodes to nodes containing filter text in their label.
* TODO: Return results in pages
* @public
*/
async getFilteredNodePaths(requestOptions) {
const result = await this._detail.getFilteredNodePaths(requestOptions);
return result.map((npe) => this._localizationHelper.getLocalizedNodePathElement(npe));
}
/**
* Get information about the sources of content when building it for specific ECClasses. Sources involve classes of the primary select instance,
* its related instances for loading related and navigation properties.
* @public
*/
async getContentSources(requestOptions) {
return this._detail.getContentSources(requestOptions);
}
/**
* Retrieves the content descriptor which can be used to get content
* @public
*/
async getContentDescriptor(requestOptions) {
const response = await this._detail.getContentDescriptor(requestOptions);
const descriptor = presentation_common_1.Descriptor.fromJSON(JSON.parse(response));
return descriptor ? this._localizationHelper.getLocalizedContentDescriptor(descriptor) : undefined;
}
/**
* Retrieves the content set size based on the supplied content descriptor override
* @public
*/
async getContentSetSize(requestOptions) {
return this._detail.getContentSetSize(requestOptions);
}
/**
* Retrieves the content set based on the supplied content descriptor.
* @public
*/
async getContentSet(requestOptions) {
let items = await this._detail.getContentSet({
...requestOptions,
...(!requestOptions.omitFormattedValues && this.props.schemaContextProvider !== undefined ? { omitFormattedValues: true } : undefined),
});
if (!requestOptions.omitFormattedValues && this.props.schemaContextProvider !== undefined) {
const koqPropertyFormatter = new presentation_common_1.KoqPropertyValueFormatter(this.props.schemaContextProvider(requestOptions.imodel), this.props.defaultFormats);
const formatter = new internal_1.ContentFormatter(new internal_1.ContentPropertyValueFormatter(koqPropertyFormatter), requestOptions.unitSystem ?? this.props.defaultUnitSystem);
items = await formatter.formatContentItems(items, requestOptions.descriptor);
}
return this._localizationHelper.getLocalizedContentItems(items);
}
/**
* Retrieves the content based on the supplied content descriptor override.
* @public
*/
async getContent(requestOptions) {
const content = await this._detail.getContent({
...requestOptions,
...(!requestOptions.omitFormattedValues && this.props.schemaContextProvider !== undefined ? { omitFormattedValues: true } : undefined),
});
if (!content) {
return undefined;
}
if (!requestOptions.omitFormattedValues && this.props.schemaContextProvider !== undefined) {
const koqPropertyFormatter = new presentation_common_1.KoqPropertyValueFormatter(this.props.schemaContextProvider(requestOptions.imodel), this.props.defaultFormats);
const formatter = new internal_1.ContentFormatter(new internal_1.ContentPropertyValueFormatter(koqPropertyFormatter), requestOptions.unitSystem ?? this.props.defaultUnitSystem);
await formatter.formatContent(content);
}
return this._localizationHelper.getLocalizedContent(content);
}
/**
* Retrieves distinct values of specific field from the content based on the supplied content descriptor override.
* @param requestOptions Options for the request
* @return A promise object that returns either distinct values on success or an error string on error.
* @public
*/
async getPagedDistinctValues(requestOptions) {
const result = await this._detail.getPagedDistinctValues(requestOptions);
return {
...result,
items: result.items.map((g) => this._localizationHelper.getLocalizedDisplayValueGroup(g)),
};
}
async getElementProperties(requestOptions) {
if ((0, internal_1.isSingleElementPropertiesRequestOptions)(requestOptions)) {
return this.getSingleElementProperties(requestOptions);
}
return this.getMultipleElementProperties(requestOptions);
}
async getSingleElementProperties(requestOptions) {
const { elementId, contentParser, ...optionsNoElementId } = requestOptions;
const parser = contentParser ?? internal_1.buildElementProperties;
const content = await this.getContent({
...optionsNoElementId,
descriptor: {
displayType: presentation_common_1.DefaultContentDisplayTypes.PropertyPane,
contentFlags: presentation_common_1.ContentFlags.ShowLabels,
},
rulesetOrId: "ElementProperties",
keys: new presentation_common_1.KeySet([{ className: "BisCore:Element", id: elementId }]),
});
if (!content || content.contentSet.length === 0) {
return undefined;
}
return parser(content.descriptor, content.contentSet[0]);
}
async getMultipleElementProperties(requestOptions) {
const { contentParser, batchSize: batchSizeOption, ...contentOptions } = requestOptions;
const parser = contentParser ?? internal_1.buildElementProperties;
const workerThreadsCount = this._props.workerThreadsCount ?? 2;
// We don't want to request content for all classes at once - each class results in a huge content descriptor object that's cached in memory
// and can be shared across all batch requests for that class. Handling multiple classes at the same time not only increases memory footprint,
// but also may push descriptors out of cache, requiring us to recreate them, thus making performance worse. For those reasons we handle at
// most `workerThreadsCount / 2` classes in parallel.
/* c8 ignore next */
const classParallelism = workerThreadsCount > 1 ? Math.ceil(workerThreadsCount / 2) : 1;
// We want all worker threads to be constantly busy. However, there's some fairly expensive work being done after the worker thread is done,
// but before we receive the response. That means the worker thread would be starving if we sent only `workerThreadsCount` requests in parallel.
// To avoid that, we keep twice as much requests active.
/* c8 ignore next */
const batchesParallelism = workerThreadsCount > 0 ? workerThreadsCount : 1;
/* c8 ignore next */
const batchSize = batchSizeOption ?? 100;
const elementsIdentifier = (() => {
if ("elementIds" in contentOptions && contentOptions.elementIds !== undefined) {
const elementIds = contentOptions.elementIds;
delete contentOptions.elementIds;
return { elementIds };
}
if ("elementClasses" in contentOptions && contentOptions.elementClasses !== undefined) {
const elementClasses = contentOptions.elementClasses;
delete contentOptions.elementClasses;
return { elementClasses };
}
/* c8 ignore next */
return { elementClasses: ["BisCore:Element"] };
})();
const descriptorGetter = async (partialProps) => this.getContentDescriptor({ ...contentOptions, displayType: presentation_common_1.DefaultContentDisplayTypes.Grid, contentFlags: presentation_common_1.ContentFlags.ShowLabels, ...partialProps });
const contentSetGetter = async (partialProps) => this.getContentSet({ ...contentOptions, ...partialProps });
const { itemBatches, count } = "elementIds" in elementsIdentifier
? (0, ElementPropertiesHelper_js_1.getContentItemsObservableFromElementIds)(requestOptions.imodel, descriptorGetter, contentSetGetter, elementsIdentifier.elementIds, classParallelism, batchesParallelism, batchSize)
: (0, ElementPropertiesHelper_js_1.getContentItemsObservableFromClassNames)(requestOptions.imodel, descriptorGetter, contentSetGetter, elementsIdentifier.elementClasses, classParallelism, batchesParallelism, batchSize);
return {
total: await (0, rxjs_1.firstValueFrom)(count),
async *iterator() {
for await (const itemsBatch of (0, rxjs_for_await_1.eachValueFrom)(itemBatches)) {
const { descriptor, items } = itemsBatch;
yield items.map((item) => parser(descriptor, item));
}
},
};
}
/**
* Retrieves display label definition of specific item
* @public
*/
async getDisplayLabelDefinition(requestOptions) {
const labelDefinition = await this._detail.getDisplayLabelDefinition(requestOptions);
return this._localizationHelper.getLocalizedLabelDefinition(labelDefinition);
}
/**
* Retrieves display label definitions of specific items
* @public
*/
async getDisplayLabelDefinitions(requestOptions) {
const labelDefinitions = await this._detail.getDisplayLabelDefinitions(requestOptions);
return this._localizationHelper.getLocalizedLabelDefinitions(labelDefinitions);
}
/**
* Retrieves available selection scopes.
* @public
* @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `computeSelection` from [@itwin/unified-selection](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#selection-scopes) package instead.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
async getSelectionScopes(_requestOptions) {
return SelectionScopesHelper_js_1.SelectionScopesHelper.getSelectionScopes();
}
/**
* Computes selection based on provided element IDs and selection scope.
* @public
* @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `computeSelection` from [@itwin/unified-selection](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#selection-scopes) package instead.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
async computeSelection(requestOptions) {
return SelectionScopesHelper_js_1.SelectionScopesHelper.computeSelection(requestOptions);
}
/**
* Compares two hierarchies specified in the request options
* @public
*/
async compareHierarchies(requestOptions) {
if (!requestOptions.prev.rulesetOrId && !requestOptions.prev.rulesetVariables) {
return { changes: [] };
}
const { rulesetOrId, prev, rulesetVariables, ...options } = requestOptions;
this._detail.registerRuleset(rulesetOrId);
const currRulesetId = (0, PresentationManagerDetail_js_1.getRulesetIdObject)(requestOptions.rulesetOrId);
const prevRulesetId = prev.rulesetOrId ? (0, PresentationManagerDetail_js_1.getRulesetIdObject)(prev.rulesetOrId) : currRulesetId;
if (prevRulesetId.parts.id !== currRulesetId.parts.id) {
throw new presentation_common_1.PresentationError(presentation_common_1.PresentationStatus.InvalidArgument, "Can't compare rulesets with different IDs");
}
const currRulesetVariables = rulesetVariables ?? [];
const prevRulesetVariables = prev.rulesetVariables ?? currRulesetVariables;
const params = {
requestId: NativePlatform_js_1.NativePlatformRequestTypes.CompareHierarchies,
...options,
prevRulesetId: prevRulesetId.uniqueId,
currRulesetId: currRulesetId.uniqueId,
prevRulesetVariables: JSON.stringify(prevRulesetVariables),
currRulesetVariables: JSON.stringify(currRulesetVariables),
expandedNodeKeys: JSON.stringify(options.expandedNodeKeys ?? []),
};
return JSON.parse(await this._detail.request(params));
}
}
exports.PresentationManager = PresentationManager;
//# sourceMappingURL=PresentationManager.js.map