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.ts53
1 files changed, 30 insertions, 23 deletions
diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts
index 85b0088..61123d9 100644
--- a/packages/main/src/services/ConfigPersistenceService.ts
+++ b/packages/main/src/services/ConfigPersistenceService.ts
@@ -17,12 +17,10 @@
17 * 17 *
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20import { FSWatcher, watch } from 'fs';
21import { watch } from 'fs';
22import { readFile, stat, writeFile } from 'fs/promises'; 21import { readFile, stat, writeFile } from 'fs/promises';
23import JSON5 from 'json5'; 22import JSON5 from 'json5';
24import { throttle } from 'lodash'; 23import { throttle } from 'lodash';
25import { IDisposer } from 'mobx-state-tree';
26import { join } from 'path'; 24import { join } from 'path';
27 25
28import type { ConfigSnapshotOut } from '../stores/Config'; 26import type { ConfigSnapshotOut } from '../stores/Config';
@@ -30,25 +28,26 @@ import type { ConfigSnapshotOut } from '../stores/Config';
30export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; 28export type ReadConfigResult = { found: true; data: unknown; } | { found: false; };
31 29
32export class ConfigPersistenceService { 30export class ConfigPersistenceService {
33 readonly #userDataDir: string; 31 static inject = ['userDataDir', 'configFileName'] as const;
32
33 private readonly configFilePath: string;
34 34
35 readonly #configFileName: string; 35 private timeLastWritten: Date | null = null;
36 36
37 readonly #configFilePath: string; 37 private watcher: FSWatcher | null = null;
38 38
39 constructor( 39 constructor(
40 userDataDir: string, 40 private readonly userDataDir: string,
41 configFileName: string, 41 private readonly configFileName: string,
42 ) { 42 ) {
43 this.#userDataDir = userDataDir; 43 this.configFileName = configFileName;
44 this.#configFileName = configFileName; 44 this.configFilePath = join(this.userDataDir, this.configFileName);
45 this.#configFilePath = join(this.#userDataDir, this.#configFileName);
46 } 45 }
47 46
48 async readConfig(): Promise<ReadConfigResult> { 47 async readConfig(): Promise<ReadConfigResult> {
49 let configStr; 48 let configStr;
50 try { 49 try {
51 configStr = await readFile(this.#configFilePath, 'utf8'); 50 configStr = await readFile(this.configFilePath, 'utf8');
52 } catch (err) { 51 } catch (err) {
53 if ((err as NodeJS.ErrnoException).code === 'ENOENT') { 52 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
54 return { found: false }; 53 return { found: false };
@@ -61,20 +60,24 @@ export class ConfigPersistenceService {
61 }; 60 };
62 } 61 }
63 62
64 async writeConfig(configSnapshot: ConfigSnapshotOut): Promise<Date> { 63 async writeConfig(configSnapshot: ConfigSnapshotOut): Promise<void> {
65 const configJson = JSON5.stringify(configSnapshot, { 64 const configJson = JSON5.stringify(configSnapshot, {
66 space: 2, 65 space: 2,
67 }); 66 });
68 await writeFile(this.#configFilePath, configJson, 'utf8'); 67 await writeFile(this.configFilePath, configJson, 'utf8');
69 const { mtime } = await stat(this.#configFilePath); 68 const stats = await stat(this.configFilePath);
70 return mtime; 69 this.timeLastWritten = stats.mtime;
71 } 70 }
72 71
73 watchConfig(callback: (mtime: Date) => Promise<void>, throttleMs: number): IDisposer { 72 watchConfig(callback: () => Promise<void>, throttleMs: number): void {
73 if (this.watcher !== null) {
74 throw new Error('watchConfig was already called');
75 }
76
74 const configChanged = throttle(async () => { 77 const configChanged = throttle(async () => {
75 let mtime: Date; 78 let mtime: Date;
76 try { 79 try {
77 const stats = await stat(this.#configFilePath); 80 const stats = await stat(this.configFilePath);
78 mtime = stats.mtime; 81 mtime = stats.mtime;
79 } catch (err) { 82 } catch (err) {
80 if ((err as NodeJS.ErrnoException).code === 'ENOENT') { 83 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
@@ -82,22 +85,26 @@ export class ConfigPersistenceService {
82 } 85 }
83 throw err; 86 throw err;
84 } 87 }
85 return callback(mtime); 88 if (this.timeLastWritten === null || mtime > this.timeLastWritten) {
89 return callback();
90 }
86 }, throttleMs); 91 }, throttleMs);
87 92
88 const watcher = watch(this.#userDataDir, { 93 this.watcher = watch(this.userDataDir, {
89 persistent: false, 94 persistent: false,
90 }); 95 });
91 96
92 watcher.on('change', (eventType, filename) => { 97 this.watcher.on('change', (eventType, filename) => {
93 if (eventType === 'change' 98 if (eventType === 'change'
94 && (filename === this.#configFileName || filename === null)) { 99 && (filename === this.configFileName || filename === null)) {
95 configChanged()?.catch((err) => { 100 configChanged()?.catch((err) => {
96 console.log('Unhandled error while listening for config changes', err); 101 console.log('Unhandled error while listening for config changes', err);
97 }); 102 });
98 } 103 }
99 }); 104 });
105 }
100 106
101 return () => watcher.close(); 107 dispose(): void {
108 this.watcher?.close();
102 } 109 }
103} 110}