@itwin/core-backend
Version:
iTwin.js backend components
343 lines • 15.5 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 NativeApp
*/
import { assert, Logger, LogLevel } from "@itwin/core-bentley";
import { createIpcDispatcher, createIpcProxy, getPullChangesIpcChannel, IModelError, IModelNotFoundResponse, ipcAppChannels, iTwinChannel, unwrapIpcInvokeReturn, } from "@itwin/core-common";
import { ProgressStatus } from "./CheckpointManager";
import { BriefcaseDb, IModelDb, SnapshotDb, StandaloneDb } from "./IModelDb";
import { IModelHost } from "./IModelHost";
import { IModelNative } from "./internal/NativePlatform";
import { _implicitTxn, _nativeDb } from "./internal/Symbols";
import { cancelTileContentRequests } from "./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
*/
export 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(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(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(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(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 = 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 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 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(ipcAppChannels.appNotify, methodName, ...args);
}
/** @internal */
static notifyTxns(briefcase, methodName, ...args) {
this.notify(ipcAppChannels.txns, briefcase, methodName, ...args);
}
/** @internal */
static notifyEditingScope(briefcase, methodName, ...args) {
this.notify(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.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.shutdown();
}
}
/**
* 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
*/
export 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 = createIpcDispatcher(impl, impl.channelName, () => !IpcHost.noStack);
return IpcHost.handle(impl.channelName, async (_evt, funcName, ...args) => dispatch(funcName, ...args));
}
}
/**
* Implementation of IpcAppFunctions
*/
class IpcAppHandler extends IpcHandler {
get channelName() { return ipcAppChannels.functions; }
_iModelKeyToPullStatus = new Map();
async log(_timestamp, level, category, message, metaData) {
switch (level) {
case LogLevel.Error:
Logger.logError(category, message, metaData);
break;
case LogLevel.Info:
Logger.logInfo(category, message, metaData);
break;
case LogLevel.Trace:
Logger.logTrace(category, message, metaData);
break;
case LogLevel.Warning:
Logger.logWarning(category, message, metaData);
break;
}
}
async cancelTileContentRequests(tokenProps, contentIds) {
return cancelTileContentRequests(tokenProps, contentIds);
}
async cancelElementGraphicsRequests(key, requestIds) {
return IModelDb.findByKey(key)[_nativeDb].cancelElementGraphicsRequests(requestIds);
}
async openBriefcase(args) {
const db = await BriefcaseDb.open(args);
return db.toJSON();
}
async openCheckpoint(checkpoint) {
return (await SnapshotDb.openCheckpoint(checkpoint)).getConnectionProps();
}
async openStandalone(filePath, openMode, opts) {
return StandaloneDb.openFile(filePath, openMode, opts).getConnectionProps();
}
async openSnapshot(filePath, opts) {
let resolvedFileName = filePath;
if (IModelHost.snapshotFileNameResolver) { // eslint-disable-line @typescript-eslint/no-deprecated
resolvedFileName = IModelHost.snapshotFileNameResolver.tryResolveFileName(filePath); // eslint-disable-line @typescript-eslint/no-deprecated
if (!resolvedFileName)
throw new IModelNotFoundResponse(); // eslint-disable-line @typescript-eslint/only-throw-error
}
return SnapshotDb.openFile(resolvedFileName, opts).getConnectionProps();
}
async closeIModel(key) {
IModelDb.findByKey(key).close();
}
async saveChanges(key, description) {
IModelDb.findByKey(key)[_implicitTxn].saveChanges(description);
}
async abandonChanges(key) {
IModelDb.findByKey(key)[_implicitTxn].abandonChanges();
}
async hasPendingTxns(key) {
return IModelDb.findByKey(key)[_nativeDb].hasPendingTxns();
}
async isUndoPossible(key) {
return IModelDb.findByKey(key)[_nativeDb].isUndoPossible();
}
async isRedoPossible(key) {
return IModelDb.findByKey(key)[_nativeDb].isRedoPossible();
}
async getUndoString(key) {
return IModelDb.findByKey(key)[_nativeDb].getUndoString();
}
async getRedoString(key) {
return IModelDb.findByKey(key)[_nativeDb].getRedoString();
}
async pullChanges(key, toIndex, options) {
const iModelDb = BriefcaseDb.findByKey(key);
this._iModelKeyToPullStatus.set(key, ProgressStatus.Continue);
const checkAbort = () => this._iModelKeyToPullStatus.get(key) ?? ProgressStatus.Continue;
let onProgress;
if (options?.reportProgress) {
const progressCallback = (loaded, total) => {
IpcHost.send(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, ProgressStatus.Abort);
}
async pushChanges(key, description) {
const iModelDb = BriefcaseDb.findByKey(key);
await iModelDb.pushChanges({ description });
return iModelDb.changeset;
}
async toggleGraphicalEditingScope(key, startSession) {
const val = IModelDb.findByKey(key)[_nativeDb].setGeometricModelTrackingEnabled(startSession);
if (val.error)
throw new IModelError(val.error.status, "Failed to toggle graphical editing scope");
assert(undefined !== val.result);
return val.result;
}
async isGraphicalEditingSupported(key) {
return IModelDb.findByKey(key)[_nativeDb].isGeometricModelTrackingSupported();
}
async reverseTxns(key, numOperations) {
return BriefcaseDb.findByKey(key).txns.reverseTxns(numOperations);
}
async reverseTxnsAsync(key, numOperations, args) {
return BriefcaseDb.findByKey(key).txns.reverseTxnsAsync(numOperations, args);
}
async reverseAllTxn(key) {
return BriefcaseDb.findByKey(key).txns.reverseAll();
}
async reverseAllTxnsAsync(key, args) {
return BriefcaseDb.findByKey(key).txns.reverseAllTxnsAsync(args);
}
async reinstateTxn(key) {
return BriefcaseDb.findByKey(key).txns.reinstateTxn();
}
async reinstateTxnAsync(key, args) {
return BriefcaseDb.findByKey(key).txns.reinstateTxnAsync(args);
}
async restartTxnSession(key) {
return IModelDb.findByKey(key).restartTxnSession();
}
async queryConcurrency(pool) {
return IModelNative.platform.queryConcurrency(pool);
}
}
/**
* Prevents progress callback being called more frequently when provided interval.
* @internal
*/
export 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