@itwin/core-backend
Version:
iTwin.js backend components
515 lines • 25.5 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 IModelHost
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileNameResolver = exports.KnownLocations = exports.Platform = exports.IModelHost = exports.IModelHostConfiguration = void 0;
// To avoid circular load errors, the "Element" classes must be loaded before IModelHost.
require("./IModelDb"); // DO NOT REMOVE OR MOVE THIS LINE!
const NativePlatform_1 = require("./internal/NativePlatform");
const os = require("os");
require("reflect-metadata"); // this has to be before @itwin/object-storage-* and @itwin/cloud-agnostic-core imports because those packages contain decorators that use this polyfill.
const imodeljs_native_1 = require("@bentley/imodeljs-native");
const cloud_agnostic_core_1 = require("@itwin/cloud-agnostic-core");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const object_storage_azure_1 = require("@itwin/object-storage-azure");
const object_storage_core_1 = require("@itwin/object-storage-core");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const BisCoreSchema_1 = require("./BisCoreSchema");
const BriefcaseManager_1 = require("./BriefcaseManager");
const CloudSqlite_1 = require("./CloudSqlite");
const FunctionalSchema_1 = require("./domains/FunctionalSchema");
const GenericSchema_1 = require("./domains/GenericSchema");
const GeoCoordConfig_1 = require("./GeoCoordConfig");
const IModelJsFs_1 = require("./IModelJsFs");
const DevToolsRpcImpl_1 = require("./rpc-impl/DevToolsRpcImpl");
const IModelReadRpcImpl_1 = require("./rpc-impl/IModelReadRpcImpl");
const IModelTileRpcImpl_1 = require("./rpc-impl/IModelTileRpcImpl");
const SnapshotIModelRpcImpl_1 = require("./rpc-impl/SnapshotIModelRpcImpl");
const RpcBackend_1 = require("./RpcBackend");
const TileStorage_1 = require("./TileStorage");
const Settings_1 = require("./workspace/Settings");
const inversify_1 = require("inversify");
const path_1 = require("path");
const WorkspaceImpl_1 = require("./internal/workspace/WorkspaceImpl");
const SettingsImpl_1 = require("./internal/workspace/SettingsImpl");
const SettingsSchemasImpl_1 = require("./internal/workspace/SettingsSchemasImpl");
const Symbols_1 = require("./internal/Symbols");
const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.IModelHost;
/** Configuration of core-backend.
* @public
*/
class IModelHostConfiguration {
static defaultTileRequestTimeout = 20 * 1000;
static defaultLogTileLoadTimeThreshold = 40;
static defaultLogTileSizeThreshold = 20 * 1000000;
/** @internal */
static defaultMaxTileCacheDbSize = 1024 * 1024 * 1024;
appAssetsDir;
cacheDir;
/** @beta */
workspace;
hubAccess;
/** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */
authorizationClient;
/** @beta */
restrictTileUrlsByClientIp;
compressCachedTiles;
/** @beta */
tileCacheAzureCredentials;
/** @internal */
tileTreeRequestTimeout = IModelHostConfiguration.defaultTileRequestTimeout;
/** @internal */
tileContentRequestTimeout = IModelHostConfiguration.defaultTileRequestTimeout;
/** @internal */
logTileLoadTimeThreshold = IModelHostConfiguration.defaultLogTileLoadTimeThreshold;
/** @internal */
logTileSizeThreshold = IModelHostConfiguration.defaultLogTileSizeThreshold;
/** @internal */
crashReportingConfig;
/**
* Configuration controlling whether to use the thinned down native instance functions for element, model, and aspect CRUD operations
* or use the previous behavior of using the native side for all CRUD operations. Set to true to revert to the previous behavior.
* @beta
*/
disableThinnedNativeInstanceWorkflow;
}
exports.IModelHostConfiguration = IModelHostConfiguration;
/**
* Settings for `IModelHost.appWorkspace`.
* @note this includes the default dictionary from the SettingsSpecRegistry
*/
class ApplicationSettings extends SettingsImpl_1.SettingsImpl {
_remove;
verifyPriority(priority) {
if (priority > Settings_1.SettingsPriority.application) // only application or lower may appear in ApplicationSettings
throw new Error("Use IModelSettings");
}
updateDefaults() {
const defaults = {};
for (const [schemaName, val] of IModelHost.settingsSchemas.settingDefs) {
if (val.default)
defaults[schemaName] = val.default;
}
this.addDictionary({ name: "_default_", priority: 0 }, defaults);
}
constructor() {
super();
this._remove = IModelHost.settingsSchemas.onSchemaChanged.addListener(() => this.updateDefaults());
this.updateDefaults();
}
close() {
if (this._remove) {
this._remove();
this._remove = undefined;
}
}
}
const definedInStartup = (obj) => {
if (obj === undefined)
throw new Error("IModelHost.startup must be called first");
return obj;
};
/** IModelHost initializes ($backend) and captures its configuration. A backend must call [[IModelHost.startup]] before using any backend classes.
* See [the learning article]($docs/learning/backend/IModelHost.md)
* @public
*/
class IModelHost {
constructor() { }
/** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */
static authorizationClient;
static backendVersion = "";
static _profileName;
static _cacheDir = "";
static _settingsSchemas;
static _appWorkspace;
// Omit the hubAccess field from configuration so it stays internal.
static configuration;
/**
* The name of the *Profile* directory (a subdirectory of "[[cacheDir]]/profiles/") for this process.
*
* The *Profile* directory is used to cache data that is specific to a type-of-usage of the iTwin.js library.
* It is important that information in the profile cache be consistent but isolated across sessions (i.e.
* data for a profile is maintained between runs, but each profile is completely independent and
* unaffected by the presence or use of others.)
* @note **Only one process at a time may be using a given profile**, and an exception will be thrown by [[startup]]
* if a second process attempts to use the same profile.
* @beta
*/
static get profileName() {
return this._profileName;
}
/** The full path of the Profile directory.
* @see [[profileName]]
* @beta
*/
static get profileDir() {
return (0, path_1.join)(this._cacheDir, "profiles", this._profileName);
}
/** Event raised during startup to allow loading settings data */
static onWorkspaceStartup = new core_bentley_1.BeEvent();
/** Event raised just after the backend IModelHost was started */
static onAfterStartup = new core_bentley_1.BeEvent();
/** Event raised just before the backend IModelHost is to be shut down */
static onBeforeShutdown = new core_bentley_1.BeEvent();
/** @internal */
static session = { applicationId: "2686", applicationVersion: "1.0.0", sessionId: "" };
/** A uniqueId for this session */
static get sessionId() { return this.session.sessionId; }
static set sessionId(id) { this.session.sessionId = id; }
/** The Id of this application - needs to be set only if it is an agent application. The applicationId will otherwise originate at the frontend. */
static get applicationId() { return this.session.applicationId; }
static set applicationId(id) { this.session.applicationId = id; }
/** The version of this application - needs to be set if is an agent application. The applicationVersion will otherwise originate at the frontend. */
static get applicationVersion() { return this.session.applicationVersion; }
static set applicationVersion(version) { this.session.applicationVersion = version; }
/** A string that can identify the current user to other users when collaborating. */
static userMoniker = "unknown";
/** Root directory holding files that iTwin.js caches */
static get cacheDir() { return this._cacheDir; }
/** The application [[Workspace]] for this `IModelHost`
* @note this `Workspace` only holds [[WorkspaceContainer]]s and [[Settings]] scoped to the currently loaded application(s).
* All organization, iTwin, and iModel based containers or settings must be accessed through [[IModelDb.workspace]] and
* attempting to add them to this Workspace will fail.
* @beta
*/
static get appWorkspace() { return definedInStartup(this._appWorkspace); }
/** The registry of schemas describing the [[Setting]]s for the application session.
* Applications should register their schemas via methods like [[SettingsSchemas.addGroup]].
* @beta
*/
static get settingsSchemas() { return definedInStartup(this._settingsSchemas); }
/** The optional [[FileNameResolver]] that resolves keys and partial file names for snapshot iModels.
* @deprecated in 4.10 - will not be removed until after 2026-06-13. When opening a snapshot by file name, ensure to pass already resolved path. Using a key to open a snapshot is now deprecated.
*/
static snapshotFileNameResolver; // eslint-disable-line @typescript-eslint/no-deprecated
/** Get the current access token for this IModelHost, or a blank string if none is available.
* @note for web backends, this will *always* return a blank string because the backend itself has no token (but never needs one either.)
* For all IpcHosts, where this backend is servicing a single frontend, this will be the user's token. For ElectronHost, the backend
* obtains the token and forwards it to the frontend.
* @note accessTokens expire periodically and are automatically refreshed, if possible. Therefore tokens should not be saved, and the value
* returned by this method may change over time throughout the course of a session.
*/
static async getAccessToken() {
try {
return (await IModelHost.authorizationClient?.getAccessToken()) ?? "";
}
catch {
return "";
}
}
static loadNative(options) {
(0, NativePlatform_1.loadNativePlatform)();
if (options.crashReportingConfig && options.crashReportingConfig.crashDir && !core_bentley_1.ProcessDetector.isElectronAppBackend && !core_bentley_1.ProcessDetector.isMobileAppBackend) {
NativePlatform_1.IModelNative.platform.setCrashReporting(options.crashReportingConfig);
core_bentley_1.Logger.logTrace(loggerCategory, "Configured crash reporting", {
enableCrashDumps: options.crashReportingConfig?.enableCrashDumps,
wantFullMemoryDumps: options.crashReportingConfig?.wantFullMemoryDumps,
enableNodeReport: options.crashReportingConfig?.enableNodeReport,
uploadToBentley: options.crashReportingConfig?.uploadToBentley,
});
if (options.crashReportingConfig.enableNodeReport) {
if (process.report !== undefined) {
process.report.reportOnFatalError = true;
process.report.reportOnUncaughtException = true;
process.report.directory = options.crashReportingConfig.crashDir;
core_bentley_1.Logger.logTrace(loggerCategory, "Configured Node.js crash reporting");
}
else {
core_bentley_1.Logger.logWarning(loggerCategory, "Unable to configure Node.js crash reporting");
}
}
}
}
/** @internal */
static tileStorage;
static _hubAccess;
/** @internal */
static [Symbols_1._setHubAccess](hubAccess) { this._hubAccess = hubAccess; }
/** get the current hubAccess, if present.
* @internal
*/
static [Symbols_1._getHubAccess]() { return this._hubAccess; }
/** Provides access to the IModelHub for this IModelHost
* @internal
* @note If [[IModelHostOptions.hubAccess]] was undefined when initializing this class, accessing this property will throw an error.
* To determine whether one is present, use [[_getHubAccess]].
*/
static get [Symbols_1._hubAccess]() {
if (IModelHost._hubAccess === undefined)
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "No BackendHubAccess supplied in IModelHostOptions");
return IModelHost._hubAccess;
}
static initializeWorkspace(configuration) {
const settingAssets = (0, path_1.join)(KnownLocations.packageAssetsDir, "Settings");
this._settingsSchemas = (0, SettingsSchemasImpl_1.constructSettingsSchemas)();
this._settingsSchemas.addDirectory((0, path_1.join)(settingAssets, "Schemas"));
this._appWorkspace = (0, WorkspaceImpl_1.constructWorkspace)(new ApplicationSettings(), configuration.workspace);
// Create the CloudCache for Workspaces. This will fail if another process is already using the same profile.
try {
this.appWorkspace.getCloudCache();
}
catch (e) {
throw (e.errorNumber === core_bentley_1.DbResult.BE_SQLITE_BUSY) ? new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_BUSY, `Profile [${this.profileDir}] is already in use by another process`) : e;
}
this.appWorkspace.settings.addDirectory(settingAssets, Settings_1.SettingsPriority.defaults);
GeoCoordConfig_1.GeoCoordConfig.onStartup();
// allow applications to load their default settings
this.onWorkspaceStartup.raiseEvent();
}
static _isValid = false;
/** true between a successful call to [[startup]] and before [[shutdown]] */
static get isValid() {
return IModelHost._isValid;
}
/** This method must be called before any iTwin.js services are used.
* @param options Host configuration data.
* Raises [[onAfterStartup]].
* @see [[shutdown]].
*/
static async startup(options) {
if (this._isValid)
return; // we're already initialized
this._isValid = true;
options = options ?? {};
if (this.sessionId === "")
this.sessionId = core_bentley_1.Guid.createValue();
this.authorizationClient = options.authorizationClient;
this.backendVersion = require("../../package.json").version; // eslint-disable-line @typescript-eslint/no-require-imports
(0, RpcBackend_1.initializeRpcBackend)(options.enableOpenTelemetry);
this.loadNative(options);
this.setupCacheDir(options);
this.initializeWorkspace(options);
BriefcaseManager_1.BriefcaseManager.initialize((0, path_1.join)(this._cacheDir, "imodels"));
[
IModelReadRpcImpl_1.IModelReadRpcImpl,
IModelTileRpcImpl_1.IModelTileRpcImpl,
SnapshotIModelRpcImpl_1.SnapshotIModelRpcImpl, // eslint-disable-line @typescript-eslint/no-deprecated
DevToolsRpcImpl_1.DevToolsRpcImpl,
].forEach((rpc) => rpc.register()); // register all of the RPC implementations
[
BisCoreSchema_1.BisCoreSchema,
GenericSchema_1.GenericSchema,
FunctionalSchema_1.FunctionalSchema,
].forEach((schema) => schema.registerSchema()); // register all of the schemas
const { hubAccess, ...otherOptions } = options;
if (undefined !== hubAccess)
this._hubAccess = hubAccess;
this.configuration = otherOptions;
this.setupTileCache();
process.once("beforeExit", IModelHost.shutdown);
this.onAfterStartup.raiseEvent();
}
static setupCacheDir(configuration) {
this._cacheDir = (0, path_1.normalize)(configuration.cacheDir ?? imodeljs_native_1.NativeLibrary.defaultCacheDir);
IModelJsFs_1.IModelJsFs.recursiveMkDirSync(this._cacheDir);
this._profileName = configuration.profileName ?? "default";
core_bentley_1.Logger.logInfo(loggerCategory, `cacheDir: [${this.cacheDir}], profileDir: [${this.profileDir}]`);
}
/** This method must be called when an iTwin.js host is shut down. Raises [[onBeforeShutdown]] */
static async shutdown() {
// Note: This method is set as a node listener where `this` is unbound. Call private method to
// ensure `this` is correct. Don't combine these methods.
return IModelHost.doShutdown();
}
/**
* Create a new iModel.
* @returns the Guid of the newly created iModel.
* @throws [IModelError]($common) in case of errors.
* @note If [[IModelHostOptions.hubAccess]] was undefined in the call to [[startup]], this function will throw an error.
*/
static async createNewIModel(arg) {
return this[Symbols_1._hubAccess].createNewIModel(arg);
}
static async doShutdown() {
if (!this._isValid)
return;
this._isValid = false;
this.onBeforeShutdown.raiseEvent();
this.configuration = undefined;
this.tileStorage = undefined;
this._appWorkspace?.close();
this._appWorkspace = undefined;
this._settingsSchemas = undefined;
CloudSqlite_1.CloudSqlite.CloudCaches.destroy();
process.removeListener("beforeExit", IModelHost.shutdown);
}
/**
* Add or update a property that should be included in a crash report.
* @internal
*/
static setCrashReportProperty(name, value) {
NativePlatform_1.IModelNative.platform.setCrashReportProperty(name, value);
}
/**
* Remove a previously defined property so that will not be included in a crash report.
* @internal
*/
static removeCrashReportProperty(name) {
NativePlatform_1.IModelNative.platform.setCrashReportProperty(name, undefined);
}
/**
* Get all properties that will be included in a crash report.
* @internal
*/
static getCrashReportProperties() {
return NativePlatform_1.IModelNative.platform.getCrashReportProperties();
}
/** The directory where application assets may be found */
static get appAssetsDir() {
return undefined !== IModelHost.configuration ? IModelHost.configuration.appAssetsDir : undefined;
}
/** The time, in milliseconds, for which IModelTileRpcInterface.requestTileTreeProps should wait before returning a "pending" status.
* @internal
*/
static get tileTreeRequestTimeout() {
return IModelHost.configuration?.tileTreeRequestTimeout ?? IModelHostConfiguration.defaultTileRequestTimeout;
}
/** The time, in milliseconds, for which IModelTileRpcInterface.requestTileContent should wait before returning a "pending" status.
* @internal
*/
static get tileContentRequestTimeout() {
return IModelHost.configuration?.tileContentRequestTimeout ?? IModelHostConfiguration.defaultTileRequestTimeout;
}
/** The backend will log when a tile took longer to load than this threshold in seconds. */
static get logTileLoadTimeThreshold() {
return IModelHost.configuration?.logTileLoadTimeThreshold ?? IModelHostConfiguration.defaultLogTileLoadTimeThreshold;
}
/** The backend will log when a tile is loaded with a size in bytes above this threshold. */
static get logTileSizeThreshold() {
return IModelHost.configuration?.logTileSizeThreshold ?? IModelHostConfiguration.defaultLogTileSizeThreshold;
}
/** Whether external tile caching is active.
* @internal
*/
static get usingExternalTileCache() {
return undefined !== IModelHost.tileStorage;
}
/** Whether to restrict tile cache URLs by client IP address.
* @internal
*/
static get restrictTileUrlsByClientIp() {
return undefined !== IModelHost.configuration && (IModelHost.configuration.restrictTileUrlsByClientIp ? true : false);
}
/** Whether to compress cached tiles.
* @internal
*/
static get compressCachedTiles() {
return false !== IModelHost.configuration?.compressCachedTiles;
}
static setupTileCache() {
(0, core_bentley_1.assert)(undefined !== IModelHost.configuration);
const config = IModelHost.configuration;
const storage = config.tileCacheStorage;
const credentials = config.tileCacheAzureCredentials;
if (!storage && !credentials) {
NativePlatform_1.IModelNative.platform.setMaxTileCacheSize(config.maxTileCacheDbSize ?? IModelHostConfiguration.defaultMaxTileCacheDbSize);
return;
}
NativePlatform_1.IModelNative.platform.setMaxTileCacheSize(0);
if (credentials) {
if (storage)
throw new core_common_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Cannot use both Azure and custom cloud storage providers for tile cache.");
this.setupAzureTileCache(credentials);
}
if (storage)
IModelHost.tileStorage = new TileStorage_1.TileStorage(storage);
}
static setupAzureTileCache(credentials) {
const config = {
// eslint-disable-next-line @typescript-eslint/naming-convention
ServerSideStorage: {
dependencyName: "azure",
accountName: credentials.account,
accountKey: credentials.accessKey,
baseUrl: credentials.baseUrl ?? `https://${credentials.account}.blob.core.windows.net`,
},
};
const ioc = new inversify_1.Container();
ioc.bind(cloud_agnostic_core_1.Types.dependenciesConfig).toConstantValue(config);
new object_storage_azure_1.AzureServerStorageBindings().register(ioc, config.ServerSideStorage);
IModelHost.tileStorage = new TileStorage_1.TileStorage(ioc.get(object_storage_core_1.ServerStorage));
}
/** @internal */
static computeSchemaChecksum(arg) {
return NativePlatform_1.IModelNative.platform.computeSchemaChecksum(arg);
}
}
exports.IModelHost = IModelHost;
/** Information about the platform on which the app is running.
* @public
*/
class Platform {
/** Get the name of the platform. */
static get platformName() {
return process.platform;
}
}
exports.Platform = Platform;
/** Well known directories that may be used by the application.
* @public
*/
class KnownLocations {
/** The directory where the imodeljs-native assets are stored. */
static get nativeAssetsDir() {
return NativePlatform_1.IModelNative.platform.DgnDb.getAssetsDir();
}
/** The directory where the core-backend assets are stored. */
static get packageAssetsDir() {
return (0, path_1.join)(__dirname, "assets");
}
/** The temporary directory. */
static get tmpdir() {
return os.tmpdir();
}
}
exports.KnownLocations = KnownLocations;
/** Extend this class to provide custom file name resolution behavior.
* @note Only `tryResolveKey` and/or `tryResolveFileName` need to be overridden as the implementations of `resolveKey` and `resolveFileName` work for most purposes.
* @see [[IModelHost.snapshotFileNameResolver]]
* @public
* @deprecated in 4.10 - will not be removed until after 2026-06-13. When opening a snapshot by file name, ensure to pass already resolved path. Using a key to open a snapshot is now deprecated.
*/
class FileNameResolver {
/** Resolve a file name from the specified key.
* @param _fileKey The key that identifies the file name in a `Map` or other similar data structure.
* @returns The resolved file name or `undefined` if not found.
*/
tryResolveKey(_fileKey) { return undefined; }
/** Resolve a file name from the specified key.
* @param fileKey The key that identifies the file name in a `Map` or other similar data structure.
* @returns The resolved file name.
* @throws [[IModelError]] if not found.
*/
resolveKey(fileKey) {
const resolvedFileName = this.tryResolveKey(fileKey);
if (undefined === resolvedFileName) {
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, `${fileKey} not resolved`);
}
return resolvedFileName;
}
/** Resolve the input file name, which may be a partial name, into a full path file name.
* @param inFileName The partial file name.
* @returns The resolved full path file name or `undefined` if not found.
*/
tryResolveFileName(inFileName) { return inFileName; }
/** Resolve the input file name, which may be a partial name, into a full path file name.
* @param inFileName The partial file name.
* @returns The resolved full path file name.
* @throws [[IModelError]] if not found.
*/
resolveFileName(inFileName) {
const resolvedFileName = this.tryResolveFileName(inFileName);
if (undefined === resolvedFileName) {
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, `${inFileName} not resolved`);
}
return resolvedFileName;
}
}
exports.FileNameResolver = FileNameResolver;
//# sourceMappingURL=IModelHost.js.map