UNPKG

@itwin/presentation-backend

Version:

Backend of iTwin.js Presentation library

447 lines • 19.9 kB
/*--------------------------------------------------------------------------------------------- * 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