aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/services/ConfigPersistenceService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/services/ConfigPersistenceService.ts')
-rw-r--r--packages/main/src/services/ConfigPersistenceService.ts103
1 files changed, 103 insertions, 0 deletions
diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts
new file mode 100644
index 0000000..85b0088
--- /dev/null
+++ b/packages/main/src/services/ConfigPersistenceService.ts
@@ -0,0 +1,103 @@
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 type { ConfigSnapshotOut } from '../stores/Config';
29
30export type ReadConfigResult = { found: true; data: unknown; } | { found: false; };
31
32export class ConfigPersistenceService {
33 readonly #userDataDir: string;
34
35 readonly #configFileName: string;
36
37 readonly #configFilePath: string;
38
39 constructor(
40 userDataDir: string,
41 configFileName: string,
42 ) {
43 this.#userDataDir = userDataDir;
44 this.#configFileName = configFileName;
45 this.#configFilePath = join(this.#userDataDir, this.#configFileName);
46 }
47
48 async readConfig(): Promise<ReadConfigResult> {
49 let configStr;
50 try {
51 configStr = await readFile(this.#configFilePath, 'utf8');
52 } catch (err) {
53 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
54 return { found: false };
55 }
56 throw err;
57 }
58 return {
59 found: true,
60 data: JSON5.parse(configStr),
61 };
62 }
63
64 async writeConfig(configSnapshot: ConfigSnapshotOut): Promise<Date> {
65 const configJson = JSON5.stringify(configSnapshot, {
66 space: 2,
67 });
68 await writeFile(this.#configFilePath, configJson, 'utf8');
69 const { mtime } = await stat(this.#configFilePath);
70 return mtime;
71 }
72
73 watchConfig(callback: (mtime: Date) => Promise<void>, throttleMs: number): IDisposer {
74 const configChanged = throttle(async () => {
75 let mtime: Date;
76 try {
77 const stats = await stat(this.#configFilePath);
78 mtime = stats.mtime;
79 } catch (err) {
80 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
81 return;
82 }
83 throw err;
84 }
85 return callback(mtime);
86 }, throttleMs);
87
88 const watcher = watch(this.#userDataDir, {
89 persistent: false,
90 });
91
92 watcher.on('change', (eventType, filename) => {
93 if (eventType === 'change'
94 && (filename === this.#configFileName || filename === null)) {
95 configChanged()?.catch((err) => {
96 console.log('Unhandled error while listening for config changes', err);
97 });
98 }
99 });
100
101 return () => watcher.close();
102 }
103}