summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-12-03 15:48:29 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-12-09 13:40:44 +0100
commitc94bc940b646f374d4a6f99393d6370bca1973b4 (patch)
treee351ca7461c93b12b05cd3c87524cadc39c29a5d /gui
parent62947d7d0e1376f6f3601b63ac7a78647ebc5b52 (diff)
downloadmullvadvpn-c94bc940b646f374d4a6f99393d6370bca1973b4.tar.xz
mullvadvpn-c94bc940b646f374d4a6f99393d6370bca1973b4.zip
Add new IPC helper functions
Diffstat (limited to 'gui')
-rw-r--r--gui/src/shared/account-expiry.ts7
-rw-r--r--gui/src/shared/ipc-helpers.ts184
-rw-r--r--gui/src/shared/string-helpers.ts3
3 files changed, 189 insertions, 5 deletions
diff --git a/gui/src/shared/account-expiry.ts b/gui/src/shared/account-expiry.ts
index 4511797f3c..a76eb6ecc4 100644
--- a/gui/src/shared/account-expiry.ts
+++ b/gui/src/shared/account-expiry.ts
@@ -1,6 +1,7 @@
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import { messages } from './gettext';
+import { capitalize } from './string-helpers';
type DateArgument = string | Date | moment.Moment;
@@ -47,9 +48,5 @@ export function formatRemainingTime(
{ duration },
);
- return shouldCapitalizeFirstLetter ? capitalizeFirstLetter(remaining) : remaining;
-}
-
-function capitalizeFirstLetter(inputString: string): string {
- return inputString.charAt(0).toUpperCase() + inputString.slice(1);
+ return shouldCapitalizeFirstLetter ? capitalize(remaining) : remaining;
}
diff --git a/gui/src/shared/ipc-helpers.ts b/gui/src/shared/ipc-helpers.ts
new file mode 100644
index 0000000000..1c1465b09b
--- /dev/null
+++ b/gui/src/shared/ipc-helpers.ts
@@ -0,0 +1,184 @@
+import { ipcMain, ipcRenderer, WebContents } from 'electron';
+import log from 'electron-log';
+import { capitalize } from './string-helpers';
+
+type Handler<T, R> = (callback: (arg: T) => R) => void;
+type Sender<T, R> = (arg: T) => R;
+type Notifier<T> = (webContents: WebContents, arg: T) => void;
+type Listener<T> = (callback: (arg: T) => void) => void;
+
+interface IpcCall<T, R> {
+ direction: 'renderer-to-main' | 'main-to-renderer';
+ send: (event: string) => Notifier<T> | Sender<T, R>;
+ receive: (event: string) => Listener<T> | Handler<T, R>;
+}
+
+interface MainToRenderer<T> extends IpcCall<T, never> {
+ direction: 'main-to-renderer';
+ send: (event: string) => Notifier<T>;
+ receive: (event: string) => Listener<T>;
+}
+
+interface RendererToMain<T, R> extends IpcCall<T, R> {
+ direction: 'renderer-to-main';
+ send: (event: string) => Sender<T, R>;
+ receive: (event: string) => Handler<T, R>;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type AnyIpcCall = IpcCall<any, any>;
+type Schema = Record<string, Record<string, AnyIpcCall>>;
+
+// Renames all IPC calls, e.g. `callName` to either `notifyCallName` or `handleCallName` depending
+// on direction.
+type IpcMainKey<N extends string, I extends AnyIpcCall> = I['direction'] extends 'main-to-renderer'
+ ? `notify${Capitalize<N>}`
+ : `handle${Capitalize<N>}`;
+
+// Selects either the send or receive function depending on direction.
+type IpcMainFn<I extends AnyIpcCall> = I['direction'] extends 'main-to-renderer'
+ ? ReturnType<I['send']>
+ : ReturnType<I['receive']>;
+
+// Renames all receiving IPC calls, e.g. `callName` to `listenCallName`.
+type IpcRendererKey<
+ N extends string,
+ I extends AnyIpcCall
+> = I['direction'] extends 'main-to-renderer' ? `listen${Capitalize<N>}` : N;
+
+// Selects either the send or receive function depending on direction.
+type IpcRendererFn<I extends AnyIpcCall> = I['direction'] extends 'main-to-renderer'
+ ? ReturnType<I['receive']>
+ : ReturnType<I['send']>;
+
+// Transforms the provided schema to the correct type for the main event channel.
+type IpcMain<S extends Schema> = {
+ [G in keyof S]: {
+ [K in keyof S[G] as IpcMainKey<string & K, S[G][K]>]: IpcMainFn<S[G][K]>;
+ };
+};
+
+// Transforms the provided schema to the correct type for the renderer event channel.
+type IpcRenderer<S extends Schema> = {
+ [G in keyof S]: {
+ [K in keyof S[G] as IpcRendererKey<string & K, S[G][K]>]: IpcRendererFn<S[G][K]>;
+ };
+};
+
+// Preforms the transformation of the main event channel in accordance with the above types.
+export function createIpcMain<S extends Schema>(ipc: S): IpcMain<S> {
+ return createIpc(ipc, (event, key, spec) => {
+ const capitalizedKey = capitalize(key);
+ const newKey =
+ spec.direction === 'main-to-renderer' ? `notify${capitalizedKey}` : `handle${capitalizedKey}`;
+ const newValue = spec.direction === 'main-to-renderer' ? spec.send(event) : spec.receive(event);
+
+ return [newKey, newValue];
+ });
+}
+
+// Preforms the transformation of the renderer event channel in accordance with the above types.
+export function createIpcRenderer<S extends Schema>(ipc: S): IpcRenderer<S> {
+ return createIpc(ipc, (event, key, spec) => {
+ const newKey = spec.direction === 'main-to-renderer' ? `listen${capitalize(key)}` : key;
+ const newValue = spec.direction === 'main-to-renderer' ? spec.receive(event) : spec.send(event);
+
+ return [newKey, newValue];
+ });
+}
+
+function createIpc<S extends Schema, T, R extends IpcMain<S> | IpcRenderer<S>>(
+ ipc: S,
+ fn: (event: string, key: string, spec: AnyIpcCall) => [newKey: string, newValue: T],
+): R {
+ return Object.fromEntries(
+ Object.entries(ipc).map(([groupKey, group]) => {
+ const newGroup = Object.fromEntries(
+ Object.entries(group).map(([key, spec]) => fn(`${groupKey}-${key}`, key, spec)),
+ );
+ return [groupKey, newGroup];
+ }),
+ ) as R;
+}
+
+// Sends a request from the renderer process to the main process without any possibility to respond.
+export function send<T>(): RendererToMain<T, void> {
+ return {
+ direction: 'renderer-to-main',
+ send: (event: string) => (newValue: T) => ipcRenderer.send(event, newValue),
+ receive: (event: string) => (handlerFn: (value: T) => void) => {
+ ipcMain.on(event, (_event, newValue: T) => {
+ handlerFn(newValue);
+ });
+ },
+ };
+}
+
+// Sends a synchronous request from the renderer process to the main process.
+export function invokeSync<T, R>(): RendererToMain<T, R> {
+ return {
+ direction: 'renderer-to-main',
+ send: (event: string) => (newValue: T) => ipcRenderer.sendSync(event, newValue),
+ receive: (event: string) => (handlerFn: (value: T) => R) => {
+ ipcMain.on(event, (ipcEvent, newValue: T) => {
+ ipcEvent.returnValue = handlerFn(newValue);
+ });
+ },
+ };
+}
+
+// Sends an asynchronous request from the renderer process to the main process.
+export function invoke<T, R>(): RendererToMain<T, Promise<R>> {
+ return {
+ direction: 'renderer-to-main',
+ send: invokeImpl,
+ receive: handle,
+ };
+}
+
+// Sends a request from the main process to the renderer process without any possibility to respond.
+export function notifyRenderer<T>(): MainToRenderer<T> {
+ return {
+ direction: 'main-to-renderer',
+ send: notifyRendererImpl,
+ receive: (event: string) => (fn: (value: T) => void) => {
+ ipcRenderer.on(event, (_event, newState: T) => fn(newState));
+ },
+ };
+}
+
+function notifyRendererImpl<T>(event: string): Notifier<T> {
+ return (webContents: WebContents, value: T) => {
+ if (webContents.isDestroyed()) {
+ log.error(`sender(${event}): webContents is already destroyed!`);
+ } else {
+ webContents.send(event, value);
+ }
+ };
+}
+
+type RequestResult<T> = { type: 'success'; value: T } | { type: 'error'; message: string };
+
+function invokeImpl<T, R>(event: string): Sender<T, Promise<R>> {
+ return async (arg: T): Promise<R> => {
+ const result: RequestResult<R> = await ipcRenderer.invoke(event, arg);
+ switch (result.type) {
+ case 'error':
+ throw new Error(result.message);
+ case 'success':
+ return result.value;
+ }
+ };
+}
+
+function handle<T, R>(event: string): Handler<T, Promise<R>> {
+ return (fn: (arg: T) => Promise<R>) => {
+ ipcMain.handle(event, async (_ipcEvent, arg: T) => {
+ try {
+ return { type: 'success', value: await fn(arg) };
+ } catch (error) {
+ return { type: 'error', message: error.message || '' };
+ }
+ });
+ };
+}
diff --git a/gui/src/shared/string-helpers.ts b/gui/src/shared/string-helpers.ts
new file mode 100644
index 0000000000..30a2ac9d58
--- /dev/null
+++ b/gui/src/shared/string-helpers.ts
@@ -0,0 +1,3 @@
+export function capitalize(inputString: string): string {
+ return inputString.charAt(0).toUpperCase() + inputString.slice(1);
+}