aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/services
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-26 17:29:58 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-26 17:42:27 +0100
commitfe5d0bb29e850e36693cae5594adf16f764aedf9 (patch)
treec3d7e050518ef658756972b789959a24943f3df0 /packages/main/src/services
parentfeat: Switch to json5 config format (diff)
downloadsophie-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/services')
-rw-r--r--packages/main/src/services/ConfigPersistence.ts36
-rw-r--r--packages/main/src/services/MainEnv.ts38
-rw-r--r--packages/main/src/services/impl/ConfigPersistenceImpl.ts102
3 files changed, 176 insertions, 0 deletions
diff --git a/packages/main/src/services/ConfigPersistence.ts b/packages/main/src/services/ConfigPersistence.ts
new file mode 100644
index 0000000..f9a82de
--- /dev/null
+++ b/packages/main/src/services/ConfigPersistence.ts
@@ -0,0 +1,36 @@
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 { IDisposer } from 'mobx-state-tree';
22import ms from 'ms';
23
24import type { ConfigSnapshotOut } from '../stores/Config';
25
26export const CONFIG_DEBOUNCE_TIME: number = ms('1s');
27
28export type ReadConfigResult = { found: true; data: unknown; } | { found: false; };
29
30export interface ConfigPersistence {
31 readConfig(): Promise<ReadConfigResult>;
32
33 writeConfig(configSnapshot: ConfigSnapshotOut): Promise<Date>;
34
35 watchConfig(callback: (mtime: Date) => Promise<void>): IDisposer;
36}
diff --git a/packages/main/src/services/MainEnv.ts b/packages/main/src/services/MainEnv.ts
new file mode 100644
index 0000000..23ee9a1
--- /dev/null
+++ b/packages/main/src/services/MainEnv.ts
@@ -0,0 +1,38 @@
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 { IAnyStateTreeNode, getEnv as getAnyEnv } from 'mobx-state-tree';
22
23import type { ConfigPersistence } from './ConfigPersistence';
24
25export interface MainEnv {
26 configPersistence: ConfigPersistence;
27}
28
29/**
30 * Gets a well-typed environment from `model`.
31 *
32 * Only useable inside state trees created by `createAndConnectRootStore`.
33 *
34 * @param model The state tree node.
35 */
36export function getEnv(model: IAnyStateTreeNode): MainEnv {
37 return getAnyEnv<MainEnv>(model);
38}
diff --git a/packages/main/src/services/impl/ConfigPersistenceImpl.ts b/packages/main/src/services/impl/ConfigPersistenceImpl.ts
new file mode 100644
index 0000000..097ab74
--- /dev/null
+++ b/packages/main/src/services/impl/ConfigPersistenceImpl.ts
@@ -0,0 +1,102 @@
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 { watch } from 'fs';
22import { readFile, stat, writeFile } from 'fs/promises';
23import JSON5 from 'json5';
24import { throttle } from 'lodash';
25import { IDisposer } from 'mobx-state-tree';
26import { join } from 'path';
27
28import { CONFIG_DEBOUNCE_TIME, ConfigPersistence, ReadConfigResult } from '../ConfigPersistence';
29import type { ConfigSnapshotOut } from '../../stores/Config';
30
31export class ConfigPersistenceImpl implements ConfigPersistence {
32 readonly #userDataDir: string;
33
34 readonly #configFileName: string;
35
36 readonly #configFilePath: string;
37
38 constructor(
39 userDataDir: string,
40 configFileName: string,
41 ) {
42 this.#userDataDir = userDataDir;
43 this.#configFileName = configFileName;
44 this.#configFilePath = join(this.#userDataDir, this.#configFileName);
45 }
46
47 async readConfig(): Promise<ReadConfigResult> {
48 let configStr;
49 try {
50 configStr = await readFile(this.#configFilePath, 'utf8');
51 } catch (err) {
52 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
53 return { found: false };
54 }
55 throw err;
56 }
57 return {
58 found: true,
59 data: JSON5.parse(configStr),
60 };
61 }
62
63 async writeConfig(configSnapshot: ConfigSnapshotOut): Promise<Date> {
64 const configJson = JSON5.stringify(configSnapshot, {
65 space: 2,
66 });
67 await writeFile(this.#configFilePath, configJson, 'utf8');
68 const { mtime } = await stat(this.#configFilePath);
69 return mtime;
70 }
71
72 watchConfig(callback: (mtime: Date) => Promise<void>): IDisposer {
73 const configChanged = throttle(async () => {
74 let mtime: Date;
75 try {
76 const stats = await stat(this.#configFilePath);
77 mtime = stats.mtime;
78 } catch (err) {
79 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
80 return;
81 }
82 throw err;
83 }
84 return callback(mtime);
85 }, CONFIG_DEBOUNCE_TIME);
86
87 const watcher = watch(this.#userDataDir, {
88 persistent: false,
89 });
90
91 watcher.on('change', (eventType, filename) => {
92 if (eventType === 'change'
93 && (filename === this.#configFileName || filename === null)) {
94 configChanged()?.catch((err) => {
95 console.log('Unhandled error while listening for config changes', err);
96 });
97 }
98 });
99
100 return () => watcher.close();
101 }
102}