UNPKG

@itwin/core-backend

Version:
349 lines • 16.4 kB
"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 NativeApp */ Object.defineProperty(exports, "__esModule", { value: true }); exports.IpcHandler = exports.IpcHost = void 0; exports.throttleProgressCallback = throttleProgressCallback; const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const CheckpointManager_1 = require("./CheckpointManager"); const IModelDb_1 = require("./IModelDb"); const IModelHost_1 = require("./IModelHost"); const NativePlatform_1 = require("./internal/NativePlatform"); const Symbols_1 = require("./internal/Symbols"); const IModelTileRpcImpl_1 = require("./rpc-impl/IModelTileRpcImpl"); /** * Used by applications that have a dedicated backend. IpcHosts may send messages to their corresponding IpcApp. * @note if either end terminates, the other must too. * @public */ class IpcHost { static noStack = false; static _ipc; /** Get the implementation of the [IpcSocketBackend]($common) interface. */ static get ipc() { return this._ipc; } // eslint-disable-line @typescript-eslint/no-non-null-assertion /** Determine whether Ipc is available for this backend. This will only be true if [[startup]] has been called on this class. */ static get isValid() { return undefined !== this._ipc; } /** * Send a message to the frontend over an Ipc channel. * @param channel the name of the channel matching the name registered with [[IpcApp.addListener]]. * @param data The content of the message. */ static send(channel, ...data) { this.ipc.send((0, core_common_1.iTwinChannel)(channel), ...data); } /** * Establish a handler for an Ipc channel to receive [[Frontend.invoke]] calls * @param channel The name of the channel for this handler. * @param handler A function that supplies the implementation for `channel` * @note returns A function to call to remove the handler. */ static handle(channel, handler) { return this.ipc.handle((0, core_common_1.iTwinChannel)(channel), handler); } /** * Establish a handler to receive messages sent via [[IpcApp.send]]. * @param channel The name of the channel for the messages. * @param listener A function called when messages are sent over `channel` * @note returns A function to call to remove the listener. */ static addListener(channel, listener) { return this.ipc.addListener((0, core_common_1.iTwinChannel)(channel), listener); } /** * Remove a previously registered listener * @param channel The name of the channel for the listener previously registered with [[addListener]] * @param listener The function passed to [[addListener]] */ static removeListener(channel, listener) { this.ipc.removeListener((0, core_common_1.iTwinChannel)(channel), listener); } static _nextInvokeId = 0; static _pendingInvokes = new Map(); /** * Send a message to the frontend via `channel` and expect a result asynchronously. The handler must be established on the frontend via [[IpcApp.handle]] * @param channel The name of the channel for the method. * @note `args` are serialized with the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), so only * primitive types and `ArrayBuffers` are allowed. * @note The returned Promise rejects if [[shutdown]] is called before the frontend responds. * @beta */ static async invoke(channel, ...args) { // Electron has no main->renderer `invoke` (see https://www.electronjs.org/docs/latest/tutorial/ipc#pattern-3-main-to-renderer), // so we synthesize request/response from the universally-available `send`/`addListener` primitives: push the request via // `send` together with a unique per-request response channel, and resolve when the frontend handler replies on that channel. const requestId = ++this._nextInvokeId % Number.MAX_SAFE_INTEGER; const responseChannel = (0, core_common_1.iTwinChannel)(`${channel}-invoke_response-${requestId}`); return new Promise((resolve, reject) => { let removeListener = () => { }; const cleanup = () => { removeListener(); this._pendingInvokes.delete(requestId); }; removeListener = this.ipc.addListener(responseChannel, (_evt, result) => { cleanup(); resolve(result); }); // allow [[shutdown]] to reject any in-flight invoke rather than leaking its listener forever. this._pendingInvokes.set(requestId, () => { cleanup(); reject(new Error(`IpcHost was shut down before the frontend responded on channel "${channel}"`)); }); try { this.send(channel, responseChannel, ...args); } catch (err) { // `send` can throw synchronously (e.g. a destroyed Electron window, or a closed websocket). Without this, // the listener and _pendingInvokes entry registered above would leak for the life of the process. cleanup(); reject(err instanceof Error ? err : new Error(String(err))); } }); } /** * Call a method on the frontend through an Ipc channel. * @param channelName the channel registered by the frontend handler. * @param methodName the name of a method implemented by the frontend handler. * @param args arguments to `methodName` * @returns a Promise with the return value from `methodName` */ static async callIpcChannel(channelName, methodName, ...args) { const retVal = await this.invoke(channelName, methodName, ...args); return (0, core_common_1.unwrapIpcInvokeReturn)(retVal); } /** * Create a type safe Proxy object to make IPC calls to a registered frontend interface. * @param channelName the channel registered by the frontend handler. * @beta */ static makeIpcProxy(channelName) { return (0, core_common_1.createIpcProxy)(async (methodName, ...args) => IpcHost.callIpcChannel(channelName, methodName, ...args)); } static notify(channel, briefcase, methodName, ...args) { if (this.isValid) return this.send(`${channel}/${briefcase.key}`, methodName, ...args); } /** @internal */ static notifyIpcFrontend(methodName, ...args) { return IpcHost.send(core_common_1.ipcAppChannels.appNotify, methodName, ...args); } /** @internal */ static notifyTxns(briefcase, methodName, ...args) { this.notify(core_common_1.ipcAppChannels.txns, briefcase, methodName, ...args); } /** @internal */ static notifyEditingScope(briefcase, methodName, ...args) { this.notify(core_common_1.ipcAppChannels.editingScope, briefcase, methodName, ...args); } /** * Start the backend of an Ipc app. * @param opt * @note this method calls [[IModelHost.startup]] internally. */ static async startup(opt) { this._ipc = opt?.ipcHost?.socket; if (opt?.ipcHost?.exceptions?.noStack) this.noStack = true; if (this.isValid) { // for tests, we use IpcHost but don't have a frontend IpcAppHandler.register(); } await IModelHost_1.IModelHost.startup(opt?.iModelHost); } /** Shutdown IpcHost backend. Also calls [[IModelHost.shutdown]] */ static async shutdown() { this._ipc = undefined; // reject any in-flight invokes so their callers don't hang forever after the socket is gone. const pending = [...this._pendingInvokes.values()]; this._pendingInvokes.clear(); for (const rejectPending of pending) rejectPending(); await IModelHost_1.IModelHost.shutdown(); } } exports.IpcHost = IpcHost; /** * Base class for all implementations of an Ipc interface. * * Create a subclass to implement your Ipc interface. Your class should be declared like this: * ```ts * class MyHandler extends IpcHandler implements MyInterface * ``` * to ensure all methods and signatures are correct. * * Then, call `MyClass.register` at startup to connect your class to your channel. * @public */ class IpcHandler { /** * Register this class as the handler for methods on its channel. This static method creates a new instance * that becomes the handler and is `this` when its methods are called. * @returns A function that can be called to remove the handler. * @note this method should only be called once per channel. If it is called multiple times, subsequent calls replace the previous ones. */ static register() { const impl = new this(); // create an instance of subclass. "as any" is necessary because base class is abstract const dispatch = (0, core_common_1.createIpcDispatcher)(impl, impl.channelName, () => !IpcHost.noStack); return IpcHost.handle(impl.channelName, async (_evt, funcName, ...args) => dispatch(funcName, ...args)); } } exports.IpcHandler = IpcHandler; /** * Implementation of IpcAppFunctions */ class IpcAppHandler extends IpcHandler { get channelName() { return core_common_1.ipcAppChannels.functions; } _iModelKeyToPullStatus = new Map(); async log(_timestamp, level, category, message, metaData) { switch (level) { case core_bentley_1.LogLevel.Error: core_bentley_1.Logger.logError(category, message, metaData); break; case core_bentley_1.LogLevel.Info: core_bentley_1.Logger.logInfo(category, message, metaData); break; case core_bentley_1.LogLevel.Trace: core_bentley_1.Logger.logTrace(category, message, metaData); break; case core_bentley_1.LogLevel.Warning: core_bentley_1.Logger.logWarning(category, message, metaData); break; } } async cancelTileContentRequests(tokenProps, contentIds) { return (0, IModelTileRpcImpl_1.cancelTileContentRequests)(tokenProps, contentIds); } async cancelElementGraphicsRequests(key, requestIds) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].cancelElementGraphicsRequests(requestIds); } async openBriefcase(args) { const db = await IModelDb_1.BriefcaseDb.open(args); return db.toJSON(); } async openCheckpoint(checkpoint) { return (await IModelDb_1.SnapshotDb.openCheckpoint(checkpoint)).getConnectionProps(); } async openStandalone(filePath, openMode, opts) { return IModelDb_1.StandaloneDb.openFile(filePath, openMode, opts).getConnectionProps(); } async openSnapshot(filePath, opts) { let resolvedFileName = filePath; if (IModelHost_1.IModelHost.snapshotFileNameResolver) { // eslint-disable-line @typescript-eslint/no-deprecated resolvedFileName = IModelHost_1.IModelHost.snapshotFileNameResolver.tryResolveFileName(filePath); // eslint-disable-line @typescript-eslint/no-deprecated if (!resolvedFileName) throw new core_common_1.IModelNotFoundResponse(); // eslint-disable-line @typescript-eslint/only-throw-error } return IModelDb_1.SnapshotDb.openFile(resolvedFileName, opts).getConnectionProps(); } async closeIModel(key) { IModelDb_1.IModelDb.findByKey(key).close(); } async saveChanges(key, description) { IModelDb_1.IModelDb.findByKey(key)[Symbols_1._implicitTxn].saveChanges(description); } async abandonChanges(key) { IModelDb_1.IModelDb.findByKey(key)[Symbols_1._implicitTxn].abandonChanges(); } async hasPendingTxns(key) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].hasPendingTxns(); } async isUndoPossible(key) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].isUndoPossible(); } async isRedoPossible(key) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].isRedoPossible(); } async getUndoString(key) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].getUndoString(); } async getRedoString(key) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].getRedoString(); } async pullChanges(key, toIndex, options) { const iModelDb = IModelDb_1.BriefcaseDb.findByKey(key); this._iModelKeyToPullStatus.set(key, CheckpointManager_1.ProgressStatus.Continue); const checkAbort = () => this._iModelKeyToPullStatus.get(key) ?? CheckpointManager_1.ProgressStatus.Continue; let onProgress; if (options?.reportProgress) { const progressCallback = (loaded, total) => { IpcHost.send((0, core_common_1.getPullChangesIpcChannel)(iModelDb.iModelId), { loaded, total }); return checkAbort(); }; onProgress = throttleProgressCallback(progressCallback, checkAbort, options?.progressInterval); } else if (options?.enableCancellation) { onProgress = checkAbort; } try { await iModelDb.pullChanges({ toIndex, onProgress }); } finally { this._iModelKeyToPullStatus.delete(key); } return iModelDb.changeset; } async cancelPullChangesRequest(key) { this._iModelKeyToPullStatus.set(key, CheckpointManager_1.ProgressStatus.Abort); } async pushChanges(key, description) { const iModelDb = IModelDb_1.BriefcaseDb.findByKey(key); await iModelDb.pushChanges({ description }); return iModelDb.changeset; } async toggleGraphicalEditingScope(key, startSession) { const val = IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].setGeometricModelTrackingEnabled(startSession); if (val.error) throw new core_common_1.IModelError(val.error.status, "Failed to toggle graphical editing scope"); (0, core_bentley_1.assert)(undefined !== val.result); return val.result; } async isGraphicalEditingSupported(key) { return IModelDb_1.IModelDb.findByKey(key)[Symbols_1._nativeDb].isGeometricModelTrackingSupported(); } async reverseTxns(key, numOperations) { return IModelDb_1.BriefcaseDb.findByKey(key).txns.reverseTxns(numOperations); } async reverseTxnsAsync(key, numOperations, args) { return IModelDb_1.BriefcaseDb.findByKey(key).txns.reverseTxnsAsync(numOperations, args); } async reverseAllTxn(key) { return IModelDb_1.BriefcaseDb.findByKey(key).txns.reverseAll(); } async reverseAllTxnsAsync(key, args) { return IModelDb_1.BriefcaseDb.findByKey(key).txns.reverseAllTxnsAsync(args); } async reinstateTxn(key) { return IModelDb_1.BriefcaseDb.findByKey(key).txns.reinstateTxn(); } async reinstateTxnAsync(key, args) { return IModelDb_1.BriefcaseDb.findByKey(key).txns.reinstateTxnAsync(args); } async restartTxnSession(key) { return IModelDb_1.IModelDb.findByKey(key).restartTxnSession(); } async queryConcurrency(pool) { return NativePlatform_1.IModelNative.platform.queryConcurrency(pool); } } /** * Prevents progress callback being called more frequently when provided interval. * @internal */ function throttleProgressCallback(func, checkAbort, progressInterval) { const interval = progressInterval ?? 250; // by default, only send progress events every 250 milliseconds let nextTime = Date.now() + interval; const progressCallback = (loaded, total) => { const now = Date.now(); if (loaded >= total || now >= nextTime) { nextTime = now + interval; return func(loaded, total); } return checkAbort(); }; return progressCallback; } //# sourceMappingURL=IpcHost.js.map