diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-12-26 17:29:58 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-12-26 17:42:27 +0100 |
commit | fe5d0bb29e850e36693cae5594adf16f764aedf9 (patch) | |
tree | c3d7e050518ef658756972b789959a24943f3df0 /packages/main/src/stores | |
parent | feat: Switch to json5 config format (diff) | |
download | sophie-fe5d0bb29e850e36693cae5594adf16f764aedf9.tar.gz sophie-fe5d0bb29e850e36693cae5594adf16f764aedf9.tar.zst sophie-fe5d0bb29e850e36693cae5594adf16f764aedf9.zip |
refactor: Config persistence architecture
The architecture in the main process is split into 3 main parts:
* services: interfaces for services are injected into the stores through
the MainEnv interface (for testability)
* services/impl: electron-specific implementations of services
* stores: the actions of the stores can invoke (asynchronous) services
Diffstat (limited to 'packages/main/src/stores')
-rw-r--r-- | packages/main/src/stores/Config.ts | 96 | ||||
-rw-r--r-- | packages/main/src/stores/MainStore.ts (renamed from packages/main/src/stores/RootStore.ts) | 23 |
2 files changed, 104 insertions, 15 deletions
diff --git a/packages/main/src/stores/Config.ts b/packages/main/src/stores/Config.ts index 483a491..eb53635 100644 --- a/packages/main/src/stores/Config.ts +++ b/packages/main/src/stores/Config.ts | |||
@@ -18,15 +18,103 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { Instance } from 'mobx-state-tree'; | 21 | import { debounce } from 'lodash'; |
22 | import { config as originalConfig, ThemeSource } from '@sophie/shared'; | 22 | import { |
23 | applySnapshot, | ||
24 | flow, | ||
25 | getSnapshot, | ||
26 | IDisposer, | ||
27 | Instance, | ||
28 | onSnapshot, | ||
29 | } from 'mobx-state-tree'; | ||
30 | import { | ||
31 | config as originalConfig, | ||
32 | ConfigSnapshotIn, | ||
33 | ConfigSnapshotOut, | ||
34 | ThemeSource, | ||
35 | } from '@sophie/shared'; | ||
23 | 36 | ||
24 | export type { ConfigSnapshotIn, ConfigSnapshotOut } from '@sophie/shared'; | 37 | import { CONFIG_DEBOUNCE_TIME, ReadConfigResult } from '../services/ConfigPersistence'; |
38 | import { getEnv } from '../services/MainEnv'; | ||
25 | 39 | ||
26 | export const config = originalConfig.actions((self) => ({ | 40 | export const config = originalConfig.actions((self) => ({ |
27 | setThemeSource(mode: ThemeSource) { | 41 | setThemeSource(mode: ThemeSource) { |
28 | self.themeSource = mode; | 42 | self.themeSource = mode; |
29 | }, | 43 | }, |
30 | })); | 44 | })).actions((self) => { |
45 | let lastSnapshotOnDisk: ConfigSnapshotOut | null = null; | ||
46 | let writingConfig = false; | ||
47 | let configMtime: Date | null = null; | ||
48 | let onSnapshotDisposer: IDisposer | null = null; | ||
49 | let watcherDisposer: IDisposer | null = null; | ||
50 | |||
51 | function dispose() { | ||
52 | onSnapshotDisposer?.(); | ||
53 | watcherDisposer?.(); | ||
54 | } | ||
55 | |||
56 | const actions: { | ||
57 | beforeDetach(): void, | ||
58 | readConfig(): Promise<boolean>; | ||
59 | writeConfig(): Promise<void>; | ||
60 | initConfig(): Promise<void>; | ||
61 | } = { | ||
62 | beforeDetach() { | ||
63 | dispose(); | ||
64 | }, | ||
65 | readConfig: flow(function*() { | ||
66 | const result: ReadConfigResult = yield getEnv(self).configPersistence.readConfig(); | ||
67 | if (result.found) { | ||
68 | try { | ||
69 | applySnapshot(self, result.data); | ||
70 | lastSnapshotOnDisk = getSnapshot(self); | ||
71 | console.log('Loaded config'); | ||
72 | } catch (err) { | ||
73 | console.error('Failed to read config', result.data, err); | ||
74 | } | ||
75 | } | ||
76 | return result.found; | ||
77 | }), | ||
78 | writeConfig: flow(function*() { | ||
79 | const snapshot = getSnapshot(self); | ||
80 | writingConfig = true; | ||
81 | try { | ||
82 | configMtime = yield getEnv(self).configPersistence.writeConfig(snapshot); | ||
83 | lastSnapshotOnDisk = snapshot; | ||
84 | console.log('Wrote config'); | ||
85 | } finally { | ||
86 | writingConfig = false; | ||
87 | } | ||
88 | }), | ||
89 | initConfig: flow(function*() { | ||
90 | dispose(); | ||
91 | const foundConfig: boolean = yield actions.readConfig(); | ||
92 | if (!foundConfig) { | ||
93 | console.log('Creating new config file'); | ||
94 | try { | ||
95 | yield actions.writeConfig(); | ||
96 | } catch (err) { | ||
97 | console.error('Failed to initialize config'); | ||
98 | } | ||
99 | } | ||
100 | onSnapshotDisposer = onSnapshot(self, debounce((snapshot) => { | ||
101 | // We can compare snapshots by reference, since it is only recreated on store changes. | ||
102 | if (lastSnapshotOnDisk !== snapshot) { | ||
103 | actions.writeConfig().catch((err) => { | ||
104 | console.log('Failed to write config on config change', err); | ||
105 | }) | ||
106 | } | ||
107 | }, CONFIG_DEBOUNCE_TIME)); | ||
108 | watcherDisposer = getEnv(self).configPersistence.watchConfig(async (mtime) => { | ||
109 | if (!writingConfig && (configMtime === null || mtime > configMtime)) { | ||
110 | await actions.readConfig(); | ||
111 | } | ||
112 | }); | ||
113 | }), | ||
114 | }; | ||
115 | return actions; | ||
116 | }); | ||
31 | 117 | ||
32 | export interface Config extends Instance<typeof config> {} | 118 | export interface Config extends Instance<typeof config> {} |
119 | |||
120 | export type { ConfigSnapshotIn, ConfigSnapshotOut }; | ||
diff --git a/packages/main/src/stores/RootStore.ts b/packages/main/src/stores/MainStore.ts index 31e2b71..ee215a7 100644 --- a/packages/main/src/stores/RootStore.ts +++ b/packages/main/src/stores/MainStore.ts | |||
@@ -19,15 +19,13 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; | 21 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; |
22 | import { | 22 | import { BrowserViewBounds, emptySharedStore } from '@sophie/shared'; |
23 | BrowserViewBounds, | ||
24 | emptySharedStore, | ||
25 | } from '@sophie/shared'; | ||
26 | 23 | ||
27 | import type { Config } from './Config'; | 24 | import type { Config } from './Config'; |
25 | import { MainEnv } from '../services/MainEnv'; | ||
28 | import { sharedStore } from './SharedStore'; | 26 | import { sharedStore } from './SharedStore'; |
29 | 27 | ||
30 | export const rootStore = types.model('RootStore', { | 28 | export const mainStore = types.model('MainStore', { |
31 | browserViewBounds: types.model('BrowserViewBounds', { | 29 | browserViewBounds: types.model('BrowserViewBounds', { |
32 | x: 0, | 30 | x: 0, |
33 | y: 0, | 31 | y: 0, |
@@ -48,11 +46,14 @@ export const rootStore = types.model('RootStore', { | |||
48 | } | 46 | } |
49 | })); | 47 | })); |
50 | 48 | ||
51 | export interface RootStore extends Instance<typeof rootStore> {} | 49 | export interface RootStore extends Instance<typeof mainStore> {} |
52 | 50 | ||
53 | export function createRootStore(): RootStore { | 51 | export function createMainStore(env: MainEnv): RootStore { |
54 | return rootStore.create({ | 52 | return mainStore.create( |
55 | browserViewBounds: {}, | 53 | { |
56 | shared: emptySharedStore, | 54 | browserViewBounds: {}, |
57 | }); | 55 | shared: emptySharedStore, |
56 | }, | ||
57 | env, | ||
58 | ); | ||
58 | } | 59 | } |