aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-28 13:06:11 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-28 13:06:11 +0100
commit6fd7d4855f26aa7f217094f815fc7c2ec14bed4f (patch)
tree208f7340b7688b035769b09248c0fd3564eaf40b
parentrefactor: Inversion of control with typed-inject (diff)
downloadsophie-6fd7d4855f26aa7f217094f815fc7c2ec14bed4f.tar.gz
sophie-6fd7d4855f26aa7f217094f815fc7c2ec14bed4f.tar.zst
sophie-6fd7d4855f26aa7f217094f815fc7c2ec14bed4f.zip
refactor: Get rid of dependency injector
-rw-r--r--packages/main/package.json3
-rw-r--r--packages/main/src/CompositionRoot.ts (renamed from packages/main/src/injector.ts)24
-rw-r--r--packages/main/src/controllers/ConfigController.ts32
-rw-r--r--packages/main/src/controllers/NativeThemeController.ts27
-rw-r--r--packages/main/src/index.ts12
-rw-r--r--packages/main/src/services/ConfigPersistenceService.ts23
-rw-r--r--packages/main/src/utils.ts (renamed from packages/main/src/controllers/MainController.ts)32
-rw-r--r--yarn.lock8
8 files changed, 68 insertions, 93 deletions
diff --git a/packages/main/package.json b/packages/main/package.json
index 55bc663..48268fb 100644
--- a/packages/main/package.json
+++ b/packages/main/package.json
@@ -17,8 +17,7 @@
17 "lodash": "^4.17.21", 17 "lodash": "^4.17.21",
18 "mobx": "^6.3.10", 18 "mobx": "^6.3.10",
19 "mobx-state-tree": "^5.1.0", 19 "mobx-state-tree": "^5.1.0",
20 "ms": "^2.1.3", 20 "ms": "^2.1.3"
21 "typed-inject": "^3.0.1"
22 }, 21 },
23 "devDependencies": { 22 "devDependencies": {
24 "@types/electron-devtools-installer": "^2.2.1", 23 "@types/electron-devtools-installer": "^2.2.1",
diff --git a/packages/main/src/injector.ts b/packages/main/src/CompositionRoot.ts
index 2c05c85..affb186 100644
--- a/packages/main/src/injector.ts
+++ b/packages/main/src/CompositionRoot.ts
@@ -19,21 +19,19 @@
19 */ 19 */
20 20
21import { app } from 'electron'; 21import { app } from 'electron';
22import ms from 'ms';
23import { createInjector, Injector } from 'typed-inject';
24 22
25import { ConfigController } from './controllers/ConfigController'; 23import { ConfigController } from './controllers/ConfigController';
26import { MainController } from './controllers/MainController';
27import { NativeThemeController } from './controllers/NativeThemeController'; 24import { NativeThemeController } from './controllers/NativeThemeController';
28import { ConfigPersistenceService } from './services/ConfigPersistenceService'; 25import { ConfigPersistenceService } from './services/ConfigPersistenceService';
26import { MainStore } from './stores/MainStore';
27import { DisposeHelper } from './utils';
29 28
30export const injector: Injector<{ 29export class CompositionRoot extends DisposeHelper {
31 'mainController': MainController, 30 async init(store: MainStore): Promise<void> {
32}> = createInjector() 31 const configPersistenceService = new ConfigPersistenceService(app.getPath('userData'));
33 .provideFactory('userDataDir', () => app.getPath('userData')) 32 await this.registerDisposable(new ConfigController(
34 .provideValue('configFileName', 'config.json5') 33 configPersistenceService,
35 .provideValue('configDebounceTime', ms('1s')) 34 )).connect(store.config);
36 .provideClass('configPersistenceService', ConfigPersistenceService) 35 this.registerDisposable(new NativeThemeController()).connect(store);
37 .provideClass('configController', ConfigController) 36 }
38 .provideClass('nativeThemeController', NativeThemeController) 37}
39 .provideClass('mainController', MainController);
diff --git a/packages/main/src/controllers/ConfigController.ts b/packages/main/src/controllers/ConfigController.ts
index a28746c..318506f 100644
--- a/packages/main/src/controllers/ConfigController.ts
+++ b/packages/main/src/controllers/ConfigController.ts
@@ -19,34 +19,30 @@
19 */ 19 */
20 20
21import { debounce } from 'lodash'; 21import { debounce } from 'lodash';
22import { 22import ms from 'ms';
23 applySnapshot, 23import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree';
24 getSnapshot,
25 IDisposer,
26 onSnapshot,
27} from 'mobx-state-tree';
28 24
29import type { ConfigPersistenceService } from '../services/ConfigPersistenceService'; 25import type { ConfigPersistenceService } from '../services/ConfigPersistenceService';
30import type { Config, ConfigSnapshotOut } from '../stores/Config'; 26import type { Config, ConfigSnapshotOut } from '../stores/Config';
27import { DisposeHelper } from '../utils';
31 28
32export class ConfigController { 29const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s');
33 static inject = ['configPersistenceService', 'configDebounceTime'] as const;
34 30
31export class ConfigController extends DisposeHelper {
35 private config: Config | null = null; 32 private config: Config | null = null;
36 33
37 private onSnapshotDisposer: IDisposer | null = null;
38
39 private lastSnapshotOnDisk: ConfigSnapshotOut | null = null; 34 private lastSnapshotOnDisk: ConfigSnapshotOut | null = null;
40 35
41 private writingConfig: boolean = false; 36 private writingConfig: boolean = false;
42 37
43 constructor( 38 constructor(
44 private readonly persistenceService: ConfigPersistenceService, 39 private readonly persistenceService: ConfigPersistenceService,
45 private readonly debounceTime: number, 40 private readonly debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME,
46 ) { 41 ) {
42 super();
47 } 43 }
48 44
49 async initConfig(config: Config): Promise<void> { 45 async connect(config: Config): Promise<void> {
50 this.config = config; 46 this.config = config;
51 47
52 const foundConfig: boolean = await this.readConfig(); 48 const foundConfig: boolean = await this.readConfig();
@@ -59,20 +55,20 @@ export class ConfigController {
59 } 55 }
60 } 56 }
61 57
62 this.onSnapshotDisposer = onSnapshot(this.config, debounce((snapshot) => { 58 this.registerDisposable(onSnapshot(this.config, debounce((snapshot) => {
63 // We can compare snapshots by reference, since it is only recreated on store changes. 59 // We can compare snapshots by reference, since it is only recreated on store changes.
64 if (this.lastSnapshotOnDisk !== snapshot) { 60 if (this.lastSnapshotOnDisk !== snapshot) {
65 this.writeConfig().catch((err) => { 61 this.writeConfig().catch((err) => {
66 console.log('Failed to write config on config change', err); 62 console.log('Failed to write config on config change', err);
67 }) 63 })
68 } 64 }
69 }, this.debounceTime)); 65 }, this.debounceTime)));
70 66
71 this.persistenceService.watchConfig(async () => { 67 this.registerDisposable(this.persistenceService.watchConfig(async () => {
72 if (!this.writingConfig) { 68 if (!this.writingConfig) {
73 await this.readConfig(); 69 await this.readConfig();
74 } 70 }
75 }, this.debounceTime); 71 }, this.debounceTime));
76 } 72 }
77 73
78 private async readConfig(): Promise<boolean> { 74 private async readConfig(): Promise<boolean> {
@@ -100,8 +96,4 @@ export class ConfigController {
100 this.writingConfig = false; 96 this.writingConfig = false;
101 } 97 }
102 } 98 }
103
104 dispose(): void {
105 this.onSnapshotDisposer?.();
106 }
107} 99}
diff --git a/packages/main/src/controllers/NativeThemeController.ts b/packages/main/src/controllers/NativeThemeController.ts
index a50d41e..931660c 100644
--- a/packages/main/src/controllers/NativeThemeController.ts
+++ b/packages/main/src/controllers/NativeThemeController.ts
@@ -20,30 +20,23 @@
20 20
21import { nativeTheme } from 'electron'; 21import { nativeTheme } from 'electron';
22import { autorun } from 'mobx'; 22import { autorun } from 'mobx';
23import { IDisposer } from 'mobx-state-tree';
24 23
25import type { MainStore } from '../stores/MainStore'; 24import type { MainStore } from '../stores/MainStore';
25import { DisposeHelper } from '../utils';
26 26
27export class NativeThemeController { 27export class NativeThemeController extends DisposeHelper {
28 private autorunDisposer: IDisposer | null = null;
29
30 private shouldUseDarkColorsListener: (() => void) | null = null;
31
32 connect(store: MainStore): void { 28 connect(store: MainStore): void {
33 this.autorunDisposer = autorun(() => { 29 this.registerDisposable(autorun(() => {
34 nativeTheme.themeSource = store.config.themeSource; 30 nativeTheme.themeSource = store.config.themeSource;
35 }); 31 }));
32
36 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); 33 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors);
37 this.shouldUseDarkColorsListener = () => { 34 const shouldUseDarkColorsListener = () => {
38 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); 35 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors);
39 }; 36 };
40 nativeTheme.on('updated', this.shouldUseDarkColorsListener); 37 nativeTheme.on('updated', shouldUseDarkColorsListener);
41 } 38 this.registerDisposable(() => {
42 39 nativeTheme.off('updated', shouldUseDarkColorsListener);
43 dispose(): void { 40 });
44 if (this.shouldUseDarkColorsListener !== null) {
45 nativeTheme.off('updated', this.shouldUseDarkColorsListener);
46 }
47 this.autorunDisposer?.();
48 } 41 }
49} 42}
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index 8eb0803..3e9e338 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -41,11 +41,11 @@ import {
41} from '@sophie/shared'; 41} from '@sophie/shared';
42import { URL } from 'url'; 42import { URL } from 'url';
43 43
44import { CompositionRoot } from './CompositionRoot';
44import { 45import {
45 installDevToolsExtensions, 46 installDevToolsExtensions,
46 openDevToolsWhenReady, 47 openDevToolsWhenReady,
47} from './devTools'; 48} from './devTools';
48import { injector } from './injector';
49import { createMainStore } from './stores/MainStore'; 49import { createMainStore } from './stores/MainStore';
50 50
51const isDevelopment = import.meta.env.MODE === 'development'; 51const isDevelopment = import.meta.env.MODE === 'development';
@@ -104,10 +104,12 @@ if (isDevelopment) {
104let mainWindow: BrowserWindow | null = null; 104let mainWindow: BrowserWindow | null = null;
105 105
106const store = createMainStore(); 106const store = createMainStore();
107 107const compositionRoot = new CompositionRoot();
108const controller = injector.resolve('mainController'); 108compositionRoot.init(store).catch((err) => {
109controller.connect(store).catch((err) => { 109 console.log('Failed to initialize application', err);
110 console.log('Error while initializing app', err); 110});
111app.on('will-quit', () => {
112 compositionRoot.dispose();
111}); 113});
112 114
113const rendererBaseUrl = getResourceUrl('../renderer/'); 115const rendererBaseUrl = getResourceUrl('../renderer/');
diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts
index 61123d9..34d0e3e 100644
--- a/packages/main/src/services/ConfigPersistenceService.ts
+++ b/packages/main/src/services/ConfigPersistenceService.ts
@@ -17,28 +17,25 @@
17 * 17 *
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20import { FSWatcher, watch } from 'fs'; 20import { watch } from 'fs';
21import { readFile, stat, writeFile } from 'fs/promises'; 21import { readFile, stat, writeFile } from 'fs/promises';
22import JSON5 from 'json5'; 22import JSON5 from 'json5';
23import { throttle } from 'lodash'; 23import { 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';
27 28
28export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; 29export type ReadConfigResult = { found: true; data: unknown; } | { found: false; };
29 30
30export class ConfigPersistenceService { 31export class ConfigPersistenceService {
31 static inject = ['userDataDir', 'configFileName'] as const;
32
33 private readonly configFilePath: string; 32 private readonly configFilePath: string;
34 33
35 private timeLastWritten: Date | null = null; 34 private timeLastWritten: Date | null = null;
36 35
37 private watcher: FSWatcher | null = null;
38
39 constructor( 36 constructor(
40 private readonly userDataDir: string, 37 private readonly userDataDir: string,
41 private readonly configFileName: string, 38 private readonly configFileName: string = 'config.json5',
42 ) { 39 ) {
43 this.configFileName = configFileName; 40 this.configFileName = configFileName;
44 this.configFilePath = join(this.userDataDir, this.configFileName); 41 this.configFilePath = join(this.userDataDir, this.configFileName);
@@ -69,11 +66,7 @@ export class ConfigPersistenceService {
69 this.timeLastWritten = stats.mtime; 66 this.timeLastWritten = stats.mtime;
70 } 67 }
71 68
72 watchConfig(callback: () => Promise<void>, throttleMs: number): void { 69 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer {
73 if (this.watcher !== null) {
74 throw new Error('watchConfig was already called');
75 }
76
77 const configChanged = throttle(async () => { 70 const configChanged = throttle(async () => {
78 let mtime: Date; 71 let mtime: Date;
79 try { 72 try {
@@ -90,11 +83,11 @@ export class ConfigPersistenceService {
90 } 83 }
91 }, throttleMs); 84 }, throttleMs);
92 85
93 this.watcher = watch(this.userDataDir, { 86 const watcher = watch(this.userDataDir, {
94 persistent: false, 87 persistent: false,
95 }); 88 });
96 89
97 this.watcher.on('change', (eventType, filename) => { 90 watcher.on('change', (eventType, filename) => {
98 if (eventType === 'change' 91 if (eventType === 'change'
99 && (filename === this.configFileName || filename === null)) { 92 && (filename === this.configFileName || filename === null)) {
100 configChanged()?.catch((err) => { 93 configChanged()?.catch((err) => {
@@ -102,9 +95,7 @@ export class ConfigPersistenceService {
102 }); 95 });
103 } 96 }
104 }); 97 });
105 }
106 98
107 dispose(): void { 99 return () => watcher.close();
108 this.watcher?.close();
109 } 100 }
110} 101}
diff --git a/packages/main/src/controllers/MainController.ts b/packages/main/src/utils.ts
index 6b97330..11c78e9 100644
--- a/packages/main/src/controllers/MainController.ts
+++ b/packages/main/src/utils.ts
@@ -18,21 +18,29 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { ConfigController } from './ConfigController'; 21export type Disposable = Disposer | DisposableObject;
22import type { NativeThemeController } from './NativeThemeController';
23import type { MainStore } from '../stores/MainStore';
24 22
25export class MainController { 23export type Disposer = () => void;
26 static inject = ['configController', 'nativeThemeController'] as const;
27 24
28 constructor( 25export interface DisposableObject {
29 private readonly configController: ConfigController, 26 dispose(): void;
30 private readonly nativeThemeController: NativeThemeController, 27}
31 ) { 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;
32 } 39 }
33 40
34 async connect(store: MainStore): Promise<void> { 41 dispose(): void {
35 await this.configController.initConfig(store.config); 42 for (let i = this.disposers.length - 1; i >= 0; i -= 1) {
36 this.nativeThemeController.connect(store); 43 this.disposers[i]();
44 }
37 } 45 }
38} 46}
diff --git a/yarn.lock b/yarn.lock
index 918aa7a..214d987 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1238,7 +1238,6 @@ __metadata:
1238 mobx-state-tree: ^5.1.0 1238 mobx-state-tree: ^5.1.0
1239 ms: ^2.1.3 1239 ms: ^2.1.3
1240 rimraf: ^3.0.2 1240 rimraf: ^3.0.2
1241 typed-inject: ^3.0.1
1242 typescript: ^4.5.4 1241 typescript: ^4.5.4
1243 vite: ^2.7.7 1242 vite: ^2.7.7
1244 languageName: unknown 1243 languageName: unknown
@@ -7378,13 +7377,6 @@ __metadata:
7378 languageName: node 7377 languageName: node
7379 linkType: hard 7378 linkType: hard
7380 7379
7381"typed-inject@npm:^3.0.1":
7382 version: 3.0.1
7383 resolution: "typed-inject@npm:3.0.1"
7384 checksum: a400797a42951bc46a38873e9be40fe90fdba6ae3362e2d4da4972c4ec48e962ff167bc5f090ca42dc20bb3f6063cd07a80426df520c64dc9e94d1e89ff4779e
7385 languageName: node
7386 linkType: hard
7387
7388"typedarray-to-buffer@npm:^3.1.5": 7380"typedarray-to-buffer@npm:^3.1.5":
7389 version: 3.1.5 7381 version: 3.1.5
7390 resolution: "typedarray-to-buffer@npm:3.1.5" 7382 resolution: "typedarray-to-buffer@npm:3.1.5"