1 | import uuid from 'uuid/v4';
|
2 | import { serializeError } from 'serialize-error';
|
3 | import type { IpcMain, IpcRenderer, WebContents, IpcMainEvent, IpcRendererEvent } from 'electron';
|
4 | import 'object.entries/auto';
|
5 |
|
6 | type IpcEvent = IpcRendererEvent & IpcMainEvent;
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | export type Listener =
|
13 | | { (event?: IpcEvent): void }
|
14 | | { (arg1?: unknown, event?: IpcEvent): void }
|
15 | | { (arg1?: unknown, arg2?: unknown, event?: IpcEvent): void }
|
16 | | { (arg1?: unknown, arg2?: unknown, arg3?: unknown, event?: IpcEvent): void }
|
17 | | {
|
18 | (
|
19 | arg1?: unknown,
|
20 | arg2?: unknown,
|
21 | arg3?: unknown,
|
22 | arg4?: unknown,
|
23 | event?: IpcEvent,
|
24 | ): void;
|
25 | }
|
26 | | {
|
27 | (
|
28 | arg1?: unknown,
|
29 | arg2?: unknown,
|
30 | arg3?: unknown,
|
31 | arg4?: unknown,
|
32 | arg5?: unknown,
|
33 | event?: IpcEvent,
|
34 | ): void;
|
35 | };
|
36 | export type Options = { maxTimeoutMs?: number };
|
37 |
|
38 |
|
39 | type WrappedListener = { (event: IpcEvent, replyChannel: string, ...dataArgs: any[]): void };
|
40 |
|
41 | export default class PromiseIpcBase {
|
42 | private eventEmitter: IpcMain | IpcRenderer;
|
43 |
|
44 | private maxTimeoutMs: number;
|
45 |
|
46 | private routeListenerMap: Map<string, Listener>;
|
47 |
|
48 | private listenerMap: Map<Listener, WrappedListener>;
|
49 |
|
50 | constructor(opts: { maxTimeoutMs?: number } | undefined, eventEmitter: IpcMain | IpcRenderer) {
|
51 | if (opts && opts.maxTimeoutMs) {
|
52 | this.maxTimeoutMs = opts.maxTimeoutMs;
|
53 | }
|
54 |
|
55 | this.eventEmitter = eventEmitter;
|
56 | this.routeListenerMap = new Map();
|
57 | this.listenerMap = new Map();
|
58 | }
|
59 |
|
60 | public send(
|
61 | route: string,
|
62 | sender: WebContents | IpcRenderer,
|
63 | ...dataArgs: unknown[]
|
64 | ): Promise<unknown> {
|
65 | return new Promise((resolve, reject) => {
|
66 | const replyChannel = `${route}#${uuid()}`;
|
67 | let timeout: NodeJS.Timeout;
|
68 | let didTimeOut = false;
|
69 |
|
70 | this.eventEmitter.once(
|
71 | replyChannel,
|
72 | (event: IpcEvent, status: string, returnData: unknown) => {
|
73 | clearTimeout(timeout);
|
74 | if (didTimeOut) {
|
75 | return null;
|
76 | }
|
77 | switch (status) {
|
78 | case 'success':
|
79 | return resolve(returnData);
|
80 | case 'failure':
|
81 | return reject(returnData);
|
82 | default:
|
83 | return reject(new Error(`Unexpected IPC call status "${status}" in ${route}`));
|
84 | }
|
85 | },
|
86 | );
|
87 | sender.send(route, replyChannel, ...dataArgs);
|
88 | if (this.maxTimeoutMs) {
|
89 | timeout = setTimeout(() => {
|
90 | didTimeOut = true;
|
91 | reject(new Error(`${route} timed out.`));
|
92 | }, this.maxTimeoutMs);
|
93 | }
|
94 | });
|
95 | }
|
96 |
|
97 | public on(route: string, listener: Listener): PromiseIpcBase {
|
98 | const prevListener = this.routeListenerMap.get(route);
|
99 | if (prevListener === listener) {
|
100 | return this;
|
101 | }
|
102 | if (this.routeListenerMap.has(route)) {
|
103 | this.off(route, prevListener);
|
104 | }
|
105 | const wrappedListener: WrappedListener = (event, replyChannel, ...dataArgs): void => {
|
106 |
|
107 |
|
108 | Promise.resolve()
|
109 | .then(() => listener(...dataArgs, event))
|
110 | .then((results) => {
|
111 | event.sender.send(replyChannel, 'success', results);
|
112 | })
|
113 | .catch((e) => {
|
114 | event.sender.send(replyChannel, 'failure', serializeError(e));
|
115 | });
|
116 | };
|
117 | this.routeListenerMap.set(route, listener);
|
118 | this.listenerMap.set(listener, wrappedListener);
|
119 | this.eventEmitter.on(route, wrappedListener);
|
120 | return this;
|
121 | }
|
122 |
|
123 | public off(route: string, listener?: Listener): void {
|
124 | const registeredListener = this.routeListenerMap.get(route);
|
125 | if (listener && listener !== registeredListener) {
|
126 | return;
|
127 | }
|
128 | const wrappedListener = this.listenerMap.get(registeredListener);
|
129 | this.eventEmitter.removeListener(route, wrappedListener);
|
130 | this.listenerMap.delete(registeredListener);
|
131 | this.routeListenerMap.delete(route);
|
132 | }
|
133 |
|
134 | public removeListener(route: string, listener?: Listener): void {
|
135 | this.off(route, listener);
|
136 | }
|
137 | }
|