diff options
Diffstat (limited to 'packages/main/src/reactions/synchronizeConfig.ts')
-rw-r--r-- | packages/main/src/reactions/synchronizeConfig.ts | 73 |
1 files changed, 41 insertions, 32 deletions
diff --git a/packages/main/src/reactions/synchronizeConfig.ts b/packages/main/src/reactions/synchronizeConfig.ts index 9436c16..6044ee7 100644 --- a/packages/main/src/reactions/synchronizeConfig.ts +++ b/packages/main/src/reactions/synchronizeConfig.ts | |||
@@ -18,51 +18,56 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import deepEqual from 'deep-equal'; | ||
22 | import { debounce } from 'lodash-es'; | 21 | import { debounce } from 'lodash-es'; |
23 | import { reaction } from 'mobx'; | 22 | import { reaction } from 'mobx'; |
24 | import ms from 'ms'; | ||
25 | 23 | ||
26 | import type ConfigRepository from '../infrastructure/config/ConfigRepository'; | 24 | import type ConfigRepository from '../infrastructure/config/ConfigRepository.js'; |
27 | import type SharedStore from '../stores/SharedStore'; | 25 | import type SharedStore from '../stores/SharedStore.js'; |
28 | import type Config from '../stores/config/Config'; | 26 | import type Config from '../stores/config/Config.js'; |
29 | import type Disposer from '../utils/Disposer'; | 27 | import type Disposer from '../utils/Disposer.js'; |
30 | import { getLogger } from '../utils/log'; | 28 | import getLogger from '../utils/getLogger.js'; |
31 | 29 | ||
32 | const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); | 30 | const DEFAULT_CONFIG_DEBOUNCE_TIME_MS = 1000; |
33 | 31 | ||
34 | const log = getLogger('synchronizeConfig'); | 32 | const log = getLogger('synchronizeConfig'); |
35 | 33 | ||
34 | export function serializeConfig(config: Config): string { | ||
35 | return `${JSON.stringify(config, undefined, 2)}\n`; | ||
36 | } | ||
37 | |||
36 | export default async function synchronizeConfig( | 38 | export default async function synchronizeConfig( |
37 | sharedStore: SharedStore, | 39 | sharedStore: SharedStore, |
38 | repository: ConfigRepository, | 40 | repository: ConfigRepository, |
39 | debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, | 41 | debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME_MS, |
40 | ): Promise<Disposer> { | 42 | ): Promise<Disposer> { |
41 | let lastConfigOnDisk: Config | undefined; | 43 | let lastConfigOnDisk: string | undefined; |
42 | 44 | ||
43 | async function writeConfig(): Promise<void> { | 45 | async function writeConfig(serializedConfig: string): Promise<void> { |
44 | const { config } = sharedStore; | 46 | await repository.writeConfig(serializedConfig); |
45 | await repository.writeConfig(config); | 47 | lastConfigOnDisk = serializedConfig; |
46 | lastConfigOnDisk = config; | ||
47 | } | 48 | } |
48 | 49 | ||
49 | async function readConfig(): Promise<boolean> { | 50 | async function readConfig(): Promise<boolean> { |
50 | const result = await repository.readConfig(); | 51 | const result = await repository.readConfig(); |
51 | if (result.found) { | 52 | if (result.found) { |
53 | const { contents } = result; | ||
54 | if (contents === lastConfigOnDisk) { | ||
55 | // No need to re-apply config if we have already applied it. | ||
56 | return true; | ||
57 | } | ||
52 | try { | 58 | try { |
53 | // This cast is unsound if the config file is invalid, | 59 | // This cast is unsound if the config file is invalid, |
54 | // but we'll throw an error in the end anyways. | 60 | // but we'll throw an error in the end anyways. |
55 | sharedStore.loadConfig(result.data as Config); | 61 | const data = JSON.parse(contents) as Config; |
62 | sharedStore.loadConfig(data); | ||
63 | log.info('Loaded config'); | ||
56 | } catch (error) { | 64 | } catch (error) { |
57 | log.error('Failed to apply config snapshot', result.data, error); | 65 | log.error('Failed to apply config snapshot', contents, error); |
58 | return true; | 66 | return true; |
59 | } | 67 | } |
60 | lastConfigOnDisk = sharedStore.config; | 68 | lastConfigOnDisk = serializeConfig(sharedStore.config); |
61 | // We can't use `comparer.structural` from `mobx`, because | 69 | if (contents !== lastConfigOnDisk) { |
62 | // it handles missing values and `undefined` values differently, | 70 | await writeConfig(lastConfigOnDisk); |
63 | // but JSON5 is unable to distinguish them. | ||
64 | if (!deepEqual(result.data, lastConfigOnDisk, { strict: true })) { | ||
65 | await writeConfig(); | ||
66 | } | 71 | } |
67 | } | 72 | } |
68 | return result.found; | 73 | return result.found; |
@@ -70,20 +75,23 @@ export default async function synchronizeConfig( | |||
70 | 75 | ||
71 | if (!(await readConfig())) { | 76 | if (!(await readConfig())) { |
72 | log.info('Config file was not found'); | 77 | log.info('Config file was not found'); |
73 | await writeConfig(); | 78 | const serializedConfig = serializeConfig(sharedStore.config); |
79 | await writeConfig(serializedConfig); | ||
74 | log.info('Created config file'); | 80 | log.info('Created config file'); |
75 | } | 81 | } |
76 | 82 | ||
83 | const debouncedSerializeConfig = debounce(() => { | ||
84 | const serializedConfig = serializeConfig(sharedStore.config); | ||
85 | if (serializedConfig !== lastConfigOnDisk) { | ||
86 | writeConfig(serializedConfig).catch((error) => { | ||
87 | log.error('Failed to write config on config change', error); | ||
88 | }); | ||
89 | } | ||
90 | }, debounceTime); | ||
91 | |||
77 | const disposeReaction = reaction( | 92 | const disposeReaction = reaction( |
78 | () => sharedStore.config, | 93 | () => sharedStore.config, |
79 | debounce((config) => { | 94 | debouncedSerializeConfig, |
80 | // We can compare snapshots by reference, since it is only recreated on store changes. | ||
81 | if (!deepEqual(config, lastConfigOnDisk, { strict: true })) { | ||
82 | writeConfig().catch((error) => { | ||
83 | log.error('Failed to write config on config change', error); | ||
84 | }); | ||
85 | } | ||
86 | }, debounceTime), | ||
87 | ); | 95 | ); |
88 | 96 | ||
89 | const disposeWatcher = repository.watchConfig(async () => { | 97 | const disposeWatcher = repository.watchConfig(async () => { |
@@ -92,10 +100,11 @@ export default async function synchronizeConfig( | |||
92 | } catch (error) { | 100 | } catch (error) { |
93 | log.error('Failed to read config', error); | 101 | log.error('Failed to read config', error); |
94 | } | 102 | } |
95 | }, debounceTime); | 103 | }); |
96 | 104 | ||
97 | return () => { | 105 | return () => { |
98 | disposeWatcher(); | 106 | disposeWatcher(); |
99 | disposeReaction(); | 107 | disposeReaction(); |
108 | debouncedSerializeConfig.flush(); | ||
100 | }; | 109 | }; |
101 | } | 110 | } |