diff options
Diffstat (limited to 'packages/main/src/services/ConfigPersistenceService.ts')
-rw-r--r-- | packages/main/src/services/ConfigPersistenceService.ts | 53 |
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 | 20 | import { FSWatcher, watch } from 'fs'; | |
21 | import { watch } from 'fs'; | ||
22 | import { readFile, stat, writeFile } from 'fs/promises'; | 21 | import { readFile, stat, writeFile } from 'fs/promises'; |
23 | import JSON5 from 'json5'; | 22 | import JSON5 from 'json5'; |
24 | import { throttle } from 'lodash'; | 23 | import { throttle } from 'lodash'; |
25 | import { IDisposer } from 'mobx-state-tree'; | ||
26 | import { join } from 'path'; | 24 | import { join } from 'path'; |
27 | 25 | ||
28 | import type { ConfigSnapshotOut } from '../stores/Config'; | 26 | import type { ConfigSnapshotOut } from '../stores/Config'; |
@@ -30,25 +28,26 @@ import type { ConfigSnapshotOut } from '../stores/Config'; | |||
30 | export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; | 28 | export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; |
31 | 29 | ||
32 | export class ConfigPersistenceService { | 30 | export 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 | } |