aboutsummaryrefslogtreecommitdiffstats
path: root/packages/preload/src/contextBridge
diff options
context:
space:
mode:
Diffstat (limited to 'packages/preload/src/contextBridge')
-rw-r--r--packages/preload/src/contextBridge/SharedStoreConnector.ts88
-rw-r--r--packages/preload/src/contextBridge/TranslationsConnector.ts71
-rw-r--r--packages/preload/src/contextBridge/createSophieRenderer.ts88
3 files changed, 171 insertions, 76 deletions
diff --git a/packages/preload/src/contextBridge/SharedStoreConnector.ts b/packages/preload/src/contextBridge/SharedStoreConnector.ts
new file mode 100644
index 0000000..13f9ac1
--- /dev/null
+++ b/packages/preload/src/contextBridge/SharedStoreConnector.ts
@@ -0,0 +1,88 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import {
22 Action,
23 MainToRendererIpcMessage,
24 RendererToMainIpcMessage,
25 SharedStoreListener,
26 SharedStoreSnapshotIn,
27} from '@sophie/shared';
28import { ipcRenderer } from 'electron';
29import log from 'loglevel';
30import type { IJsonPatch } from 'mobx-state-tree';
31
32export default class SharedStoreConnector {
33 private onSharedStoreChangeCalled = false;
34
35 private listener: SharedStoreListener | undefined;
36
37 constructor(private readonly allowReplaceListener: boolean) {
38 ipcRenderer.on(
39 MainToRendererIpcMessage.SharedStorePatch,
40 (_event, patch) => {
41 try {
42 // `mobx-state-tree` will validate the patch, so we can safely cast here.
43 this.listener?.onPatch(patch as IJsonPatch[]);
44 } catch (error) {
45 log.error('Shared store listener onPatch failed', error);
46 this.listener = undefined;
47 }
48 },
49 );
50 }
51
52 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> {
53 if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) {
54 throw new Error('Shared store change listener was already set');
55 }
56 this.onSharedStoreChangeCalled = true;
57 let success = false;
58 let snapshot: unknown;
59 try {
60 snapshot = await ipcRenderer.invoke(
61 RendererToMainIpcMessage.GetSharedStoreSnapshot,
62 );
63 success = true;
64 } catch (error) {
65 log.error('Failed to get initial shared store snapshot', error);
66 }
67 if (!success) {
68 throw new Error('Failed to connect to shared store');
69 }
70 // `mobx-state-tree` will validate the snapshot, so we can safely cast here.
71 listener.onSnapshot(snapshot as SharedStoreSnapshotIn);
72 this.listener = listener;
73 }
74}
75
76export function dispatchAction(actionToDispatch: Action): void {
77 // Let the full zod parse error bubble up to the main world,
78 // since all data it may contain was provided from the main world.
79 const parsedAction = Action.parse(actionToDispatch);
80 try {
81 ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction);
82 } catch (error) {
83 // Do not leak IPC failure details into the main world.
84 const message = 'Failed to dispatch action';
85 log.error(message, actionToDispatch, error);
86 throw new Error(message);
87 }
88}
diff --git a/packages/preload/src/contextBridge/TranslationsConnector.ts b/packages/preload/src/contextBridge/TranslationsConnector.ts
new file mode 100644
index 0000000..284b793
--- /dev/null
+++ b/packages/preload/src/contextBridge/TranslationsConnector.ts
@@ -0,0 +1,71 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import {
22 MainToRendererIpcMessage,
23 RendererToMainIpcMessage,
24 Translation,
25} from '@sophie/shared';
26import { ipcRenderer } from 'electron';
27import type { ResourceKey } from 'i18next';
28import log from 'loglevel';
29
30export default class TranslationsConnector {
31 private listener: (() => void) | undefined;
32
33 constructor(private readonly devMode: boolean) {
34 ipcRenderer.on(MainToRendererIpcMessage.ReloadTranslations, () => {
35 try {
36 this.listener?.();
37 } catch (error) {
38 log.error('Translations listener onReloadTranslations failed', error);
39 this.listener = undefined;
40 }
41 });
42 }
43
44 onReloadTranslations(listener: () => void): void {
45 if (!this.devMode) {
46 throw new Error(
47 'Translation reloading is only supported in development mode',
48 );
49 }
50 this.listener = listener;
51 }
52}
53
54export async function getTranslation(
55 translation: Translation,
56): Promise<ResourceKey> {
57 const parsedTranslation = Translation.parse(translation);
58 try {
59 // We don't have any way to validate translations,
60 // but they are coming from a trusted source and will be validated
61 // in the renderer anyways, so we should be fine.
62 return (await ipcRenderer.invoke(
63 RendererToMainIpcMessage.GetTranslation,
64 parsedTranslation,
65 )) as ResourceKey;
66 } catch (error) {
67 const message = 'Failed to get translation';
68 log.error(message, translation, error);
69 throw new Error(message);
70 }
71}
diff --git a/packages/preload/src/contextBridge/createSophieRenderer.ts b/packages/preload/src/contextBridge/createSophieRenderer.ts
index 8bdf07e..3de9d9e 100644
--- a/packages/preload/src/contextBridge/createSophieRenderer.ts
+++ b/packages/preload/src/contextBridge/createSophieRenderer.ts
@@ -18,85 +18,21 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { 21import { SophieRenderer } from '@sophie/shared';
22 Action,
23 MainToRendererIpcMessage,
24 RendererToMainIpcMessage,
25 SharedStoreListener,
26 SharedStoreSnapshotIn,
27 SophieRenderer,
28} from '@sophie/shared';
29import { ipcRenderer } from 'electron';
30import log from 'loglevel';
31import type { IJsonPatch } from 'mobx-state-tree';
32 22
33class SharedStoreConnector { 23import SharedStoreConnector, { dispatchAction } from './SharedStoreConnector';
34 readonly #allowReplaceListener: boolean; 24import TranslationsConnector, { getTranslation } from './TranslationsConnector';
35 25
36 #onSharedStoreChangeCalled = false; 26export default function createSophieRenderer(devMode: boolean): SophieRenderer {
37 27 const sharedStoreConnector = new SharedStoreConnector(devMode);
38 #listener: SharedStoreListener | undefined; 28 const translationsConnector = new TranslationsConnector(devMode);
39
40 constructor(allowReplaceListener: boolean) {
41 this.#allowReplaceListener = allowReplaceListener;
42 ipcRenderer.on(
43 MainToRendererIpcMessage.SharedStorePatch,
44 (_event, patch) => {
45 try {
46 // `mobx-state-tree` will validate the patch, so we can safely cast here.
47 this.#listener?.onPatch(patch as IJsonPatch[]);
48 } catch (error) {
49 log.error('Shared store listener onPatch failed', error);
50 this.#listener = undefined;
51 }
52 },
53 );
54 }
55
56 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> {
57 if (this.#onSharedStoreChangeCalled && !this.#allowReplaceListener) {
58 throw new Error('Shared store change listener was already set');
59 }
60 this.#onSharedStoreChangeCalled = true;
61 let success = false;
62 let snapshot: unknown;
63 try {
64 snapshot = await ipcRenderer.invoke(
65 RendererToMainIpcMessage.GetSharedStoreSnapshot,
66 );
67 success = true;
68 } catch (error) {
69 log.error('Failed to get initial shared store snapshot', error);
70 }
71 if (!success) {
72 throw new Error('Failed to connect to shared store');
73 }
74 // `mobx-state-tree` will validate the snapshot, so we can safely cast here.
75 listener.onSnapshot(snapshot as SharedStoreSnapshotIn);
76 this.#listener = listener;
77 }
78}
79
80function dispatchAction(actionToDispatch: Action): void {
81 // Let the full zod parse error bubble up to the main world,
82 // since all data it may contain was provided from the main world.
83 const parsedAction = Action.parse(actionToDispatch);
84 try {
85 ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction);
86 } catch (error) {
87 // Do not leak IPC failure details into the main world.
88 const message = 'Failed to dispatch action';
89 log.error(message, actionToDispatch, error);
90 throw new Error(message);
91 }
92}
93
94export default function createSophieRenderer(
95 allowReplaceListener: boolean,
96): SophieRenderer {
97 const connector = new SharedStoreConnector(allowReplaceListener);
98 return { 29 return {
99 onSharedStoreChange: connector.onSharedStoreChange.bind(connector), 30 onSharedStoreChange:
31 sharedStoreConnector.onSharedStoreChange.bind(sharedStoreConnector),
100 dispatchAction, 32 dispatchAction,
33 getTranslation,
34 onReloadTranslations: translationsConnector.onReloadTranslations.bind(
35 translationsConnector,
36 ),
101 }; 37 };
102} 38}