aboutsummaryrefslogtreecommitdiffstats
path: root/packages/preload/src/contextBridge/createSophieRenderer.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/preload/src/contextBridge/createSophieRenderer.ts')
-rw-r--r--packages/preload/src/contextBridge/createSophieRenderer.ts96
1 files changed, 96 insertions, 0 deletions
diff --git a/packages/preload/src/contextBridge/createSophieRenderer.ts b/packages/preload/src/contextBridge/createSophieRenderer.ts
new file mode 100644
index 0000000..2055080
--- /dev/null
+++ b/packages/preload/src/contextBridge/createSophieRenderer.ts
@@ -0,0 +1,96 @@
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 action,
24 MainToRendererIpcMessage,
25 RendererToMainIpcMessage,
26 sharedStore,
27 SharedStoreListener,
28 SophieRenderer,
29} from '@sophie/shared';
30import { ipcRenderer } from 'electron';
31import log from 'loglevel';
32import type { IJsonPatch } from 'mobx-state-tree';
33
34class SharedStoreConnector {
35 private onSharedStoreChangeCalled = false;
36
37 private listener: SharedStoreListener | null = null;
38
39 constructor(private readonly allowReplaceListener: boolean) {
40 ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_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 (err) {
45 log.error('Shared store listener onPatch failed', err);
46 this.listener = null;
47 }
48 });
49 }
50
51 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> {
52 if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) {
53 throw new Error('Shared store change listener was already set');
54 }
55 this.onSharedStoreChangeCalled = true;
56 let success = false;
57 let snapshot: unknown | null = null;
58 try {
59 snapshot = await ipcRenderer.invoke(RendererToMainIpcMessage.GetSharedStoreSnapshot);
60 success = true;
61 } catch (err) {
62 log.error('Failed to get initial shared store snapshot', err);
63 }
64 if (success) {
65 if (sharedStore.is(snapshot)) {
66 listener.onSnapshot(snapshot);
67 this.listener = listener;
68 return;
69 }
70 log.error('Got invalid initial shared store snapshot', snapshot);
71 }
72 throw new Error('Failed to connect to shared store');
73 }
74}
75
76function 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 (err) {
83 // Do not leak IPC failure details into the main world.
84 const message = 'Failed to dispatch action';
85 log.error(message, actionToDispatch, err);
86 throw new Error(message);
87 }
88}
89
90export default function createSophieRenderer(allowReplaceListener: boolean): SophieRenderer {
91 const connector = new SharedStoreConnector(allowReplaceListener);
92 return {
93 onSharedStoreChange: connector.onSharedStoreChange.bind(connector),
94 dispatchAction,
95 };
96}