@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
447 lines • 19.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/ban-ts-comment */
import * as hash from "object-hash";
import * as path from "path";
import { IModelDb, IpcHost } from "@itwin/core-backend";
import { BeEvent, Logger } from "@itwin/core-bentley";
import { Content, ContentFlags, DefaultContentDisplayTypes, Descriptor, InstanceKey, Item, Key, KeySet, SelectClassInfo, } from "@itwin/presentation-common";
import { deepReplaceNullsToUndefined, PresentationIpcEvents } from "@itwin/presentation-common/internal";
import { normalizeFullClassName } from "@itwin/presentation-shared";
import { PresentationBackendLoggerCategory } from "./BackendLoggerCategory.js";
import { createDefaultNativePlatform, NativePlatformRequestTypes, NativePresentationUnitSystem, PresentationNativePlatformResponseError, } from "./NativePlatform.js";
import { HierarchyCacheMode } from "./PresentationManager.js";
// @ts-ignore TS complains about `with` in CJS builds, but not ESM
import elementPropertiesRuleset from "./primary-presentation-rules/ElementProperties.PresentationRuleSet.json" with { type: "json" };
import { RulesetManagerImpl } from "./RulesetManager.js";
// @ts-ignore TS complains about `with` in CJS builds, but not ESM
import bisSupplementalRuleset from "./supplemental-presentation-rules/BisCore.PresentationRuleSet.json" with { type: "json" };
// @ts-ignore TS complains about `with` in CJS builds, but not ESM
import funcSupplementalRuleset from "./supplemental-presentation-rules/Functional.PresentationRuleSet.json" with { type: "json" };
import { combineDiagnosticsOptions, getElementKey, reportDiagnostics } from "./Utils.js";
/**
* Produce content descriptor that is not intended for querying content. Allows the implementation to omit certain
* operations to make obtaining content descriptor faster.
* @internal
*/
export const DESCRIPTOR_ONLY_CONTENT_FLAG = 1 << 9;
/** @internal */
export class PresentationManagerDetail {
_disposed;
_nativePlatform;
_diagnosticsOptions;
rulesets;
activeUnitSystem;
onUsed;
constructor(params) {
this._disposed = false;
this._nativePlatform =
params.addon ??
createNativePlatform(params.id ?? "", params.workerThreadsCount ?? 2, IpcHost.isValid ? ipcUpdatesHandler : noopUpdatesHandler, params.caching, params.defaultFormats, params.useMmap);
setupRulesets(this._nativePlatform, params.supplementalRulesetDirectories ?? [], params.rulesetDirectories ?? []);
this.activeUnitSystem = params.defaultUnitSystem;
this.onUsed = new BeEvent();
this.rulesets = new RulesetManagerImpl(() => this.getNativePlatform());
this._diagnosticsOptions = params.diagnostics;
}
[Symbol.dispose]() {
if (this._disposed) {
return;
}
this.getNativePlatform()[Symbol.dispose]();
this._nativePlatform = undefined;
this.onUsed.clear();
this._disposed = true;
}
getNativePlatform() {
if (this._disposed) {
throw new Error("Attempting to use Presentation manager after disposal");
}
return this._nativePlatform;
}
async getNodes(requestOptions) {
const { rulesetOrId, parentKey, ...strippedOptions } = requestOptions;
const params = {
requestId: parentKey ? NativePlatformRequestTypes.GetChildren : NativePlatformRequestTypes.GetRootNodes,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
nodeKey: parentKey,
};
return this.request(params);
}
async getNodesCount(requestOptions) {
const { rulesetOrId, parentKey, ...strippedOptions } = requestOptions;
const params = {
requestId: parentKey ? NativePlatformRequestTypes.GetChildrenCount : NativePlatformRequestTypes.GetRootNodesCount,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
nodeKey: parentKey,
};
return JSON.parse(await this.request(params));
}
async getNodesDescriptor(requestOptions) {
const { rulesetOrId, parentKey, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetNodesDescriptor,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
nodeKey: parentKey,
};
return this.request(params);
}
async getNodePaths(requestOptions) {
const { rulesetOrId, instancePaths, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetNodePaths,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
paths: instancePaths,
};
const paths = deepReplaceNullsToUndefined(JSON.parse(await this.request(params)));
return paths;
}
async getFilteredNodePaths(requestOptions) {
const { rulesetOrId, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetFilteredNodePaths,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
};
const paths = deepReplaceNullsToUndefined(JSON.parse(await this.request(params)));
return paths;
}
async getContentDescriptor(requestOptions) {
const { rulesetOrId, contentFlags, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetContentDescriptor,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
contentFlags: contentFlags ?? DESCRIPTOR_ONLY_CONTENT_FLAG, // only set "descriptor only" flag if there are no flags provided
keys: getKeysForContentRequest(requestOptions.keys, (map) => bisElementInstanceKeysProcessor(requestOptions.imodel, map)),
};
return this.request(params);
}
async getContentSources(requestOptions) {
const params = {
requestId: NativePlatformRequestTypes.GetContentSources,
rulesetId: "ElementProperties",
...requestOptions,
};
const json = JSON.parse(await this.request(params));
return deepReplaceNullsToUndefined(json.sources.map((sourceJson) => SelectClassInfo.fromCompressedJSON(sourceJson, json.classesMap)));
}
async getContentSetSize(requestOptions) {
const { rulesetOrId, descriptor, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetContentSetSize,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
keys: getKeysForContentRequest(requestOptions.keys, (map) => bisElementInstanceKeysProcessor(requestOptions.imodel, map)),
descriptorOverrides: createContentDescriptorOverrides(descriptor),
};
return JSON.parse(await this.request(params));
}
async getContentSet(requestOptions) {
const { rulesetOrId, descriptor, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetContentSet,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
keys: getKeysForContentRequest(requestOptions.keys, (map) => bisElementInstanceKeysProcessor(requestOptions.imodel, map)),
descriptorOverrides: createContentDescriptorOverrides(descriptor),
};
return JSON.parse(await this.request(params))
.map((json) => Item.fromJSON(deepReplaceNullsToUndefined(json)))
.filter((item) => !!item);
}
async getContent(requestOptions) {
const { rulesetOrId, descriptor, ...strippedOptions } = requestOptions;
const params = {
requestId: NativePlatformRequestTypes.GetContent,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptions,
keys: getKeysForContentRequest(requestOptions.keys, (map) => bisElementInstanceKeysProcessor(requestOptions.imodel, map)),
descriptorOverrides: createContentDescriptorOverrides(descriptor),
};
return Content.fromJSON(deepReplaceNullsToUndefined(JSON.parse(await this.request(params))));
}
async getPagedDistinctValues(requestOptions) {
const { rulesetOrId, ...strippedOptions } = requestOptions;
const { descriptor, keys, ...strippedOptionsNoDescriptorAndKeys } = strippedOptions;
const params = {
requestId: NativePlatformRequestTypes.GetPagedDistinctValues,
rulesetId: this.registerRuleset(rulesetOrId),
...strippedOptionsNoDescriptorAndKeys,
keys: getKeysForContentRequest(keys, (map) => bisElementInstanceKeysProcessor(requestOptions.imodel, map)),
descriptorOverrides: createContentDescriptorOverrides(descriptor),
};
return deepReplaceNullsToUndefined(JSON.parse(await this.request(params)));
}
async getDisplayLabelDefinition(requestOptions) {
const params = {
requestId: NativePlatformRequestTypes.GetDisplayLabel,
...requestOptions,
};
return deepReplaceNullsToUndefined(JSON.parse(await this.request(params)));
}
async getDisplayLabelDefinitions(requestOptions) {
const concreteKeys = requestOptions.keys
.map((k) => {
if (normalizeFullClassName(k.className).toLowerCase() === "BisCore.Element".toLowerCase()) {
return getElementKey(requestOptions.imodel, k.id);
}
return k;
})
.filter((k) => !!k);
const contentRequestOptions = {
...requestOptions,
rulesetOrId: "RulesDrivenECPresentationManager_RulesetId_DisplayLabel",
descriptor: {
displayType: DefaultContentDisplayTypes.List,
contentFlags: ContentFlags.ShowLabels | ContentFlags.NoFields,
},
keys: new KeySet(concreteKeys),
};
const content = await this.getContent(contentRequestOptions);
return concreteKeys.map((key) => {
const item = content ? content.contentSet.find((it) => it.primaryKeys.length > 0 && InstanceKey.compare(it.primaryKeys[0], key) === 0) : undefined;
if (!item) {
return { displayValue: "", rawValue: "", typeName: "" };
}
return item.label;
});
}
/** Registers given ruleset and replaces the ruleset with its ID in the resulting object */
registerRuleset(rulesetOrId) {
if (typeof rulesetOrId === "object") {
const rulesetWithNativeId = { ...rulesetOrId, id: this.getRulesetId(rulesetOrId) };
return this.rulesets.add(rulesetWithNativeId).id;
}
return rulesetOrId;
}
/** @internal */
getRulesetId(rulesetOrId) {
return getRulesetIdObject(rulesetOrId).uniqueId;
}
async request(params) {
this.onUsed.raiseEvent();
const { requestId, imodel, unitSystem, diagnostics: requestDiagnostics, cancelEvent, ...strippedParams } = params;
const imodelAddon = this.getNativePlatform().getImodelAddon(imodel);
const response = await withOptionalDiagnostics([this._diagnosticsOptions, requestDiagnostics], async (diagnosticsOptions) => {
const nativeRequestParams = {
requestId,
params: {
unitSystem: toOptionalNativeUnitSystem(unitSystem ?? this.activeUnitSystem),
...strippedParams,
...(diagnosticsOptions ? { diagnostics: diagnosticsOptions } : undefined),
},
};
return this.getNativePlatform().handleRequest(imodelAddon, JSON.stringify(nativeRequestParams), cancelEvent);
});
return response.result;
}
}
async function withOptionalDiagnostics(diagnosticsOptions, nativePlatformRequestHandler) {
const contexts = diagnosticsOptions.map((d) => d?.requestContextSupplier?.());
const combinedOptions = combineDiagnosticsOptions(...diagnosticsOptions);
let responseDiagnostics;
try {
const response = await nativePlatformRequestHandler(combinedOptions);
responseDiagnostics = response.diagnostics;
return response;
}
catch (e) {
if (e instanceof PresentationNativePlatformResponseError) {
responseDiagnostics = e.diagnostics;
}
throw e;
/* c8 ignore next */
}
finally {
if (responseDiagnostics) {
const diagnostics = { logs: [responseDiagnostics] };
diagnosticsOptions.forEach((options, i) => {
options && reportDiagnostics(diagnostics, options, contexts[i]);
});
}
}
}
function setupRulesets(nativePlatform, supplementalRulesetDirectories, primaryRulesetDirectories) {
nativePlatform.addRuleset(JSON.stringify(elementPropertiesRuleset));
nativePlatform.registerSupplementalRuleset(JSON.stringify(bisSupplementalRuleset));
nativePlatform.registerSupplementalRuleset(JSON.stringify(funcSupplementalRuleset));
nativePlatform.setupSupplementalRulesetDirectories(collateAssetDirectories(supplementalRulesetDirectories));
nativePlatform.setupRulesetDirectories(collateAssetDirectories(primaryRulesetDirectories));
}
/** @internal */
export function getRulesetIdObject(rulesetOrId) {
if (typeof rulesetOrId === "object") {
if (IpcHost.isValid) {
// in case of native apps we don't want to enforce ruleset id uniqueness as ruleset variables
// are stored on a backend and creating new id will lose those variables
return {
uniqueId: rulesetOrId.id,
parts: { id: rulesetOrId.id },
};
}
const hashedId = hash.MD5(rulesetOrId);
return {
uniqueId: `${rulesetOrId.id}-${hashedId}`,
parts: {
id: rulesetOrId.id,
hash: hashedId,
},
};
}
return { uniqueId: rulesetOrId, parts: { id: rulesetOrId } };
}
/** @internal */
export function getKeysForContentRequest(keys, classInstanceKeysProcessor) {
const result = {
instanceKeys: [],
nodeKeys: [],
};
const classInstancesMap = new Map();
keys.forEach((key) => {
if (Key.isNodeKey(key)) {
result.nodeKeys.push(key);
}
if (Key.isInstanceKey(key)) {
addInstanceKey(classInstancesMap, key);
}
});
if (classInstanceKeysProcessor) {
classInstanceKeysProcessor(classInstancesMap);
}
for (const entry of classInstancesMap) {
if (entry[1].size > 0) {
result.instanceKeys.push([entry["0"], [...entry[1]]]);
}
}
return result;
}
/** @internal */
export function bisElementInstanceKeysProcessor(imodel, classInstancesMap) {
const elementClassName = "BisCore:Element";
const elementIds = classInstancesMap.get(elementClassName);
if (elementIds) {
const deleteElementIds = new Array();
elementIds.forEach((elementId) => {
const concreteKey = getElementKey(imodel, elementId);
if (concreteKey && concreteKey.className !== elementClassName) {
deleteElementIds.push(elementId);
addInstanceKey(classInstancesMap, { className: concreteKey.className, id: elementId });
}
});
for (const id of deleteElementIds) {
elementIds.delete(id);
}
}
}
function addInstanceKey(classInstancesMap, key) {
let set = classInstancesMap.get(key.className);
if (!set) {
set = new Set();
classInstancesMap.set(key.className, set);
}
set.add(key.id);
}
function createNativePlatform(id, workerThreadsCount, updateCallback, caching, defaultFormats, useMmap) {
return new (createDefaultNativePlatform({
id,
taskAllocationsMap: { [Number.MAX_SAFE_INTEGER]: workerThreadsCount },
updateCallback,
cacheConfig: createCacheConfig(caching?.hierarchies),
contentCacheSize: caching?.content?.size,
workerConnectionCacheSize: caching?.workerConnectionCacheSize,
defaultFormats: toNativeUnitFormatsMap(defaultFormats),
useMmap,
}))();
function createCacheConfig(config) {
switch (config?.mode) {
case HierarchyCacheMode.Disk:
return { ...config, directory: normalizeDirectory(config.directory) };
case HierarchyCacheMode.Hybrid:
return {
...config,
disk: config.disk ? { ...config.disk, directory: normalizeDirectory(config.disk.directory) } : undefined,
};
case HierarchyCacheMode.Memory:
return config;
default:
return { mode: HierarchyCacheMode.Disk, directory: "" };
}
}
function normalizeDirectory(directory) {
return directory ? path.resolve(directory) : "";
}
function toNativeUnitFormatsMap(map) {
if (!map) {
return undefined;
}
const nativeFormatsMap = {};
Object.entries(map).forEach(([phenomenon, formats]) => {
nativeFormatsMap[phenomenon] = (Array.isArray(formats) ? formats : [formats]).map((unitSystemsFormat) => ({
unitSystems: unitSystemsFormat.unitSystems.map(toNativeUnitSystem),
format: unitSystemsFormat.format,
}));
});
return nativeFormatsMap;
}
}
function toOptionalNativeUnitSystem(unitSystem) {
return unitSystem ? toNativeUnitSystem(unitSystem) : undefined;
}
function toNativeUnitSystem(unitSystem) {
switch (unitSystem) {
case "imperial":
return NativePresentationUnitSystem.BritishImperial;
case "metric":
return NativePresentationUnitSystem.Metric;
case "usCustomary":
return NativePresentationUnitSystem.UsCustomary;
case "usSurvey":
return NativePresentationUnitSystem.UsSurvey;
}
}
function collateAssetDirectories(dirs) {
return [...new Set(dirs)];
}
const createContentDescriptorOverrides = (descriptorOrOverrides) => {
if (descriptorOrOverrides instanceof Descriptor) {
return descriptorOrOverrides.createDescriptorOverrides();
}
return descriptorOrOverrides;
};
function parseUpdateInfo(info) {
if (info === undefined) {
return undefined;
}
const parsedInfo = {};
for (const fileName in info) {
/* c8 ignore next 3 */
if (!info.hasOwnProperty(fileName)) {
continue;
}
const imodelDb = IModelDb.findByFilename(fileName);
if (!imodelDb) {
Logger.logError(PresentationBackendLoggerCategory.PresentationManager, `Update records IModelDb not found with path ${fileName}`);
continue;
}
parsedInfo[imodelDb.getRpcProps().key] = info[fileName];
}
return Object.keys(parsedInfo).length > 0 ? parsedInfo : undefined;
}
/** @internal */
export function ipcUpdatesHandler(info) {
const parsed = parseUpdateInfo(info);
if (parsed) {
IpcHost.send(PresentationIpcEvents.Update, parsed);
}
}
/** @internal */
/* c8 ignore next */
export function noopUpdatesHandler(_info) { }
//# sourceMappingURL=PresentationManagerDetail.js.map