From da610a51f49807d1409f36b98e06e89447a4202b Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 27 Jan 2022 01:02:02 +0100 Subject: refactor: Extract config handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the handling of the contents of the config file out of the stores and into dedicated files to simplify the code of the stores. Signed-off-by: Kristóf Marussy --- .../main/src/infrastructure/config/ConfigFile.ts | 142 -------------------- .../src/infrastructure/config/ConfigRepository.ts | 2 +- .../src/infrastructure/config/impl/ConfigFile.ts | 141 ++++++++++++++++++++ packages/main/src/initReactions.ts | 2 +- packages/main/src/reactions/synchronizeConfig.ts | 2 +- packages/main/src/stores/Profile.ts | 30 +---- packages/main/src/stores/ProfileSettings.ts | 30 +++++ packages/main/src/stores/Service.ts | 35 +---- packages/main/src/stores/SharedStore.ts | 91 +------------ .../main/src/stores/__tests__/SharedStore.spec.ts | 7 +- packages/main/src/stores/config/Config.ts | 30 +++++ packages/main/src/stores/config/ProfileConfig.ts | 25 ++++ packages/main/src/stores/config/ServiceConfig.ts | 29 ++++ packages/main/src/stores/config/loadConfig.ts | 146 +++++++++++++++++++++ packages/main/src/utils/generateId.ts | 27 ---- 15 files changed, 425 insertions(+), 314 deletions(-) delete mode 100644 packages/main/src/infrastructure/config/ConfigFile.ts create mode 100644 packages/main/src/infrastructure/config/impl/ConfigFile.ts create mode 100644 packages/main/src/stores/ProfileSettings.ts create mode 100644 packages/main/src/stores/config/Config.ts create mode 100644 packages/main/src/stores/config/ProfileConfig.ts create mode 100644 packages/main/src/stores/config/ServiceConfig.ts create mode 100644 packages/main/src/stores/config/loadConfig.ts delete mode 100644 packages/main/src/utils/generateId.ts diff --git a/packages/main/src/infrastructure/config/ConfigFile.ts b/packages/main/src/infrastructure/config/ConfigFile.ts deleted file mode 100644 index 193a20d..0000000 --- a/packages/main/src/infrastructure/config/ConfigFile.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 { watch } from 'node:fs'; -import { readFile, stat, writeFile } from 'node:fs/promises'; -import path from 'node:path'; - -import JSON5 from 'json5'; -import { throttle } from 'lodash-es'; - -import type { Config } from '../../stores/SharedStore'; -import type Disposer from '../../utils/Disposer'; -import { getLogger } from '../../utils/log'; - -import type ConfigRepository from './ConfigRepository'; -import type ReadConfigResult from './ReadConfigResult'; - -const log = getLogger('ConfigFile'); - -export default class ConfigFile implements ConfigRepository { - readonly #userDataDir: string; - - readonly #configFileName: string; - - readonly #configFilePath: string; - - #writingConfig = false; - - #timeLastWritten: Date | undefined; - - constructor(userDataDir: string, configFileName = 'config.json5') { - this.#userDataDir = userDataDir; - this.#configFileName = configFileName; - this.#configFilePath = path.join(userDataDir, configFileName); - } - - async readConfig(): Promise { - let configStr: string; - try { - configStr = await readFile(this.#configFilePath, 'utf8'); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - log.debug('Config file', this.#configFilePath, 'was not found'); - return { found: false }; - } - throw error; - } - log.info('Read config file', this.#configFilePath); - return { - found: true, - data: JSON5.parse(configStr), - }; - } - - async writeConfig(configSnapshot: Config): Promise { - const configJson = JSON5.stringify(configSnapshot, { - space: 2, - }); - this.#writingConfig = true; - try { - await writeFile(this.#configFilePath, configJson, 'utf8'); - const { mtime } = await stat(this.#configFilePath); - log.trace('Config file', this.#configFilePath, 'last written at', mtime); - this.#timeLastWritten = mtime; - } finally { - this.#writingConfig = false; - } - log.info('Wrote config file', this.#configFilePath); - } - - watchConfig(callback: () => Promise, throttleMs: number): Disposer { - log.debug('Installing watcher for', this.#userDataDir); - - const configChanged = throttle(async () => { - let mtime: Date; - try { - const stats = await stat(this.#configFilePath); - mtime = stats.mtime; - log.trace('Config file last modified at', mtime); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - log.debug( - 'Config file', - this.#configFilePath, - 'was deleted after being changed', - ); - return; - } - throw error; - } - if ( - !this.#writingConfig && - (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten) - ) { - log.debug( - 'Found a config file modified at', - mtime, - 'whish is newer than last written', - this.#timeLastWritten, - ); - await callback(); - } - }, throttleMs); - - const watcher = watch(this.#userDataDir, { - persistent: false, - }); - - watcher.on('change', (eventType, filename) => { - if ( - eventType === 'change' && - (filename === this.#configFileName || filename === null) - ) { - configChanged()?.catch((err) => { - log.error('Unhandled error while listening for config changes', err); - }); - } - }); - - return () => { - log.trace('Removing watcher for', this.#configFilePath); - watcher.close(); - }; - } -} diff --git a/packages/main/src/infrastructure/config/ConfigRepository.ts b/packages/main/src/infrastructure/config/ConfigRepository.ts index 0ce7fc1..e00f5a0 100644 --- a/packages/main/src/infrastructure/config/ConfigRepository.ts +++ b/packages/main/src/infrastructure/config/ConfigRepository.ts @@ -18,7 +18,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Config } from '../../stores/SharedStore'; +import type Config from '../../stores/config/Config'; import type Disposer from '../../utils/Disposer'; export type ReadConfigResult = diff --git a/packages/main/src/infrastructure/config/impl/ConfigFile.ts b/packages/main/src/infrastructure/config/impl/ConfigFile.ts new file mode 100644 index 0000000..90ee187 --- /dev/null +++ b/packages/main/src/infrastructure/config/impl/ConfigFile.ts @@ -0,0 +1,141 @@ +/* + * 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 { watch } from 'node:fs'; +import { readFile, stat, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +import JSON5 from 'json5'; +import { throttle } from 'lodash-es'; + +import type Config from '../../../stores/config/Config'; +import type Disposer from '../../../utils/Disposer'; +import { getLogger } from '../../../utils/log'; +import type ConfigRepository from '../ConfigRepository'; +import type ReadConfigResult from '../ReadConfigResult'; + +const log = getLogger('ConfigFile'); + +export default class ConfigFile implements ConfigRepository { + readonly #userDataDir: string; + + readonly #configFileName: string; + + readonly #configFilePath: string; + + #writingConfig = false; + + #timeLastWritten: Date | undefined; + + constructor(userDataDir: string, configFileName = 'config.json5') { + this.#userDataDir = userDataDir; + this.#configFileName = configFileName; + this.#configFilePath = path.join(userDataDir, configFileName); + } + + async readConfig(): Promise { + let configStr: string; + try { + configStr = await readFile(this.#configFilePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + log.debug('Config file', this.#configFilePath, 'was not found'); + return { found: false }; + } + throw error; + } + log.info('Read config file', this.#configFilePath); + return { + found: true, + data: JSON5.parse(configStr), + }; + } + + async writeConfig(configSnapshot: Config): Promise { + const configJson = JSON5.stringify(configSnapshot, { + space: 2, + }); + this.#writingConfig = true; + try { + await writeFile(this.#configFilePath, configJson, 'utf8'); + const { mtime } = await stat(this.#configFilePath); + log.trace('Config file', this.#configFilePath, 'last written at', mtime); + this.#timeLastWritten = mtime; + } finally { + this.#writingConfig = false; + } + log.info('Wrote config file', this.#configFilePath); + } + + watchConfig(callback: () => Promise, throttleMs: number): Disposer { + log.debug('Installing watcher for', this.#userDataDir); + + const configChanged = throttle(async () => { + let mtime: Date; + try { + const stats = await stat(this.#configFilePath); + mtime = stats.mtime; + log.trace('Config file last modified at', mtime); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + log.debug( + 'Config file', + this.#configFilePath, + 'was deleted after being changed', + ); + return; + } + throw error; + } + if ( + !this.#writingConfig && + (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten) + ) { + log.debug( + 'Found a config file modified at', + mtime, + 'whish is newer than last written', + this.#timeLastWritten, + ); + await callback(); + } + }, throttleMs); + + const watcher = watch(this.#userDataDir, { + persistent: false, + }); + + watcher.on('change', (eventType, filename) => { + if ( + eventType === 'change' && + (filename === this.#configFileName || filename === null) + ) { + configChanged()?.catch((err) => { + log.error('Unhandled error while listening for config changes', err); + }); + } + }); + + return () => { + log.trace('Removing watcher for', this.#configFilePath); + watcher.close(); + }; + } +} diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts index 50e561d..87ad425 100644 --- a/packages/main/src/initReactions.ts +++ b/packages/main/src/initReactions.ts @@ -20,7 +20,7 @@ import { app } from 'electron'; -import ConfigFile from './infrastructure/config/ConfigFile'; +import ConfigFile from './infrastructure/config/impl/ConfigFile'; import synchronizeConfig from './reactions/synchronizeConfig'; import synchronizeNativeTheme from './reactions/synchronizeNativeTheme'; import type MainStore from './stores/MainStore'; diff --git a/packages/main/src/reactions/synchronizeConfig.ts b/packages/main/src/reactions/synchronizeConfig.ts index 480cc1a..4a9c24b 100644 --- a/packages/main/src/reactions/synchronizeConfig.ts +++ b/packages/main/src/reactions/synchronizeConfig.ts @@ -25,7 +25,7 @@ import ms from 'ms'; import type ConfigRepository from '../infrastructure/config/ConfigRepository'; import type SharedStore from '../stores/SharedStore'; -import type { Config } from '../stores/SharedStore'; +import type Config from '../stores/config/Config'; import type Disposer from '../utils/Disposer'; import { getLogger } from '../utils/log'; diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts index ec2a64b..0fd486e 100644 --- a/packages/main/src/stores/Profile.ts +++ b/packages/main/src/stores/Profile.ts @@ -18,19 +18,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - Profile as ProfileBase, - ProfileSettingsSnapshotIn, -} from '@sophie/shared'; +import { Profile as ProfileBase } from '@sophie/shared'; import { getSnapshot, Instance } from 'mobx-state-tree'; -import generateId from '../utils/generateId'; +import overrideProps from '../utils/overrideProps'; -export interface ProfileConfig extends ProfileSettingsSnapshotIn { - id?: string | undefined; -} +import ProfileSettings from './ProfileSettings'; +import type ProfileConfig from './config/ProfileConfig'; -const Profile = ProfileBase.views((self) => ({ +const Profile = overrideProps(ProfileBase, { + settings: ProfileSettings, +}).views((self) => ({ get config(): ProfileConfig { const { id, settings } = self; return { ...getSnapshot(settings), id }; @@ -44,17 +42,3 @@ const Profile = ProfileBase.views((self) => ({ interface Profile extends Instance {} export default Profile; - -export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; - -export function addMissingProfileIds( - profileConfigs: ProfileConfig[] | undefined, -): ProfileSettingsSnapshotWithId[] { - return (profileConfigs ?? []).map((profileConfig) => { - const { id, ...settings } = profileConfig; - return [ - typeof id === 'undefined' ? generateId(settings.name) : id, - settings, - ]; - }); -} diff --git a/packages/main/src/stores/ProfileSettings.ts b/packages/main/src/stores/ProfileSettings.ts new file mode 100644 index 0000000..eed51e3 --- /dev/null +++ b/packages/main/src/stores/ProfileSettings.ts @@ -0,0 +1,30 @@ +/* + * Copyright (C) 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 { ProfileSettings } from '@sophie/shared'; + +// TODO Export a modified ProfileSettings once we need to add actions to it. +// eslint-disable-next-line unicorn/prefer-export-from -- Can't export from default. +export default ProfileSettings; + +export type { + ProfileSettingsSnapshotIn, + ProfileSettingsSnapshotOut, +} from '@sophie/shared'; diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts index e70caa6..fea0bdf 100644 --- a/packages/main/src/stores/Service.ts +++ b/packages/main/src/stores/Service.ts @@ -20,20 +20,12 @@ import type { UnreadCount } from '@sophie/service-shared'; import { Service as ServiceBase } from '@sophie/shared'; -import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; +import { Instance, getSnapshot } from 'mobx-state-tree'; -import generateId from '../utils/generateId'; import overrideProps from '../utils/overrideProps'; -import { ProfileSettingsSnapshotWithId } from './Profile'; -import ServiceSettings, { ServiceSettingsSnapshotIn } from './ServiceSettings'; - -export interface ServiceConfig - extends Omit { - id?: string | undefined; - - profile?: ReferenceIdentifier | undefined; -} +import ServiceSettings from './ServiceSettings'; +import type ServiceConfig from './config/ServiceConfig'; const Service = overrideProps(ServiceBase, { settings: ServiceSettings, @@ -90,24 +82,3 @@ const Service = overrideProps(ServiceBase, { interface Service extends Instance {} export default Service; - -export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; - -export function addMissingServiceIdsAndProfiles( - serviceConfigs: ServiceConfig[] | undefined, - profiles: ProfileSettingsSnapshotWithId[], -): ServiceSettingsSnapshotWithId[] { - return (serviceConfigs ?? []).map((serviceConfig) => { - const { id, ...settings } = serviceConfig; - const { name } = settings; - let { profile: profileId } = settings; - if (profileId === undefined) { - profileId = generateId(name); - profiles.push([profileId, { name }]); - } - return [ - id === undefined ? generateId(name) : id, - { ...settings, profile: profileId }, - ]; - }); -} diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index c34af75..b9983f6 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts @@ -19,71 +19,23 @@ */ import { SharedStore as SharedStoreBase } from '@sophie/shared'; -import { - applySnapshot, - getSnapshot, - IMSTArray, - IMSTMap, - Instance, - IReferenceType, - IStateTreeNode, - IType, - types, -} from 'mobx-state-tree'; +import { getSnapshot, Instance, types } from 'mobx-state-tree'; import { getLogger } from '../utils/log'; import overrideProps from '../utils/overrideProps'; -import GlobalSettings, { GlobalSettingsSnapshotIn } from './GlobalSettings'; -import Profile, { addMissingProfileIds, ProfileConfig } from './Profile'; -import Service, { - addMissingServiceIdsAndProfiles, - ServiceConfig, -} from './Service'; +import GlobalSettings from './GlobalSettings'; +import Profile from './Profile'; +import Service from './Service'; +import type Config from './config/Config'; +import loadConfig from './config/loadConfig'; const log = getLogger('sharedStore'); -export interface Config extends GlobalSettingsSnapshotIn { - profiles?: ProfileConfig[] | undefined; - - services?: ServiceConfig[] | undefined; -} - function getConfigs(models: { config: T }[]): T[] | undefined { return models.length === 0 ? undefined : models.map((model) => model.config); } -function applySettings< - C, - D extends IType< - { id: string; settings: C }, - unknown, - { settings: IStateTreeNode> } - >, ->( - current: IMSTArray>, - currentById: IMSTMap, - toApply: [string, C][], -): void { - const toApplyById = new Map(toApply); - const toDelete = new Set(currentById.keys()); - toApplyById.forEach((settingsSnapshot, id) => { - const model = currentById.get(id); - if (model === undefined) { - currentById.set(id, { - id, - settings: settingsSnapshot, - }); - } else { - toDelete.delete(id); - applySnapshot(model.settings, settingsSnapshot); - } - }); - current.clear(); - toDelete.forEach((id) => currentById.delete(id)); - current.push(...toApply.map(([id]) => id)); -} - const SharedStore = overrideProps(SharedStoreBase, { settings: types.optional(GlobalSettings, {}), profilesById: types.map(Profile), @@ -105,36 +57,7 @@ const SharedStore = overrideProps(SharedStoreBase, { })) .actions((self) => ({ loadConfig(config: Config): void { - const { - profiles, - profilesById, - selectedService, - services, - servicesById, - settings, - } = self; - const { id: selectedServiceId } = selectedService ?? { id: undefined }; - const { - profiles: profilesConfig, - services: servicesConfig, - ...settingsToApply - } = config; - const profilesToApply = addMissingProfileIds(profilesConfig); - const servicesToApply = addMissingServiceIdsAndProfiles( - servicesConfig, - profilesToApply, - ); - applySettings(profiles, profilesById, profilesToApply); - applySettings(services, servicesById, servicesToApply); - applySnapshot(settings, settingsToApply); - let newSelectedService; - if (selectedServiceId !== undefined) { - newSelectedService = servicesById.get(selectedServiceId); - } - if (newSelectedService === undefined && services.length > 0) { - [newSelectedService] = services; - } - self.selectedService = newSelectedService; + loadConfig(self, config); }, setShouldUseDarkColors(shouldUseDarkColors: boolean): void { self.shouldUseDarkColors = shouldUseDarkColors; diff --git a/packages/main/src/stores/__tests__/SharedStore.spec.ts b/packages/main/src/stores/__tests__/SharedStore.spec.ts index dfd59a1..268ce3f 100644 --- a/packages/main/src/stores/__tests__/SharedStore.spec.ts +++ b/packages/main/src/stores/__tests__/SharedStore.spec.ts @@ -18,9 +18,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ProfileConfig } from '../Profile'; -import type { ServiceConfig } from '../Service'; -import SharedStore, { Config } from '../SharedStore'; +import SharedStore from '../SharedStore'; +import type Config from '../config/Config'; +import type ProfileConfig from '../config/ProfileConfig'; +import type ServiceConfig from '../config/ServiceConfig'; const profileProps: ProfileConfig = { name: 'Test profile', diff --git a/packages/main/src/stores/config/Config.ts b/packages/main/src/stores/config/Config.ts new file mode 100644 index 0000000..c38e3c5 --- /dev/null +++ b/packages/main/src/stores/config/Config.ts @@ -0,0 +1,30 @@ +/* + * Copyright (C) 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 type { GlobalSettingsSnapshotIn } from '../GlobalSettings'; + +import type ProfileConfig from './ProfileConfig'; +import type ServiceConfig from './ServiceConfig'; + +export default interface Config extends GlobalSettingsSnapshotIn { + profiles?: ProfileConfig[] | undefined; + + services?: ServiceConfig[] | undefined; +} diff --git a/packages/main/src/stores/config/ProfileConfig.ts b/packages/main/src/stores/config/ProfileConfig.ts new file mode 100644 index 0000000..ce276d4 --- /dev/null +++ b/packages/main/src/stores/config/ProfileConfig.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 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 { ProfileSettingsSnapshotIn } from '@sophie/shared'; + +export default interface ProfileConfig extends ProfileSettingsSnapshotIn { + id?: string | undefined; +} diff --git a/packages/main/src/stores/config/ServiceConfig.ts b/packages/main/src/stores/config/ServiceConfig.ts new file mode 100644 index 0000000..40ea4c9 --- /dev/null +++ b/packages/main/src/stores/config/ServiceConfig.ts @@ -0,0 +1,29 @@ +/* + * Copyright (C) 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 { ServiceSettingsSnapshotIn } from '@sophie/shared'; +import { ReferenceIdentifier } from 'mobx-state-tree'; + +export default interface ServiceConfig + extends Omit { + id?: string | undefined; + + profile?: ReferenceIdentifier | undefined; +} diff --git a/packages/main/src/stores/config/loadConfig.ts b/packages/main/src/stores/config/loadConfig.ts new file mode 100644 index 0000000..770d675 --- /dev/null +++ b/packages/main/src/stores/config/loadConfig.ts @@ -0,0 +1,146 @@ +/* + * Copyright (C) 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 { + applySnapshot, + IMSTArray, + IMSTMap, + IReferenceType, + IStateTreeNode, + IType, +} from 'mobx-state-tree'; +import { nanoid } from 'nanoid'; +import slug from 'slug'; + +import GlobalSettings from '../GlobalSettings'; +import type Profile from '../Profile'; +import type { ProfileSettingsSnapshotIn } from '../ProfileSettings'; +import type Service from '../Service'; +import type { ServiceSettingsSnapshotIn } from '../ServiceSettings'; + +import type Config from './Config'; +import type ProfileConfig from './ProfileConfig'; +import type ServiceConfig from './ServiceConfig'; + +function generateId(name?: string | undefined): string { + const nameSlug = typeof name === 'undefined' ? '' : slug(name); + return `${nameSlug}_${nanoid()}`; +} + +function addMissingProfileIds( + profileConfigs: ProfileConfig[] | undefined, +): [string, ProfileSettingsSnapshotIn][] { + return (profileConfigs ?? []).map((profileConfig) => { + const { id, ...settings } = profileConfig; + return [id === undefined ? generateId(settings.name) : id, settings]; + }); +} + +function addMissingServiceIdsAndProfiles( + serviceConfigs: ServiceConfig[] | undefined, + profiles: [string, ProfileSettingsSnapshotIn][], +): [string, ServiceSettingsSnapshotIn][] { + return (serviceConfigs ?? []).map((serviceConfig) => { + const { id, ...settings } = serviceConfig; + const { name } = settings; + let { profile: profileId } = settings; + if (profileId === undefined) { + profileId = generateId(name); + profiles.push([profileId, { name }]); + } + return [ + id === undefined ? generateId(name) : id, + { ...settings, profile: profileId }, + ]; + }); +} + +type TypeWithSettings = IType< + { id: string; settings: C }, + unknown, + { settings: IStateTreeNode> } +>; + +function applySettings>( + current: IMSTArray>, + currentById: IMSTMap, + toApply: [string, C][], +): void { + const toApplyById = new Map(toApply); + const toDelete = new Set(currentById.keys()); + toApplyById.forEach((settingsSnapshot, id) => { + const model = currentById.get(id); + if (model === undefined) { + currentById.set(id, { + id, + settings: settingsSnapshot, + }); + } else { + toDelete.delete(id); + applySnapshot(model.settings, settingsSnapshot); + } + }); + toDelete.forEach((id) => currentById.delete(id)); + current.clear(); + current.push(...toApply.map(([id]) => id)); +} + +export default function loadConfig( + target: { + readonly profiles: IMSTArray>; + readonly profilesById: IMSTMap; + selectedService: Service | undefined; + readonly services: IMSTArray>; + readonly servicesById: IMSTMap; + readonly settings: GlobalSettings; + }, + config: Config, +): void { + const { + profiles, + profilesById, + selectedService, + services, + servicesById, + settings, + } = target; + const { id: selectedServiceId } = selectedService ?? { id: undefined }; + const { + profiles: profilesConfig, + services: servicesConfig, + ...settingsToApply + } = config; + const profilesToApply = addMissingProfileIds(profilesConfig); + const servicesToApply = addMissingServiceIdsAndProfiles( + servicesConfig, + profilesToApply, + ); + applySettings(profiles, profilesById, profilesToApply); + applySettings(services, servicesById, servicesToApply); + applySnapshot(settings, settingsToApply); + let newSelectedService: Service | undefined; + if (selectedServiceId !== undefined) { + newSelectedService = servicesById.get(selectedServiceId); + } + if (newSelectedService === undefined && services.length > 0) { + [newSelectedService] = services; + } + target.selectedService = newSelectedService; +} diff --git a/packages/main/src/utils/generateId.ts b/packages/main/src/utils/generateId.ts deleted file mode 100644 index 8a87e5a..0000000 --- a/packages/main/src/utils/generateId.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 { nanoid } from 'nanoid'; -import slug from 'slug'; - -export default function generateId(name?: string | undefined) { - const nameSlug = typeof name === 'undefined' ? '' : slug(name); - return `${nameSlug}_${nanoid()}`; -} -- cgit v1.2.3-54-g00ecf