/
IpcApp.ts
196 lines (175 loc) · 8.87 KB
/
IpcApp.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/*---------------------------------------------------------------------------------------------
* 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 { AsyncMethodsOf, PickAsyncMethods, PromiseReturnType } from "@itwin/core-bentley";
import {
BackendError, IModelError, IModelStatus, ipcAppChannels, IpcAppFunctions, IpcAppNotifications, IpcInvokeReturn, IpcListener, IpcSocketFrontend,
iTwinChannel, RemoveFunction,
} from "@itwin/core-common";
import { IModelApp, IModelAppOptions } from "./IModelApp";
/**
* Options for [[IpcApp.startup]]
* @public
*/
export interface IpcAppOptions {
iModelApp?: IModelAppOptions;
}
/**
* The frontend of apps with a dedicated backend that can use [Ipc]($docs/learning/IpcInterface.md).
* @public
*/
export class IpcApp {
private static _ipc: IpcSocketFrontend | undefined;
/** Get the implementation of the [[IpcSocketFrontend]] interface. */
private static get ipc(): IpcSocketFrontend { return this._ipc!; }
/** Determine whether Ipc is available for this frontend. This will only be true if [[startup]] has been called on this class. */
public static get isValid(): boolean { return undefined !== this._ipc; }
/**
* Establish a message handler function for the supplied channel over Ipc. The handler will be called when messages are sent for
* the channel via [[BackendIpc.send]].
* @param channel the name of the channel
* @param handler the message handler
* @returns A function to remove the handler
* @note Ipc is only supported if [[isValid]] is true.
*/
public static addListener(channel: string, handler: IpcListener): RemoveFunction {
return this.ipc.addListener(iTwinChannel(channel), handler);
}
/**
* 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]]
*/
public static removeListener(channel: string, listener: IpcListener) {
this.ipc.removeListener(iTwinChannel(channel), listener);
}
/**
* Send a message to the backend via `channel` and expect a result asynchronously. The handler must be established on the backend via [[BackendIpc.handle]]
* @param channel The name of the channel for the method.
* @see Electron [ipcRenderer.invoke](https://www.electronjs.org/docs/api/ipc-renderer) documentation for details.
* Note that this interface may be implemented via Electron for desktop apps, or via
* [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) for mobile or web-based
* Ipc connections. In either case, the Electron documentation provides the specifications for how it works.
* @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.
*/
public static async invoke(channel: string, ...args: any[]): Promise<any> {
return this.ipc.invoke(iTwinChannel(channel), ...args);
}
/**
* Send a message over the socket.
* @param channel The name of the channel for the message.
* @param data The optional data of the message.
* @note `data` is 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.
*/
public static send(channel: string, ...data: any[]) {
return this.ipc.send(iTwinChannel(channel), ...data);
}
/**
* Call a method on the backend through an Ipc channel.
* @param channelName the channel registered by the backend handler.
* @param methodName the name of a method implemented by the backend handler.
* @param args arguments to `methodName`
* @return a Promise with the return value from `methodName`
* @note If the backend implementation throws an exception, this method will throw a [[BackendError]] exception
* with the `errorNumber` and `message` from the backend.
* @note Ipc is only supported if [[isValid]] is true.
* @internal Use [[makeIpcProxy]] for a type-safe interface.
*/
public static async callIpcChannel(channelName: string, methodName: string, ...args: any[]): Promise<any> {
const retVal = (await this.invoke(channelName, methodName, ...args)) as IpcInvokeReturn;
if (undefined !== retVal.error) {
const err = new BackendError(retVal.error.errorNumber, retVal.error.name, retVal.error.message);
err.stack = retVal.error.stack;
throw err;
}
return retVal.result;
}
/** Create a type safe Proxy object to make IPC calls to a registered backend interface.
* @param channelName the channel registered by the backend handler.
*/
public static makeIpcProxy<K>(channelName: string): PickAsyncMethods<K> {
return new Proxy({} as PickAsyncMethods<K>, {
get(_target, methodName: string) {
return async (...args: any[]) =>
IpcApp.callIpcChannel(channelName, methodName, ...args);
},
});
}
/** Create a type safe Proxy object to call an IPC function on a of registered backend handler that accepts a "methodName" argument followed by optional arguments
* @param channelName the channel registered by the backend handler.
* @param functionName the function to call on the handler.
* @internal
*/
public static makeIpcFunctionProxy<K>(channelName: string, functionName: string): PickAsyncMethods<K> {
return new Proxy({} as PickAsyncMethods<K>, {
get(_target, methodName: string) {
return async (...args: any[]) =>
IpcApp.callIpcChannel(channelName, functionName, methodName, ...args);
},
});
}
/** @deprecated in 3.x. use [[appFunctionIpc]] */
public static async callIpcHost<T extends AsyncMethodsOf<IpcAppFunctions>>(methodName: T, ...args: Parameters<IpcAppFunctions[T]>) {
return this.callIpcChannel(ipcAppChannels.functions, methodName, ...args) as PromiseReturnType<IpcAppFunctions[T]>;
}
/** A Proxy to call one of the [IpcAppFunctions]($common) functions via IPC. */
public static appFunctionIpc = IpcApp.makeIpcProxy<IpcAppFunctions>(ipcAppChannels.functions);
/** start an IpcApp.
* @note this should not be called directly. It is called by NativeApp.startup */
public static async startup(ipc: IpcSocketFrontend, opts?: IpcAppOptions) {
this._ipc = ipc;
IpcAppNotifyHandler.register(); // receives notifications from backend
await IModelApp.startup(opts?.iModelApp);
}
/** @internal */
public static async shutdown() {
this._ipc = undefined;
await IModelApp.shutdown();
}
}
/**
* Base class for all implementations of an Ipc notification response interface. This class is implemented on your frontend to supply
* methods to receive notifications from your backend.
*
* Create a subclass to implement your Ipc response interface. Your class should be declared like this:
* ```ts
* class MyNotificationHandler extends NotificationHandler implements MyNotifications
* ```
* to ensure all method names and signatures are correct. Your methods cannot have a return value.
*
* Then, call `MyNotificationHandler.register` at startup to connect your class to your channel.
* @public
* @extensions
*/
export abstract class NotificationHandler {
/** All subclasses must implement this method to specify their response channel name. */
public abstract get channelName(): string;
public registerImpl(): RemoveFunction {
return IpcApp.addListener(this.channelName, (_evt: Event, funcName: string, ...args: any[]) => {
const func = (this as any)[funcName];
if (typeof func !== "function")
throw new IModelError(IModelStatus.FunctionNotFound, `Method "${this.constructor.name}.${funcName}" not found on NotificationHandler registered for channel: ${this.channelName}`);
func.call(this, ...args);
});
}
/**
* Register this class as the handler for notifications on its channel. This static method creates a new instance
* that becomes the notification 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, multiple handlers are established.
*/
public static register(): RemoveFunction {
return (new (this as any)() as NotificationHandler).registerImpl(); // create an instance of subclass. "as any" is necessary because base class is abstract
}
}
/** IpcApp notifications from backend */
class IpcAppNotifyHandler extends NotificationHandler implements IpcAppNotifications {
public get channelName() { return ipcAppChannels.appNotify; }
public notifyApp() { }
}