@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
352 lines • 17.2 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 Core
*/
import { firstValueFrom } from "rxjs";
import { eachValueFrom } from "rxjs-for-await";
import { ContentFlags, DefaultContentDisplayTypes, Descriptor, KeySet, KoqPropertyValueFormatter, PresentationError, PresentationStatus, } from "@itwin/presentation-common";
import { buildElementProperties, ContentFormatter, ContentPropertyValueFormatter, deepReplaceNullsToUndefined, isSingleElementPropertiesRequestOptions, LocalizationHelper, } from "@itwin/presentation-common/internal";
import { getContentItemsObservableFromClassNames, getContentItemsObservableFromElementIds } from "./ElementPropertiesHelper.js";
import { NativePlatformRequestTypes } from "./NativePlatform.js";
import { getRulesetIdObject, PresentationManagerDetail } from "./PresentationManagerDetail.js";
import { RulesetVariablesManagerImpl } from "./RulesetVariablesManager.js";
import { SelectionScopesHelper } from "./SelectionScopesHelper.js";
import { getLocalizedStringEN } from "./Utils.js";
import { _presentation_manager_detail } from "./InternalSymbols.js";
/**
* Presentation hierarchy cache mode.
* @public
*/
export 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 || (HierarchyCacheMode = {}));
/**
* Backend Presentation manager which pulls the presentation data from
* an iModel using native platform.
*
* @public
*/
export class PresentationManager {
_props;
_detail;
_localizationHelper;
/**
* Creates an instance of PresentationManager.
* @param props Optional configuration properties.
*/
constructor(props) {
this._props = props ?? {};
this._detail = new PresentationManagerDetail(this._props);
this._localizationHelper = new LocalizationHelper({ getLocalizedString: props?.getLocalizedString ?? 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 RulesetVariablesManagerImpl(() => this._detail.getNativePlatform(), rulesetId);
}
/** @internal */
/* c8 ignore next 3 */
get [_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 = 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 = 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 = 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 KoqPropertyValueFormatter(this.props.schemaContextProvider(requestOptions.imodel), this.props.defaultFormats);
const formatter = new ContentFormatter(new 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 KoqPropertyValueFormatter(this.props.schemaContextProvider(requestOptions.imodel), this.props.defaultFormats);
const formatter = new ContentFormatter(new 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 (isSingleElementPropertiesRequestOptions(requestOptions)) {
return this.getSingleElementProperties(requestOptions);
}
return this.getMultipleElementProperties(requestOptions);
}
async getSingleElementProperties(requestOptions) {
const { elementId, contentParser, ...optionsNoElementId } = requestOptions;
const parser = contentParser ?? buildElementProperties;
const content = await this.getContent({
...optionsNoElementId,
descriptor: {
displayType: DefaultContentDisplayTypes.PropertyPane,
contentFlags: ContentFlags.ShowLabels,
},
rulesetOrId: "ElementProperties",
keys: new 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 ?? 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: DefaultContentDisplayTypes.Grid, contentFlags: ContentFlags.ShowLabels, ...partialProps });
const contentSetGetter = async (partialProps) => this.getContentSet({ ...contentOptions, ...partialProps });
const { itemBatches, count } = "elementIds" in elementsIdentifier
? getContentItemsObservableFromElementIds(requestOptions.imodel, descriptorGetter, contentSetGetter, elementsIdentifier.elementIds, classParallelism, batchesParallelism, batchSize)
: getContentItemsObservableFromClassNames(requestOptions.imodel, descriptorGetter, contentSetGetter, elementsIdentifier.elementClasses, classParallelism, batchesParallelism, batchSize);
return {
total: await firstValueFrom(count),
async *iterator() {
for await (const itemsBatch of 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.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.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 = getRulesetIdObject(requestOptions.rulesetOrId);
const prevRulesetId = prev.rulesetOrId ? getRulesetIdObject(prev.rulesetOrId) : currRulesetId;
if (prevRulesetId.parts.id !== currRulesetId.parts.id) {
throw new PresentationError(PresentationStatus.InvalidArgument, "Can't compare rulesets with different IDs");
}
const currRulesetVariables = rulesetVariables ?? [];
const prevRulesetVariables = prev.rulesetVariables ?? currRulesetVariables;
const params = {
requestId: 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));
}
}
//# sourceMappingURL=PresentationManager.js.map