aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-26 21:12:08 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-26 21:15:53 +0100
commita2651dff66faf98dc1a6f924227af454eaa2311d (patch)
tree7f15c402d449b934d5a2b7858aa16523e9fc121e
parentrefactor: Rename RootStore to RendererStore (diff)
downloadsophie-a2651dff66faf98dc1a6f924227af454eaa2311d.tar.gz
sophie-a2651dff66faf98dc1a6f924227af454eaa2311d.tar.zst
sophie-a2651dff66faf98dc1a6f924227af454eaa2311d.zip
refactor: Less boilerplate around SophieRenderer
-rw-r--r--packages/main/src/index.ts48
-rw-r--r--packages/preload/src/SophieRendererImpl.ts112
-rw-r--r--packages/preload/src/contextBridge/SophieRendererImpl.ts74
-rw-r--r--packages/preload/src/index.ts5
-rw-r--r--packages/preload/src/services/RendererToMainIpcService.ts42
-rw-r--r--packages/renderer/src/devTools.ts4
-rw-r--r--packages/renderer/src/stores/RendererEnv.ts4
-rw-r--r--packages/renderer/src/stores/RendererStore.ts25
-rw-r--r--packages/shared/src/contextBridge/SophieRenderer.ts10
-rw-r--r--packages/shared/src/index.ts2
-rw-r--r--packages/shared/src/ipc.ts7
-rw-r--r--packages/shared/src/schemas.ts22
12 files changed, 201 insertions, 154 deletions
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index 67f5546..7aa3ee9 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -35,9 +35,8 @@ import {
35 WebSource, 35 WebSource,
36} from '@sophie/service-shared'; 36} from '@sophie/service-shared';
37import { 37import {
38 browserViewBounds, 38 action,
39 MainToRendererIpcMessage, 39 MainToRendererIpcMessage,
40 themeSource,
41 RendererToMainIpcMessage, 40 RendererToMainIpcMessage,
42} from '@sophie/shared'; 41} from '@sophie/shared';
43import { URL } from 'url'; 42import { URL } from 'url';
@@ -194,19 +193,39 @@ function createWindow(): Promise<unknown> {
194 }); 193 });
195 mainWindow.setBrowserView(browserView); 194 mainWindow.setBrowserView(browserView);
196 195
197 webContents.on('ipc-message', (_event, channel, ...args) => { 196 ipcMain.handle(RendererToMainIpcMessage.GetSharedStoreSnapshot, (event) => {
197 if (event.sender.id !== webContents.id) {
198 console.warn(
199 'Unexpected',
200 RendererToMainIpcMessage.GetSharedStoreSnapshot,
201 'from webContents',
202 event.sender.id,
203 );
204 return null;
205 }
206 return getSnapshot(store.shared);
207 });
208
209 ipcMain.on(RendererToMainIpcMessage.DispatchAction, (event, rawAction) => {
210 if (event.sender.id !== webContents.id) {
211 console.warn(
212 'Unexpected',
213 RendererToMainIpcMessage.DispatchAction,
214 'from webContents',
215 event.sender.id,
216 );
217 return;
218 }
198 try { 219 try {
199 switch (channel) { 220 const actionToDispatch = action.parse(rawAction);
200 case RendererToMainIpcMessage.SharedStoreSnapshotRequest: 221 switch (actionToDispatch.action) {
201 webContents.send(MainToRendererIpcMessage.SharedStoreSnapshot, getSnapshot(store.shared)); 222 case 'set-browser-view-bounds':
223 store.setBrowserViewBounds(actionToDispatch.browserViewBounds);
202 break; 224 break;
203 case RendererToMainIpcMessage.SetBrowserViewBounds: 225 case 'set-theme-source':
204 store.setBrowserViewBounds(browserViewBounds.parse(args[0])); 226 store.config.setThemeSource(actionToDispatch.themeSource)
205 break; 227 break;
206 case RendererToMainIpcMessage.SetThemeSource: 228 case 'reload-all-services':
207 store.config.setThemeSource(themeSource.parse(args[0]))
208 break;
209 case RendererToMainIpcMessage.ReloadAllServices:
210 readFile(serviceInjectPath, 'utf8').then((data) => { 229 readFile(serviceInjectPath, 'utf8').then((data) => {
211 serviceInject.code = data; 230 serviceInject.code = data;
212 }).catch((err) => { 231 }).catch((err) => {
@@ -215,12 +234,9 @@ function createWindow(): Promise<unknown> {
215 browserView.webContents.reload(); 234 browserView.webContents.reload();
216 }); 235 });
217 break; 236 break;
218 default:
219 console.error('Unknown IPC message:', channel, args);
220 break;
221 } 237 }
222 } catch (err) { 238 } catch (err) {
223 console.error('Error while processing IPC message:', channel, args, err); 239 console.error('Error while dispatching renderer action', rawAction, err);
224 } 240 }
225 }); 241 });
226 242
diff --git a/packages/preload/src/SophieRendererImpl.ts b/packages/preload/src/SophieRendererImpl.ts
deleted file mode 100644
index a06433f..0000000
--- a/packages/preload/src/SophieRendererImpl.ts
+++ /dev/null
@@ -1,112 +0,0 @@
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 { ipcRenderer } from 'electron';
22import type { IJsonPatch } from 'mobx-state-tree';
23import {
24 BrowserViewBounds,
25 browserViewBounds,
26 MainToRendererIpcMessage,
27 RendererToMainIpcMessage,
28 sharedStore,
29 SharedStoreListener,
30 SharedStoreSnapshotIn,
31 SophieRenderer,
32 themeSource,
33 ThemeSource,
34} from '@sophie/shared';
35
36export type MessageSender = (channel: RendererToMainIpcMessage, ...args: unknown[]) => void;
37
38export class SophieRendererImpl implements SophieRenderer {
39 readonly #send: MessageSender;
40
41 #listener: SharedStoreListener | null = null;
42
43 #snapshot: SharedStoreSnapshotIn | null = null;
44
45 constructor(send: MessageSender) {
46 this.#send = send;
47 }
48
49 sharedStoreSnapshotReceived(snapshot: unknown): void {
50 if (sharedStore.is(snapshot)) {
51 if (this.#listener === null) {
52 this.#snapshot = snapshot;
53 } else {
54 this.#listener.onSnapshot(snapshot);
55 }
56 } else {
57 console.error('Received invalid snapshot', snapshot);
58 this.#snapshot = null;
59 }
60 }
61
62 sharedStorePatchReceived(patch: unknown): void {
63 if (this.#listener !== null) {
64 // `mobx-state-tree` will validate the patch, so we can safely cast here.
65 this.#listener.onPatch(patch as IJsonPatch);
66 }
67 }
68
69 setSharedStoreListener(listener: SharedStoreListener): void {
70 this.#listener = listener;
71 if (this.#snapshot !== null) {
72 listener.onSnapshot(this.#snapshot);
73 this.#snapshot = null;
74 }
75 this.#send(RendererToMainIpcMessage.SharedStoreSnapshotRequest);
76 }
77
78 setBrowserViewBounds(bounds: BrowserViewBounds): void {
79 if (browserViewBounds.safeParse(bounds).success) {
80 this.#send(RendererToMainIpcMessage.SetBrowserViewBounds, bounds);
81 }
82 }
83
84 setThemeSource(mode: ThemeSource): void {
85 if (themeSource.safeParse(mode).success) {
86 this.#send(RendererToMainIpcMessage.SetThemeSource, mode);
87 }
88 }
89
90 reloadAllServices(): void {
91 this.#send(RendererToMainIpcMessage.ReloadAllServices);
92 }
93}
94
95export function createSophieRenderer(): SophieRenderer {
96 const impl = new SophieRendererImpl(ipcRenderer.send);
97
98 ipcRenderer.on(MainToRendererIpcMessage.SharedStoreSnapshot, (_event, snapshot) => {
99 impl.sharedStoreSnapshotReceived(snapshot);
100 });
101
102 ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => {
103 impl.sharedStorePatchReceived(patch);
104 });
105
106 return {
107 setSharedStoreListener: impl.setSharedStoreListener.bind(impl),
108 setBrowserViewBounds: impl.setBrowserViewBounds.bind(impl),
109 setThemeSource: impl.setThemeSource.bind(impl),
110 reloadAllServices: impl.reloadAllServices.bind(impl),
111 };
112}
diff --git a/packages/preload/src/contextBridge/SophieRendererImpl.ts b/packages/preload/src/contextBridge/SophieRendererImpl.ts
new file mode 100644
index 0000000..5d29071
--- /dev/null
+++ b/packages/preload/src/contextBridge/SophieRendererImpl.ts
@@ -0,0 +1,74 @@
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 type { IJsonPatch } from 'mobx-state-tree';
22import {
23 Action,
24 action,
25 sharedStore,
26 SharedStoreListener,
27 SophieRenderer,
28} from '@sophie/shared';
29
30import { RendererToMainIpcService } from '../services/RendererToMainIpcService';
31
32class SophieRendererImpl implements SophieRenderer {
33 readonly #ipcService: RendererToMainIpcService;
34
35 #listener: SharedStoreListener | null = null;
36
37 constructor(ipcService: RendererToMainIpcService) {
38 this.#ipcService = ipcService;
39 ipcService.onSharedStorePatch((patch) => {
40 // `mobx-state-tree` will validate the patch, so we can safely cast here.
41 this.#listener?.onPatch(patch as IJsonPatch);
42 });
43 }
44
45 onSharedStoreChange(listener: SharedStoreListener): void {
46 this.#ipcService.getSharedStoreSnapshot().then((snapshot) => {
47 if (sharedStore.is(snapshot)) {
48 listener.onSnapshot(snapshot);
49 this.#listener = listener;
50 } else {
51 console.error('Got invalid initial shared store snapshot', snapshot);
52 }
53 }).catch((err) => {
54 console.error('Failed set initial shared store snapshot', err);
55 });
56 }
57
58 dispatchAction(actionToDispatch: Action): void {
59 const parsedAction = action.safeParse(actionToDispatch);
60 if (parsedAction.success) {
61 this.#ipcService.dispatchAction(parsedAction.data);
62 } else {
63 console.error('Trying to dispatch invalid action', actionToDispatch, parsedAction.error);
64 }
65 }
66}
67
68export function createSophieRenderer(ipcService: RendererToMainIpcService): SophieRenderer {
69 const impl = new SophieRendererImpl(ipcService);
70 return {
71 onSharedStoreChange: impl.onSharedStoreChange.bind(impl),
72 dispatchAction: impl.dispatchAction.bind(impl),
73 };
74}
diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts
index ef85f70..9336433 100644
--- a/packages/preload/src/index.ts
+++ b/packages/preload/src/index.ts
@@ -20,8 +20,9 @@
20 20
21import { contextBridge } from 'electron'; 21import { contextBridge } from 'electron';
22 22
23import { createSophieRenderer } from './SophieRendererImpl'; 23import { createSophieRenderer } from './contextBridge/SophieRendererImpl';
24import { RendererToMainIpcService } from './services/RendererToMainIpcService';
24 25
25const sophieRenderer = createSophieRenderer(); 26const sophieRenderer = createSophieRenderer(new RendererToMainIpcService());
26 27
27contextBridge.exposeInMainWorld('sophieRenderer', sophieRenderer); 28contextBridge.exposeInMainWorld('sophieRenderer', sophieRenderer);
diff --git a/packages/preload/src/services/RendererToMainIpcService.ts b/packages/preload/src/services/RendererToMainIpcService.ts
new file mode 100644
index 0000000..40f1339
--- /dev/null
+++ b/packages/preload/src/services/RendererToMainIpcService.ts
@@ -0,0 +1,42 @@
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 { ipcRenderer } from 'electron';
22import {
23 Action,
24 MainToRendererIpcMessage,
25 RendererToMainIpcMessage,
26} from '@sophie/shared';
27
28export class RendererToMainIpcService {
29 getSharedStoreSnapshot(): Promise<unknown> {
30 return ipcRenderer.invoke(RendererToMainIpcMessage.GetSharedStoreSnapshot);
31 }
32
33 dispatchAction(actionToDispatch: Action): void {
34 ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, actionToDispatch);
35 }
36
37 onSharedStorePatch(callback: (patch: unknown) => void): void {
38 ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => {
39 callback(patch);
40 })
41 }
42}
diff --git a/packages/renderer/src/devTools.ts b/packages/renderer/src/devTools.ts
index 44c87ae..7c44559 100644
--- a/packages/renderer/src/devTools.ts
+++ b/packages/renderer/src/devTools.ts
@@ -64,7 +64,9 @@ export function hotReloadServices(): void {
64 import.meta.hot?.on( 64 import.meta.hot?.on(
65 'sophie:reload-services', 65 'sophie:reload-services',
66 () => { 66 () => {
67 window.sophieRenderer.reloadAllServices(); 67 window.sophieRenderer.dispatchAction({
68 action: 'reload-all-services',
69 });
68 }, 70 },
69 ); 71 );
70} 72}
diff --git a/packages/renderer/src/stores/RendererEnv.ts b/packages/renderer/src/stores/RendererEnv.ts
index f2ec519..d687738 100644
--- a/packages/renderer/src/stores/RendererEnv.ts
+++ b/packages/renderer/src/stores/RendererEnv.ts
@@ -19,10 +19,10 @@
19 */ 19 */
20 20
21import { getEnv as getAnyEnv, IAnyStateTreeNode } from 'mobx-state-tree'; 21import { getEnv as getAnyEnv, IAnyStateTreeNode } from 'mobx-state-tree';
22import type { SophieRenderer } from '@sophie/shared'; 22import type { Action } from '@sophie/shared';
23 23
24export interface RendererEnv { 24export interface RendererEnv {
25 ipc: Omit<SophieRenderer, 'setSharedStoreListener'>; 25 dispatchMainAction(action: Action): void;
26} 26}
27 27
28/** 28/**
diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts
index 001f23a..3de82ac 100644
--- a/packages/renderer/src/stores/RendererStore.ts
+++ b/packages/renderer/src/stores/RendererStore.ts
@@ -31,16 +31,22 @@ import {
31 ThemeSource, 31 ThemeSource,
32} from '@sophie/shared'; 32} from '@sophie/shared';
33 33
34import { getEnv } from './RendererEnv'; 34import { getEnv, RendererEnv } from './RendererEnv';
35 35
36export const rendererStore = types.model('RendererStore', { 36export const rendererStore = types.model('RendererStore', {
37 shared: types.optional(sharedStore, {}), 37 shared: types.optional(sharedStore, {}),
38}).actions((self) => ({ 38}).actions((self) => ({
39 setBrowserViewBounds(bounds: BrowserViewBounds) { 39 setBrowserViewBounds(browserViewBounds: BrowserViewBounds) {
40 getEnv(self).ipc.setBrowserViewBounds(bounds); 40 getEnv(self).dispatchMainAction({
41 action: 'set-browser-view-bounds',
42 browserViewBounds,
43 });
41 }, 44 },
42 setThemeSource(mode: ThemeSource) { 45 setThemeSource(themeSource: ThemeSource) {
43 getEnv(self).ipc.setThemeSource(mode); 46 getEnv(self).dispatchMainAction({
47 action: 'set-theme-source',
48 themeSource,
49 });
44 }, 50 },
45 toggleDarkMode() { 51 toggleDarkMode() {
46 if (self.shared.shouldUseDarkColors) { 52 if (self.shared.shouldUseDarkColors) {
@@ -62,11 +68,12 @@ export interface RendererStore extends Instance<typeof rendererStore> {}
62 * @param ipc The `sophieRenderer` context bridge. 68 * @param ipc The `sophieRenderer` context bridge.
63 */ 69 */
64export function createAndConnectRendererStore(ipc: SophieRenderer): RendererStore { 70export function createAndConnectRendererStore(ipc: SophieRenderer): RendererStore {
65 const store = rendererStore.create({}, { 71 const env: RendererEnv = {
66 ipc, 72 dispatchMainAction: ipc.dispatchAction,
67 }); 73 }
74 const store = rendererStore.create({}, env);
68 75
69 ipc.setSharedStoreListener({ 76 ipc.onSharedStoreChange({
70 onSnapshot(snapshot) { 77 onSnapshot(snapshot) {
71 applySnapshot(store.shared, snapshot); 78 applySnapshot(store.shared, snapshot);
72 }, 79 },
diff --git a/packages/shared/src/contextBridge/SophieRenderer.ts b/packages/shared/src/contextBridge/SophieRenderer.ts
index 6a2e432..a471250 100644
--- a/packages/shared/src/contextBridge/SophieRenderer.ts
+++ b/packages/shared/src/contextBridge/SophieRenderer.ts
@@ -20,14 +20,10 @@
20 20
21import { SharedStoreListener } from '../stores/SharedStore'; 21import { SharedStoreListener } from '../stores/SharedStore';
22 22
23import { BrowserViewBounds, ThemeSource } from '../schemas'; 23import { Action } from '../schemas';
24 24
25export interface SophieRenderer { 25export interface SophieRenderer {
26 setSharedStoreListener(listener: SharedStoreListener): void; 26 onSharedStoreChange(listener: SharedStoreListener): void;
27 27
28 setBrowserViewBounds(bounds: BrowserViewBounds): void; 28 dispatchAction(action: Action): void;
29
30 setThemeSource(mode: ThemeSource): void;
31
32 reloadAllServices(): void;
33} 29}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 713984e..2f7146c 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -26,10 +26,12 @@ export {
26} from './ipc'; 26} from './ipc';
27 27
28export type { 28export type {
29 Action,
29 BrowserViewBounds, 30 BrowserViewBounds,
30 ThemeSource, 31 ThemeSource,
31} from './schemas'; 32} from './schemas';
32export { 33export {
34 action,
33 browserViewBounds, 35 browserViewBounds,
34 themeSource, 36 themeSource,
35} from './schemas'; 37} from './schemas';
diff --git a/packages/shared/src/ipc.ts b/packages/shared/src/ipc.ts
index d2f65f7..54d761a 100644
--- a/packages/shared/src/ipc.ts
+++ b/packages/shared/src/ipc.ts
@@ -19,13 +19,10 @@
19 */ 19 */
20 20
21export enum MainToRendererIpcMessage { 21export enum MainToRendererIpcMessage {
22 SharedStoreSnapshot = 'sophie-main-to-renderer:shared-store-snapshot',
23 SharedStorePatch = 'sophie-main-to-renderer:shared-store-patch', 22 SharedStorePatch = 'sophie-main-to-renderer:shared-store-patch',
24} 23}
25 24
26export enum RendererToMainIpcMessage { 25export enum RendererToMainIpcMessage {
27 SharedStoreSnapshotRequest = 'sophie-renderer-to-main:shared-store-snapshot-request', 26 GetSharedStoreSnapshot = 'sophie-renderer-to-main:get-shared-store-snapshot',
28 SetBrowserViewBounds = 'sophie-renderer-to-main:set-browser-view-bounds', 27 DispatchAction = 'sophie-renderer-to-main:dispatch-action',
29 SetThemeSource = 'sophie-renderer-to-main:set-theme-source',
30 ReloadAllServices = 'sophie-renderer-to-main:reload-all-services',
31} 28}
diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts
index 0eff581..9090486 100644
--- a/packages/shared/src/schemas.ts
+++ b/packages/shared/src/schemas.ts
@@ -29,6 +29,28 @@ export const browserViewBounds = z.object({
29 29
30export type BrowserViewBounds = z.infer<typeof browserViewBounds>; 30export type BrowserViewBounds = z.infer<typeof browserViewBounds>;
31 31
32const setBrowserViewBoundsAction = z.object({
33 action: z.literal('set-browser-view-bounds'),
34 browserViewBounds,
35});
36
32export const themeSource = z.enum(['system', 'light', 'dark']); 37export const themeSource = z.enum(['system', 'light', 'dark']);
33 38
34export type ThemeSource = z.infer<typeof themeSource>; 39export type ThemeSource = z.infer<typeof themeSource>;
40
41const setThemeSourceAction = z.object({
42 action: z.literal('set-theme-source'),
43 themeSource,
44});
45
46const reloadAllServicesAction = z.object({
47 action: z.literal('reload-all-services'),
48});
49
50export const action = z.union([
51 setBrowserViewBoundsAction,
52 setThemeSourceAction,
53 reloadAllServicesAction,
54]);
55
56export type Action = z.infer<typeof action>;