From e56cdad02c00adf3b779d9de62d460e78be204a6 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 26 Dec 2021 19:17:23 +0100 Subject: refactor: Clarify main process architecture * stores: reactive data structures to hold application state * controllers: subscribe to store changes and call store actions in response to external events from services * services: integrate with the nodejs and electron environment (should be mocked for unit testing) --- packages/main/src/services/ConfigPersistence.ts | 36 ------- .../main/src/services/ConfigPersistenceService.ts | 103 +++++++++++++++++++++ packages/main/src/services/MainEnv.ts | 38 -------- packages/main/src/services/NativeThemeService.ts | 38 ++++++++ .../src/services/impl/ConfigPersistenceImpl.ts | 102 -------------------- 5 files changed, 141 insertions(+), 176 deletions(-) delete mode 100644 packages/main/src/services/ConfigPersistence.ts create mode 100644 packages/main/src/services/ConfigPersistenceService.ts delete mode 100644 packages/main/src/services/MainEnv.ts create mode 100644 packages/main/src/services/NativeThemeService.ts delete mode 100644 packages/main/src/services/impl/ConfigPersistenceImpl.ts (limited to 'packages/main/src/services') diff --git a/packages/main/src/services/ConfigPersistence.ts b/packages/main/src/services/ConfigPersistence.ts deleted file mode 100644 index f9a82de..0000000 --- a/packages/main/src/services/ConfigPersistence.ts +++ /dev/null @@ -1,36 +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 { IDisposer } from 'mobx-state-tree'; -import ms from 'ms'; - -import type { ConfigSnapshotOut } from '../stores/Config'; - -export const CONFIG_DEBOUNCE_TIME: number = ms('1s'); - -export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; - -export interface ConfigPersistence { - readConfig(): Promise; - - writeConfig(configSnapshot: ConfigSnapshotOut): Promise; - - watchConfig(callback: (mtime: Date) => Promise): IDisposer; -} diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts new file mode 100644 index 0000000..85b0088 --- /dev/null +++ b/packages/main/src/services/ConfigPersistenceService.ts @@ -0,0 +1,103 @@ +/* + * 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 'fs'; +import { readFile, stat, writeFile } from 'fs/promises'; +import JSON5 from 'json5'; +import { throttle } from 'lodash'; +import { IDisposer } from 'mobx-state-tree'; +import { join } from 'path'; + +import type { ConfigSnapshotOut } from '../stores/Config'; + +export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; + +export class ConfigPersistenceService { + readonly #userDataDir: string; + + readonly #configFileName: string; + + readonly #configFilePath: string; + + constructor( + userDataDir: string, + configFileName: string, + ) { + this.#userDataDir = userDataDir; + this.#configFileName = configFileName; + this.#configFilePath = join(this.#userDataDir, this.#configFileName); + } + + async readConfig(): Promise { + let configStr; + try { + configStr = await readFile(this.#configFilePath, 'utf8'); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return { found: false }; + } + throw err; + } + return { + found: true, + data: JSON5.parse(configStr), + }; + } + + async writeConfig(configSnapshot: ConfigSnapshotOut): Promise { + const configJson = JSON5.stringify(configSnapshot, { + space: 2, + }); + await writeFile(this.#configFilePath, configJson, 'utf8'); + const { mtime } = await stat(this.#configFilePath); + return mtime; + } + + watchConfig(callback: (mtime: Date) => Promise, throttleMs: number): IDisposer { + const configChanged = throttle(async () => { + let mtime: Date; + try { + const stats = await stat(this.#configFilePath); + mtime = stats.mtime; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return; + } + throw err; + } + return callback(mtime); + }, throttleMs); + + const watcher = watch(this.#userDataDir, { + persistent: false, + }); + + watcher.on('change', (eventType, filename) => { + if (eventType === 'change' + && (filename === this.#configFileName || filename === null)) { + configChanged()?.catch((err) => { + console.log('Unhandled error while listening for config changes', err); + }); + } + }); + + return () => watcher.close(); + } +} diff --git a/packages/main/src/services/MainEnv.ts b/packages/main/src/services/MainEnv.ts deleted file mode 100644 index 23ee9a1..0000000 --- a/packages/main/src/services/MainEnv.ts +++ /dev/null @@ -1,38 +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 { IAnyStateTreeNode, getEnv as getAnyEnv } from 'mobx-state-tree'; - -import type { ConfigPersistence } from './ConfigPersistence'; - -export interface MainEnv { - configPersistence: ConfigPersistence; -} - -/** - * Gets a well-typed environment from `model`. - * - * Only useable inside state trees created by `createAndConnectRootStore`. - * - * @param model The state tree node. - */ -export function getEnv(model: IAnyStateTreeNode): MainEnv { - return getAnyEnv(model); -} diff --git a/packages/main/src/services/NativeThemeService.ts b/packages/main/src/services/NativeThemeService.ts new file mode 100644 index 0000000..7a26c3c --- /dev/null +++ b/packages/main/src/services/NativeThemeService.ts @@ -0,0 +1,38 @@ +/* + * 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 { nativeTheme } from 'electron'; +import type { IDisposer } from 'mobx-state-tree'; +import type { ThemeSource } from '@sophie/shared'; + +export class NativeThemeService { + setThemeSource(themeSource: ThemeSource): void { + nativeTheme.themeSource = themeSource; + } + + onShouldUseDarkColorsUpdated(callback: (shouldUseDarkColors: boolean) => void): IDisposer { + const wrappedCallback = () => { + callback(nativeTheme.shouldUseDarkColors); + }; + wrappedCallback(); + nativeTheme.on('updated', wrappedCallback); + return () => nativeTheme.off('updated', wrappedCallback); + } +} diff --git a/packages/main/src/services/impl/ConfigPersistenceImpl.ts b/packages/main/src/services/impl/ConfigPersistenceImpl.ts deleted file mode 100644 index 097ab74..0000000 --- a/packages/main/src/services/impl/ConfigPersistenceImpl.ts +++ /dev/null @@ -1,102 +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 'fs'; -import { readFile, stat, writeFile } from 'fs/promises'; -import JSON5 from 'json5'; -import { throttle } from 'lodash'; -import { IDisposer } from 'mobx-state-tree'; -import { join } from 'path'; - -import { CONFIG_DEBOUNCE_TIME, ConfigPersistence, ReadConfigResult } from '../ConfigPersistence'; -import type { ConfigSnapshotOut } from '../../stores/Config'; - -export class ConfigPersistenceImpl implements ConfigPersistence { - readonly #userDataDir: string; - - readonly #configFileName: string; - - readonly #configFilePath: string; - - constructor( - userDataDir: string, - configFileName: string, - ) { - this.#userDataDir = userDataDir; - this.#configFileName = configFileName; - this.#configFilePath = join(this.#userDataDir, this.#configFileName); - } - - async readConfig(): Promise { - let configStr; - try { - configStr = await readFile(this.#configFilePath, 'utf8'); - } catch (err) { - if ((err as NodeJS.ErrnoException).code === 'ENOENT') { - return { found: false }; - } - throw err; - } - return { - found: true, - data: JSON5.parse(configStr), - }; - } - - async writeConfig(configSnapshot: ConfigSnapshotOut): Promise { - const configJson = JSON5.stringify(configSnapshot, { - space: 2, - }); - await writeFile(this.#configFilePath, configJson, 'utf8'); - const { mtime } = await stat(this.#configFilePath); - return mtime; - } - - watchConfig(callback: (mtime: Date) => Promise): IDisposer { - const configChanged = throttle(async () => { - let mtime: Date; - try { - const stats = await stat(this.#configFilePath); - mtime = stats.mtime; - } catch (err) { - if ((err as NodeJS.ErrnoException).code === 'ENOENT') { - return; - } - throw err; - } - return callback(mtime); - }, CONFIG_DEBOUNCE_TIME); - - const watcher = watch(this.#userDataDir, { - persistent: false, - }); - - watcher.on('change', (eventType, filename) => { - if (eventType === 'change' - && (filename === this.#configFileName || filename === null)) { - configChanged()?.catch((err) => { - console.log('Unhandled error while listening for config changes', err); - }); - } - }); - - return () => watcher.close(); - } -} -- cgit v1.2.3-54-g00ecf