aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/services/impl/ConfigPersistenceImpl.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/services/impl/ConfigPersistenceImpl.ts')
-rw-r--r--packages/main/src/services/impl/ConfigPersistenceImpl.ts102
1 files changed, 102 insertions, 0 deletions
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}