/* * Copyright (C) 2021-2022 Kristóf Marussy * * This file is part of Sophie. * * Sophie is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * SPDX-License-Identifier: AGPL-3.0-only */ import { debounce } from 'lodash'; import { applySnapshot, getSnapshot, IDisposer, onSnapshot, } from 'mobx-state-tree'; import ms from 'ms'; import { ConfigPersistenceService } from '../services/ConfigPersistenceService'; import { Config, ConfigSnapshotOut } from '../stores/Config'; const DEFAULT_DEBOUNCE_TIME = ms('1s'); class ConfigController { readonly #config: Config; readonly #persistenceService: ConfigPersistenceService; readonly #onSnapshotDisposer: IDisposer; readonly #watcherDisposer: IDisposer; #lastSnapshotOnDisk: ConfigSnapshotOut | null = null; #writingConfig: boolean = false; #configMTime: Date | null = null; constructor( config: Config, persistenceService: ConfigPersistenceService, debounceTime: number, ) { this.#config = config; this.#persistenceService = persistenceService; this.#onSnapshotDisposer = onSnapshot(this.#config, debounce((snapshot) => { // We can compare snapshots by reference, since it is only recreated on store changes. if (this.#lastSnapshotOnDisk !== snapshot) { this.#writeConfig().catch((err) => { console.log('Failed to write config on config change', err); }) } }, debounceTime)); this.#watcherDisposer = this.#persistenceService.watchConfig(async (mtime) => { if (!this.#writingConfig && (this.#configMTime === null || mtime > this.#configMTime)) { await this.#readConfig(); } }, debounceTime); } async #readConfig(): Promise { const result = await this.#persistenceService.readConfig(); if (result.found) { try { applySnapshot(this.#config, result.data); this.#lastSnapshotOnDisk = getSnapshot(this.#config); console.log('Loaded config'); } catch (err) { console.error('Failed to read config', result.data, err); } } return result.found; } async #writeConfig(): Promise { const snapshot = getSnapshot(this.#config); this.#writingConfig = true; try { this.#configMTime = await this.#persistenceService.writeConfig(snapshot); this.#lastSnapshotOnDisk = snapshot; console.log('Wrote config'); } finally { this.#writingConfig = false; } } async initConfig(): Promise { const foundConfig: boolean = await this.#readConfig(); if (!foundConfig) { console.log('Creating new config file'); try { await this.#writeConfig(); } catch (err) { console.error('Failed to initialize config'); } } } dispose(): void { this.#onSnapshotDisposer(); this.#watcherDisposer(); } } export async function initConfig( config: Config, persistenceService: ConfigPersistenceService, debounceTime: number = DEFAULT_DEBOUNCE_TIME, ): Promise { const controller = new ConfigController(config, persistenceService, debounceTime); await controller.initConfig(); return () => controller.dispose(); }