aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-28 13:51:16 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-28 13:51:16 +0100
commit5712e88785d600a63d59cb583f045375c8c16255 (patch)
tree0abe682c30d1d9c03f4dce2f6d551615026ee368
parentrefactor: Get rid of dependency injector (diff)
downloadsophie-5712e88785d600a63d59cb583f045375c8c16255.tar.gz
sophie-5712e88785d600a63d59cb583f045375c8c16255.tar.zst
sophie-5712e88785d600a63d59cb583f045375c8c16255.zip
refactor: Functional design for controllers
-rw-r--r--packages/main/src/compositionRoot.ts (renamed from packages/main/src/CompositionRoot.ts)23
-rw-r--r--packages/main/src/controllers/ConfigController.ts99
-rw-r--r--packages/main/src/controllers/config.ts93
-rw-r--r--packages/main/src/controllers/nativeTheme.ts (renamed from packages/main/src/controllers/NativeThemeController.ts)28
-rw-r--r--packages/main/src/index.ts10
-rw-r--r--packages/main/src/services/ConfigPersistenceService.ts2
-rw-r--r--packages/main/src/utils.ts27
7 files changed, 126 insertions, 156 deletions
diff --git a/packages/main/src/CompositionRoot.ts b/packages/main/src/compositionRoot.ts
index affb186..eb6f50f 100644
--- a/packages/main/src/CompositionRoot.ts
+++ b/packages/main/src/compositionRoot.ts
@@ -20,18 +20,19 @@
20 20
21import { app } from 'electron'; 21import { app } from 'electron';
22 22
23import { ConfigController } from './controllers/ConfigController'; 23import { initConfig } from './controllers/config';
24import { NativeThemeController } from './controllers/NativeThemeController'; 24import { initNativeTheme } from './controllers/nativeTheme';
25import { ConfigPersistenceService } from './services/ConfigPersistenceService'; 25import { ConfigPersistenceService } from './services/ConfigPersistenceService';
26import { MainStore } from './stores/MainStore'; 26import { MainStore } from './stores/MainStore';
27import { DisposeHelper } from './utils'; 27import { Disposer } from './utils';
28 28
29export class CompositionRoot extends DisposeHelper { 29export async function init(store: MainStore): Promise<Disposer> {
30 async init(store: MainStore): Promise<void> { 30 const configPersistenceService = new ConfigPersistenceService(app.getPath('userData'));
31 const configPersistenceService = new ConfigPersistenceService(app.getPath('userData')); 31 const disposeConfigController = await initConfig(store.config, configPersistenceService);
32 await this.registerDisposable(new ConfigController( 32 const disposeNativeThemeController = initNativeTheme(store);
33 configPersistenceService, 33
34 )).connect(store.config); 34 return () => {
35 this.registerDisposable(new NativeThemeController()).connect(store); 35 disposeNativeThemeController();
36 } 36 disposeConfigController();
37 };
37} 38}
diff --git a/packages/main/src/controllers/ConfigController.ts b/packages/main/src/controllers/ConfigController.ts
deleted file mode 100644
index 318506f..0000000
--- a/packages/main/src/controllers/ConfigController.ts
+++ /dev/null
@@ -1,99 +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 { debounce } from 'lodash';
22import ms from 'ms';
23import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree';
24
25import type { ConfigPersistenceService } from '../services/ConfigPersistenceService';
26import type { Config, ConfigSnapshotOut } from '../stores/Config';
27import { DisposeHelper } from '../utils';
28
29const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s');
30
31export class ConfigController extends DisposeHelper {
32 private config: Config | null = null;
33
34 private lastSnapshotOnDisk: ConfigSnapshotOut | null = null;
35
36 private writingConfig: boolean = false;
37
38 constructor(
39 private readonly persistenceService: ConfigPersistenceService,
40 private readonly debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME,
41 ) {
42 super();
43 }
44
45 async connect(config: Config): Promise<void> {
46 this.config = config;
47
48 const foundConfig: boolean = await this.readConfig();
49 if (!foundConfig) {
50 console.log('Creating new config file');
51 try {
52 await this.writeConfig();
53 } catch (err) {
54 console.error('Failed to initialize config');
55 }
56 }
57
58 this.registerDisposable(onSnapshot(this.config, debounce((snapshot) => {
59 // We can compare snapshots by reference, since it is only recreated on store changes.
60 if (this.lastSnapshotOnDisk !== snapshot) {
61 this.writeConfig().catch((err) => {
62 console.log('Failed to write config on config change', err);
63 })
64 }
65 }, this.debounceTime)));
66
67 this.registerDisposable(this.persistenceService.watchConfig(async () => {
68 if (!this.writingConfig) {
69 await this.readConfig();
70 }
71 }, this.debounceTime));
72 }
73
74 private async readConfig(): Promise<boolean> {
75 const result = await this.persistenceService.readConfig();
76 if (result.found) {
77 try {
78 applySnapshot(this.config!, result.data);
79 this.lastSnapshotOnDisk = getSnapshot(this.config!);
80 console.log('Loaded config');
81 } catch (err) {
82 console.error('Failed to read config', result.data, err);
83 }
84 }
85 return result.found;
86 }
87
88 private async writeConfig(): Promise<void> {
89 const snapshot = getSnapshot(this.config!);
90 this.writingConfig = true;
91 try {
92 await this.persistenceService.writeConfig(snapshot);
93 this.lastSnapshotOnDisk = snapshot;
94 console.log('Wrote config');
95 } finally {
96 this.writingConfig = false;
97 }
98 }
99}
diff --git a/packages/main/src/controllers/config.ts b/packages/main/src/controllers/config.ts
new file mode 100644
index 0000000..c7b027d
--- /dev/null
+++ b/packages/main/src/controllers/config.ts
@@ -0,0 +1,93 @@
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 { debounce } from 'lodash';
22import ms from 'ms';
23import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree';
24
25import type { ConfigPersistenceService } from '../services/ConfigPersistenceService';
26import type { Config, ConfigSnapshotOut } from '../stores/Config';
27import { Disposer } from '../utils';
28
29const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s');
30
31export async function initConfig(
32 config: Config,
33 persistenceService: ConfigPersistenceService,
34 debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME,
35): Promise<Disposer> {
36 let lastSnapshotOnDisk: ConfigSnapshotOut | null = null;
37 let writingConfig: boolean = false;
38
39 async function readConfig(): Promise<boolean> {
40 const result = await persistenceService.readConfig();
41 if (result.found) {
42 try {
43 applySnapshot(config, result.data);
44 lastSnapshotOnDisk = getSnapshot(config);
45 console.log('Loaded config');
46 } catch (err) {
47 console.error('Failed to read config', result.data, err);
48 }
49 }
50 return result.found;
51 }
52
53 async function writeConfig(): Promise<void> {
54 const snapshot = getSnapshot(config);
55 writingConfig = true;
56 try {
57 await persistenceService.writeConfig(snapshot);
58 lastSnapshotOnDisk = snapshot;
59 console.log('Wrote config');
60 } finally {
61 writingConfig = false;
62 }
63 }
64
65 if (!await readConfig()) {
66 console.log('Creating new config file');
67 try {
68 await writeConfig();
69 } catch (err) {
70 console.error('Failed to initialize config');
71 }
72 }
73
74 const disposeOnSnapshot = onSnapshot(config, debounce((snapshot) => {
75 // We can compare snapshots by reference, since it is only recreated on store changes.
76 if (lastSnapshotOnDisk !== snapshot) {
77 writeConfig().catch((err) => {
78 console.log('Failed to write config on config change', err);
79 })
80 }
81 }, debounceTime));
82
83 const disposeWatcher = persistenceService.watchConfig(async () => {
84 if (!writingConfig) {
85 await readConfig();
86 }
87 }, debounceTime);
88
89 return () => {
90 disposeWatcher();
91 disposeOnSnapshot();
92 };
93}
diff --git a/packages/main/src/controllers/NativeThemeController.ts b/packages/main/src/controllers/nativeTheme.ts
index 931660c..e4390a8 100644
--- a/packages/main/src/controllers/NativeThemeController.ts
+++ b/packages/main/src/controllers/nativeTheme.ts
@@ -22,21 +22,21 @@ import { nativeTheme } from 'electron';
22import { autorun } from 'mobx'; 22import { autorun } from 'mobx';
23 23
24import type { MainStore } from '../stores/MainStore'; 24import type { MainStore } from '../stores/MainStore';
25import { DisposeHelper } from '../utils'; 25import { Disposer } from '../utils';
26 26
27export class NativeThemeController extends DisposeHelper { 27export function initNativeTheme(store: MainStore): Disposer {
28 connect(store: MainStore): void { 28 const disposeThemeSourceReaction = autorun(() => {
29 this.registerDisposable(autorun(() => { 29 nativeTheme.themeSource = store.config.themeSource;
30 nativeTheme.themeSource = store.config.themeSource; 30 });
31 }));
32 31
32 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors);
33 const shouldUseDarkColorsListener = () => {
33 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); 34 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors);
34 const shouldUseDarkColorsListener = () => { 35 };
35 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); 36 nativeTheme.on('updated', shouldUseDarkColorsListener);
36 }; 37
37 nativeTheme.on('updated', shouldUseDarkColorsListener); 38 return () => {
38 this.registerDisposable(() => { 39 nativeTheme.off('updated', shouldUseDarkColorsListener);
39 nativeTheme.off('updated', shouldUseDarkColorsListener); 40 disposeThemeSourceReaction();
40 }); 41 };
41 }
42} 42}
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index 3e9e338..7c7be35 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -41,7 +41,7 @@ import {
41} from '@sophie/shared'; 41} from '@sophie/shared';
42import { URL } from 'url'; 42import { URL } from 'url';
43 43
44import { CompositionRoot } from './CompositionRoot'; 44import { init } from './compositionRoot';
45import { 45import {
46 installDevToolsExtensions, 46 installDevToolsExtensions,
47 openDevToolsWhenReady, 47 openDevToolsWhenReady,
@@ -104,13 +104,11 @@ if (isDevelopment) {
104let mainWindow: BrowserWindow | null = null; 104let mainWindow: BrowserWindow | null = null;
105 105
106const store = createMainStore(); 106const store = createMainStore();
107const compositionRoot = new CompositionRoot(); 107init(store).then((disposeCompositionRoot) => {
108compositionRoot.init(store).catch((err) => { 108 app.on('will-quit', disposeCompositionRoot);
109}).catch((err) => {
109 console.log('Failed to initialize application', err); 110 console.log('Failed to initialize application', err);
110}); 111});
111app.on('will-quit', () => {
112 compositionRoot.dispose();
113});
114 112
115const rendererBaseUrl = getResourceUrl('../renderer/'); 113const rendererBaseUrl = getResourceUrl('../renderer/');
116function shouldCancelMainWindowRequest(url: string, method: string): boolean { 114function shouldCancelMainWindowRequest(url: string, method: string): boolean {
diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts
index 34d0e3e..1c0315f 100644
--- a/packages/main/src/services/ConfigPersistenceService.ts
+++ b/packages/main/src/services/ConfigPersistenceService.ts
@@ -24,7 +24,7 @@ import { throttle } from 'lodash';
24import { join } from 'path'; 24import { join } from 'path';
25 25
26import type { ConfigSnapshotOut } from '../stores/Config'; 26import type { ConfigSnapshotOut } from '../stores/Config';
27import type { Disposer } from '../utils'; 27import { Disposer } from '../utils';
28 28
29export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; 29export type ReadConfigResult = { found: true; data: unknown; } | { found: false; };
30 30
diff --git a/packages/main/src/utils.ts b/packages/main/src/utils.ts
index 11c78e9..0d469dd 100644
--- a/packages/main/src/utils.ts
+++ b/packages/main/src/utils.ts
@@ -18,29 +18,6 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21export type Disposable = Disposer | DisposableObject; 21import { IDisposer } from 'mobx-state-tree';
22 22
23export type Disposer = () => void; 23export type Disposer = IDisposer;
24
25export interface DisposableObject {
26 dispose(): void;
27}
28
29export class DisposeHelper implements DisposableObject {
30 private readonly disposers: Disposer[] = [];
31
32 protected registerDisposable<T extends Disposable>(disposable: T): T {
33 if (typeof disposable === 'object') {
34 this.disposers.push(() => disposable.dispose());
35 } else {
36 this.disposers.push(disposable);
37 }
38 return disposable;
39 }
40
41 dispose(): void {
42 for (let i = this.disposers.length - 1; i >= 0; i -= 1) {
43 this.disposers[i]();
44 }
45 }
46}