diff options
Diffstat (limited to 'packages/main/src')
16 files changed, 214 insertions, 158 deletions
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index bcdc3d7..a886a16 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts | |||
@@ -19,22 +19,24 @@ | |||
19 | * SPDX-License-Identifier: AGPL-3.0-only | 19 | * SPDX-License-Identifier: AGPL-3.0-only |
20 | */ | 20 | */ |
21 | 21 | ||
22 | import { readFileSync } from 'node:fs'; | ||
23 | import { readFile } from 'node:fs/promises'; | ||
22 | import { arch } from 'node:os'; | 24 | import { arch } from 'node:os'; |
23 | import path from 'node:path'; | 25 | import path from 'node:path'; |
24 | import { URL } from 'node:url'; | 26 | import { URL } from 'node:url'; |
25 | 27 | ||
26 | import { | 28 | import { |
27 | ServiceToMainIpcMessage, | 29 | ServiceToMainIpcMessage, |
28 | unreadCount, | 30 | UnreadCount, |
29 | WebSource, | 31 | WebSource, |
30 | } from '@sophie/service-shared'; | 32 | } from '@sophie/service-shared'; |
31 | import { | 33 | import { |
32 | action, | 34 | Action, |
33 | MainToRendererIpcMessage, | 35 | MainToRendererIpcMessage, |
34 | RendererToMainIpcMessage, | 36 | RendererToMainIpcMessage, |
35 | } from '@sophie/shared'; | 37 | } from '@sophie/shared'; |
36 | import { app, BrowserView, BrowserWindow, ipcMain } from 'electron'; | 38 | import { app, BrowserView, BrowserWindow, ipcMain } from 'electron'; |
37 | import { ensureDirSync, readFile, readFileSync } from 'fs-extra'; | 39 | import { ensureDirSync } from 'fs-extra'; |
38 | import { autorun } from 'mobx'; | 40 | import { autorun } from 'mobx'; |
39 | import { getSnapshot, onAction, onPatch } from 'mobx-state-tree'; | 41 | import { getSnapshot, onAction, onPatch } from 'mobx-state-tree'; |
40 | import osName from 'os-name'; | 42 | import osName from 'os-name'; |
@@ -45,7 +47,7 @@ import { | |||
45 | installDevToolsExtensions, | 47 | installDevToolsExtensions, |
46 | openDevToolsWhenReady, | 48 | openDevToolsWhenReady, |
47 | } from './devTools'; | 49 | } from './devTools'; |
48 | import init from './init'; | 50 | import initReactions from './initReactions'; |
49 | import { createMainStore } from './stores/MainStore'; | 51 | import { createMainStore } from './stores/MainStore'; |
50 | import { getLogger } from './utils/log'; | 52 | import { getLogger } from './utils/log'; |
51 | 53 | ||
@@ -128,7 +130,7 @@ let mainWindow: BrowserWindow | undefined; | |||
128 | 130 | ||
129 | const store = createMainStore(); | 131 | const store = createMainStore(); |
130 | 132 | ||
131 | init(store) | 133 | initReactions(store) |
132 | // eslint-disable-next-line promise/always-return -- `then` instead of top-level await. | 134 | // eslint-disable-next-line promise/always-return -- `then` instead of top-level await. |
133 | .then((disposeCompositionRoot) => { | 135 | .then((disposeCompositionRoot) => { |
134 | app.on('will-quit', disposeCompositionRoot); | 136 | app.on('will-quit', disposeCompositionRoot); |
@@ -267,7 +269,7 @@ async function createWindow(): Promise<unknown> { | |||
267 | return; | 269 | return; |
268 | } | 270 | } |
269 | try { | 271 | try { |
270 | const actionToDispatch = action.parse(rawAction); | 272 | const actionToDispatch = Action.parse(rawAction); |
271 | switch (actionToDispatch.action) { | 273 | switch (actionToDispatch.action) { |
272 | case 'set-selected-service-id': | 274 | case 'set-selected-service-id': |
273 | store.shared.setSelectedServiceId(actionToDispatch.serviceId); | 275 | store.shared.setSelectedServiceId(actionToDispatch.serviceId); |
@@ -331,7 +333,7 @@ async function createWindow(): Promise<unknown> { | |||
331 | // otherwise electron emits a no handler registered warning. | 333 | // otherwise electron emits a no handler registered warning. |
332 | break; | 334 | break; |
333 | case ServiceToMainIpcMessage.SetUnreadCount: | 335 | case ServiceToMainIpcMessage.SetUnreadCount: |
334 | log.log('Unread count:', unreadCount.parse(args[0])); | 336 | log.log('Unread count:', UnreadCount.parse(args[0])); |
335 | break; | 337 | break; |
336 | default: | 338 | default: |
337 | log.error('Unknown IPC message:', channel, args); | 339 | log.error('Unknown IPC message:', channel, args); |
diff --git a/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts b/packages/main/src/infrastructure/config/ConfigFile.ts index 88d8bf8..193a20d 100644 --- a/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts +++ b/packages/main/src/infrastructure/config/ConfigFile.ts | |||
@@ -17,48 +17,52 @@ | |||
17 | * | 17 | * |
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | |||
20 | import { watch } from 'node:fs'; | 21 | import { watch } from 'node:fs'; |
21 | import { readFile, stat, writeFile } from 'node:fs/promises'; | 22 | import { readFile, stat, writeFile } from 'node:fs/promises'; |
22 | import path from 'node:path'; | 23 | import path from 'node:path'; |
23 | 24 | ||
24 | import JSON5 from 'json5'; | 25 | import JSON5 from 'json5'; |
25 | import throttle from 'lodash-es/throttle'; | 26 | import { throttle } from 'lodash-es'; |
26 | 27 | ||
27 | import type { Config } from '../../stores/SharedStore'; | 28 | import type { Config } from '../../stores/SharedStore'; |
28 | import type Disposer from '../../utils/Disposer'; | 29 | import type Disposer from '../../utils/Disposer'; |
29 | import { getLogger } from '../../utils/log'; | 30 | import { getLogger } from '../../utils/log'; |
30 | import type ConfigPersistence from '../ConfigPersistence'; | ||
31 | import type { ReadConfigResult } from '../ConfigPersistence'; | ||
32 | 31 | ||
33 | const log = getLogger('fileBasedConfigPersistence'); | 32 | import type ConfigRepository from './ConfigRepository'; |
33 | import type ReadConfigResult from './ReadConfigResult'; | ||
34 | |||
35 | const log = getLogger('ConfigFile'); | ||
36 | |||
37 | export default class ConfigFile implements ConfigRepository { | ||
38 | readonly #userDataDir: string; | ||
39 | |||
40 | readonly #configFileName: string; | ||
34 | 41 | ||
35 | export default class FileBasedConfigPersistence implements ConfigPersistence { | 42 | readonly #configFilePath: string; |
36 | private readonly configFilePath: string; | ||
37 | 43 | ||
38 | private writingConfig = false; | 44 | #writingConfig = false; |
39 | 45 | ||
40 | private timeLastWritten: Date | undefined; | 46 | #timeLastWritten: Date | undefined; |
41 | 47 | ||
42 | constructor( | 48 | constructor(userDataDir: string, configFileName = 'config.json5') { |
43 | private readonly userDataDir: string, | 49 | this.#userDataDir = userDataDir; |
44 | private readonly configFileName: string = 'config.json5', | 50 | this.#configFileName = configFileName; |
45 | ) { | 51 | this.#configFilePath = path.join(userDataDir, configFileName); |
46 | this.configFileName = configFileName; | ||
47 | this.configFilePath = path.join(this.userDataDir, this.configFileName); | ||
48 | } | 52 | } |
49 | 53 | ||
50 | async readConfig(): Promise<ReadConfigResult> { | 54 | async readConfig(): Promise<ReadConfigResult> { |
51 | let configStr: string; | 55 | let configStr: string; |
52 | try { | 56 | try { |
53 | configStr = await readFile(this.configFilePath, 'utf8'); | 57 | configStr = await readFile(this.#configFilePath, 'utf8'); |
54 | } catch (error) { | 58 | } catch (error) { |
55 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | 59 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { |
56 | log.debug('Config file', this.configFilePath, 'was not found'); | 60 | log.debug('Config file', this.#configFilePath, 'was not found'); |
57 | return { found: false }; | 61 | return { found: false }; |
58 | } | 62 | } |
59 | throw error; | 63 | throw error; |
60 | } | 64 | } |
61 | log.info('Read config file', this.configFilePath); | 65 | log.info('Read config file', this.#configFilePath); |
62 | return { | 66 | return { |
63 | found: true, | 67 | found: true, |
64 | data: JSON5.parse(configStr), | 68 | data: JSON5.parse(configStr), |
@@ -69,32 +73,32 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { | |||
69 | const configJson = JSON5.stringify(configSnapshot, { | 73 | const configJson = JSON5.stringify(configSnapshot, { |
70 | space: 2, | 74 | space: 2, |
71 | }); | 75 | }); |
72 | this.writingConfig = true; | 76 | this.#writingConfig = true; |
73 | try { | 77 | try { |
74 | await writeFile(this.configFilePath, configJson, 'utf8'); | 78 | await writeFile(this.#configFilePath, configJson, 'utf8'); |
75 | const { mtime } = await stat(this.configFilePath); | 79 | const { mtime } = await stat(this.#configFilePath); |
76 | log.trace('Config file', this.configFilePath, 'last written at', mtime); | 80 | log.trace('Config file', this.#configFilePath, 'last written at', mtime); |
77 | this.timeLastWritten = mtime; | 81 | this.#timeLastWritten = mtime; |
78 | } finally { | 82 | } finally { |
79 | this.writingConfig = false; | 83 | this.#writingConfig = false; |
80 | } | 84 | } |
81 | log.info('Wrote config file', this.configFilePath); | 85 | log.info('Wrote config file', this.#configFilePath); |
82 | } | 86 | } |
83 | 87 | ||
84 | watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer { | 88 | watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer { |
85 | log.debug('Installing watcher for', this.userDataDir); | 89 | log.debug('Installing watcher for', this.#userDataDir); |
86 | 90 | ||
87 | const configChanged = throttle(async () => { | 91 | const configChanged = throttle(async () => { |
88 | let mtime: Date; | 92 | let mtime: Date; |
89 | try { | 93 | try { |
90 | const stats = await stat(this.configFilePath); | 94 | const stats = await stat(this.#configFilePath); |
91 | mtime = stats.mtime; | 95 | mtime = stats.mtime; |
92 | log.trace('Config file last modified at', mtime); | 96 | log.trace('Config file last modified at', mtime); |
93 | } catch (error) { | 97 | } catch (error) { |
94 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | 98 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { |
95 | log.debug( | 99 | log.debug( |
96 | 'Config file', | 100 | 'Config file', |
97 | this.configFilePath, | 101 | this.#configFilePath, |
98 | 'was deleted after being changed', | 102 | 'was deleted after being changed', |
99 | ); | 103 | ); |
100 | return; | 104 | return; |
@@ -102,27 +106,27 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { | |||
102 | throw error; | 106 | throw error; |
103 | } | 107 | } |
104 | if ( | 108 | if ( |
105 | !this.writingConfig && | 109 | !this.#writingConfig && |
106 | (this.timeLastWritten === undefined || mtime > this.timeLastWritten) | 110 | (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten) |
107 | ) { | 111 | ) { |
108 | log.debug( | 112 | log.debug( |
109 | 'Found a config file modified at', | 113 | 'Found a config file modified at', |
110 | mtime, | 114 | mtime, |
111 | 'whish is newer than last written', | 115 | 'whish is newer than last written', |
112 | this.timeLastWritten, | 116 | this.#timeLastWritten, |
113 | ); | 117 | ); |
114 | await callback(); | 118 | await callback(); |
115 | } | 119 | } |
116 | }, throttleMs); | 120 | }, throttleMs); |
117 | 121 | ||
118 | const watcher = watch(this.userDataDir, { | 122 | const watcher = watch(this.#userDataDir, { |
119 | persistent: false, | 123 | persistent: false, |
120 | }); | 124 | }); |
121 | 125 | ||
122 | watcher.on('change', (eventType, filename) => { | 126 | watcher.on('change', (eventType, filename) => { |
123 | if ( | 127 | if ( |
124 | eventType === 'change' && | 128 | eventType === 'change' && |
125 | (filename === this.configFileName || filename === null) | 129 | (filename === this.#configFileName || filename === null) |
126 | ) { | 130 | ) { |
127 | configChanged()?.catch((err) => { | 131 | configChanged()?.catch((err) => { |
128 | log.error('Unhandled error while listening for config changes', err); | 132 | log.error('Unhandled error while listening for config changes', err); |
@@ -131,7 +135,7 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { | |||
131 | }); | 135 | }); |
132 | 136 | ||
133 | return () => { | 137 | return () => { |
134 | log.trace('Removing watcher for', this.configFilePath); | 138 | log.trace('Removing watcher for', this.#configFilePath); |
135 | watcher.close(); | 139 | watcher.close(); |
136 | }; | 140 | }; |
137 | } | 141 | } |
diff --git a/packages/main/src/infrastructure/ConfigPersistence.ts b/packages/main/src/infrastructure/config/ConfigRepository.ts index 184fa8d..0ce7fc1 100644 --- a/packages/main/src/infrastructure/ConfigPersistence.ts +++ b/packages/main/src/infrastructure/config/ConfigRepository.ts | |||
@@ -18,8 +18,8 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { Config } from '../stores/SharedStore'; | 21 | import type { Config } from '../../stores/SharedStore'; |
22 | import type Disposer from '../utils/Disposer'; | 22 | import type Disposer from '../../utils/Disposer'; |
23 | 23 | ||
24 | export type ReadConfigResult = | 24 | export type ReadConfigResult = |
25 | | { found: true; data: unknown } | 25 | | { found: true; data: unknown } |
diff --git a/packages/main/src/infrastructure/config/ReadConfigResult.ts b/packages/main/src/infrastructure/config/ReadConfigResult.ts new file mode 100644 index 0000000..3b3ee55 --- /dev/null +++ b/packages/main/src/infrastructure/config/ReadConfigResult.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | type ReadConfigResult = { found: true; data: unknown } | { found: false }; | ||
22 | |||
23 | export default ReadConfigResult; | ||
diff --git a/packages/main/src/init.ts b/packages/main/src/initReactions.ts index fd8dd94..50e561d 100644 --- a/packages/main/src/init.ts +++ b/packages/main/src/initReactions.ts | |||
@@ -20,21 +20,21 @@ | |||
20 | 20 | ||
21 | import { app } from 'electron'; | 21 | import { app } from 'electron'; |
22 | 22 | ||
23 | import initConfig from './controllers/initConfig'; | 23 | import ConfigFile from './infrastructure/config/ConfigFile'; |
24 | import initNativeTheme from './controllers/initNativeTheme'; | 24 | import synchronizeConfig from './reactions/synchronizeConfig'; |
25 | import FileBasedConfigPersistence from './infrastructure/impl/FileBasedConfigPersistence'; | 25 | import synchronizeNativeTheme from './reactions/synchronizeNativeTheme'; |
26 | import { MainStore } from './stores/MainStore'; | 26 | import type MainStore from './stores/MainStore'; |
27 | import type Disposer from './utils/Disposer'; | 27 | import type Disposer from './utils/Disposer'; |
28 | 28 | ||
29 | export default async function init(store: MainStore): Promise<Disposer> { | 29 | export default async function initReactions( |
30 | const configPersistenceService = new FileBasedConfigPersistence( | 30 | store: MainStore, |
31 | app.getPath('userData'), | 31 | ): Promise<Disposer> { |
32 | ); | 32 | const configRepository = new ConfigFile(app.getPath('userData')); |
33 | const disposeConfigController = await initConfig( | 33 | const disposeConfigController = await synchronizeConfig( |
34 | store.shared, | 34 | store.shared, |
35 | configPersistenceService, | 35 | configRepository, |
36 | ); | 36 | ); |
37 | const disposeNativeThemeController = initNativeTheme(store.shared); | 37 | const disposeNativeThemeController = synchronizeNativeTheme(store.shared); |
38 | 38 | ||
39 | return () => { | 39 | return () => { |
40 | disposeNativeThemeController(); | 40 | disposeNativeThemeController(); |
diff --git a/packages/main/src/controllers/__tests__/initConfig.spec.ts b/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts index fdd22c9..c145bf3 100644 --- a/packages/main/src/controllers/__tests__/initConfig.spec.ts +++ b/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts | |||
@@ -22,14 +22,14 @@ import { jest } from '@jest/globals'; | |||
22 | import { mocked } from 'jest-mock'; | 22 | import { mocked } from 'jest-mock'; |
23 | import ms from 'ms'; | 23 | import ms from 'ms'; |
24 | 24 | ||
25 | import type ConfigPersistence from '../../infrastructure/ConfigPersistence'; | 25 | import type ConfigRepository from '../../infrastructure/config/ConfigRepository'; |
26 | import { sharedStore, SharedStore } from '../../stores/SharedStore'; | 26 | import SharedStore from '../../stores/SharedStore'; |
27 | import type Disposer from '../../utils/Disposer'; | 27 | import type Disposer from '../../utils/Disposer'; |
28 | import { silenceLogger } from '../../utils/log'; | 28 | import { silenceLogger } from '../../utils/log'; |
29 | import initConfig from '../initConfig'; | 29 | import synchronizeConfig from '../synchronizeConfig'; |
30 | 30 | ||
31 | let store: SharedStore; | 31 | let store: SharedStore; |
32 | const persistenceService: ConfigPersistence = { | 32 | const repository: ConfigRepository = { |
33 | readConfig: jest.fn(), | 33 | readConfig: jest.fn(), |
34 | writeConfig: jest.fn(), | 34 | writeConfig: jest.fn(), |
35 | watchConfig: jest.fn(), | 35 | watchConfig: jest.fn(), |
@@ -43,35 +43,33 @@ beforeAll(() => { | |||
43 | }); | 43 | }); |
44 | 44 | ||
45 | beforeEach(() => { | 45 | beforeEach(() => { |
46 | store = sharedStore.create(); | 46 | store = SharedStore.create(); |
47 | }); | 47 | }); |
48 | 48 | ||
49 | describe('when initializing', () => { | 49 | describe('when synchronizeializing', () => { |
50 | describe('when there is no config file', () => { | 50 | describe('when there is no config file', () => { |
51 | beforeEach(() => { | 51 | beforeEach(() => { |
52 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 52 | mocked(repository.readConfig).mockResolvedValueOnce({ |
53 | found: false, | 53 | found: false, |
54 | }); | 54 | }); |
55 | }); | 55 | }); |
56 | 56 | ||
57 | it('should create a new config file', async () => { | 57 | it('should create a new config file', async () => { |
58 | await initConfig(store, persistenceService); | 58 | await synchronizeConfig(store, repository); |
59 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 59 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
60 | }); | 60 | }); |
61 | 61 | ||
62 | it('should bail if there is an an error creating the config file', async () => { | 62 | it('should bail if there is an an error creating the config file', async () => { |
63 | mocked(persistenceService.writeConfig).mockRejectedValue( | 63 | mocked(repository.writeConfig).mockRejectedValue(new Error('boo')); |
64 | new Error('boo'), | ||
65 | ); | ||
66 | await expect(() => | 64 | await expect(() => |
67 | initConfig(store, persistenceService), | 65 | synchronizeConfig(store, repository), |
68 | ).rejects.toBeInstanceOf(Error); | 66 | ).rejects.toBeInstanceOf(Error); |
69 | }); | 67 | }); |
70 | }); | 68 | }); |
71 | 69 | ||
72 | describe('when there is a valid config file', () => { | 70 | describe('when there is a valid config file', () => { |
73 | beforeEach(() => { | 71 | beforeEach(() => { |
74 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 72 | mocked(repository.readConfig).mockResolvedValueOnce({ |
75 | found: true, | 73 | found: true, |
76 | data: { | 74 | data: { |
77 | // Use a default empty config file to not trigger config rewrite. | 75 | // Use a default empty config file to not trigger config rewrite. |
@@ -82,23 +80,23 @@ describe('when initializing', () => { | |||
82 | }); | 80 | }); |
83 | 81 | ||
84 | it('should read the existing config file is there is one', async () => { | 82 | it('should read the existing config file is there is one', async () => { |
85 | await initConfig(store, persistenceService); | 83 | await synchronizeConfig(store, repository); |
86 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 84 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
87 | expect(store.settings.themeSource).toBe('dark'); | 85 | expect(store.settings.themeSource).toBe('dark'); |
88 | }); | 86 | }); |
89 | 87 | ||
90 | it('should bail if it cannot set up a watcher', async () => { | 88 | it('should bail if it cannot set up a watcher', async () => { |
91 | mocked(persistenceService.watchConfig).mockImplementationOnce(() => { | 89 | mocked(repository.watchConfig).mockImplementationOnce(() => { |
92 | throw new Error('boo'); | 90 | throw new Error('boo'); |
93 | }); | 91 | }); |
94 | await expect(() => | 92 | await expect(() => |
95 | initConfig(store, persistenceService), | 93 | synchronizeConfig(store, repository), |
96 | ).rejects.toBeInstanceOf(Error); | 94 | ).rejects.toBeInstanceOf(Error); |
97 | }); | 95 | }); |
98 | }); | 96 | }); |
99 | 97 | ||
100 | it('should update the config file if new details are added during read', async () => { | 98 | it('should update the config file if new details are added during read', async () => { |
101 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 99 | mocked(repository.readConfig).mockResolvedValueOnce({ |
102 | found: true, | 100 | found: true, |
103 | data: { | 101 | data: { |
104 | themeSource: 'light', | 102 | themeSource: 'light', |
@@ -107,26 +105,26 @@ describe('when initializing', () => { | |||
107 | }, | 105 | }, |
108 | }, | 106 | }, |
109 | }); | 107 | }); |
110 | await initConfig(store, persistenceService); | 108 | await synchronizeConfig(store, repository); |
111 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 109 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
112 | }); | 110 | }); |
113 | 111 | ||
114 | it('should not apply an invalid config file but should not overwrite it', async () => { | 112 | it('should not apply an invalid config file but should not overwrite it', async () => { |
115 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 113 | mocked(repository.readConfig).mockResolvedValueOnce({ |
116 | found: true, | 114 | found: true, |
117 | data: { | 115 | data: { |
118 | themeSource: -1, | 116 | themeSource: -1, |
119 | }, | 117 | }, |
120 | }); | 118 | }); |
121 | await initConfig(store, persistenceService); | 119 | await synchronizeConfig(store, repository); |
122 | expect(store.settings.themeSource).not.toBe(-1); | 120 | expect(store.settings.themeSource).not.toBe(-1); |
123 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 121 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
124 | }); | 122 | }); |
125 | 123 | ||
126 | it('should bail if it cannot determine whether there is a config file', async () => { | 124 | it('should bail if it cannot determine whether there is a config file', async () => { |
127 | mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); | 125 | mocked(repository.readConfig).mockRejectedValue(new Error('boo')); |
128 | await expect(() => | 126 | await expect(() => |
129 | initConfig(store, persistenceService), | 127 | synchronizeConfig(store, repository), |
130 | ).rejects.toBeInstanceOf(Error); | 128 | ).rejects.toBeInstanceOf(Error); |
131 | }); | 129 | }); |
132 | }); | 130 | }); |
@@ -137,36 +135,34 @@ describe('when it has loaded the config', () => { | |||
137 | let configChangedCallback: () => Promise<void>; | 135 | let configChangedCallback: () => Promise<void>; |
138 | 136 | ||
139 | beforeEach(async () => { | 137 | beforeEach(async () => { |
140 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 138 | mocked(repository.readConfig).mockResolvedValueOnce({ |
141 | found: true, | 139 | found: true, |
142 | data: store.config, | 140 | data: store.config, |
143 | }); | 141 | }); |
144 | mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); | 142 | mocked(repository.watchConfig).mockReturnValueOnce(watcherDisposer); |
145 | sutDisposer = await initConfig(store, persistenceService, throttleMs); | 143 | sutDisposer = await synchronizeConfig(store, repository, throttleMs); |
146 | [[configChangedCallback]] = mocked( | 144 | [[configChangedCallback]] = mocked(repository.watchConfig).mock.calls; |
147 | persistenceService.watchConfig, | ||
148 | ).mock.calls; | ||
149 | jest.resetAllMocks(); | 145 | jest.resetAllMocks(); |
150 | }); | 146 | }); |
151 | 147 | ||
152 | it('should throttle saving changes to the config file', () => { | 148 | it('should throttle saving changes to the config file', () => { |
153 | mocked(persistenceService.writeConfig).mockResolvedValue(); | 149 | mocked(repository.writeConfig).mockResolvedValue(); |
154 | store.settings.setThemeSource('dark'); | 150 | store.settings.setThemeSource('dark'); |
155 | jest.advanceTimersByTime(lessThanThrottleMs); | 151 | jest.advanceTimersByTime(lessThanThrottleMs); |
156 | store.settings.setThemeSource('light'); | 152 | store.settings.setThemeSource('light'); |
157 | jest.advanceTimersByTime(throttleMs); | 153 | jest.advanceTimersByTime(throttleMs); |
158 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 154 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
159 | }); | 155 | }); |
160 | 156 | ||
161 | it('should handle config writing errors gracefully', () => { | 157 | it('should handle config writing errors gracefully', () => { |
162 | mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); | 158 | mocked(repository.writeConfig).mockRejectedValue(new Error('boo')); |
163 | store.settings.setThemeSource('dark'); | 159 | store.settings.setThemeSource('dark'); |
164 | jest.advanceTimersByTime(throttleMs); | 160 | jest.advanceTimersByTime(throttleMs); |
165 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 161 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
166 | }); | 162 | }); |
167 | 163 | ||
168 | it('should read the config file when it has changed', async () => { | 164 | it('should read the config file when it has changed', async () => { |
169 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 165 | mocked(repository.readConfig).mockResolvedValueOnce({ |
170 | found: true, | 166 | found: true, |
171 | data: { | 167 | data: { |
172 | // Use a default empty config file to not trigger config rewrite. | 168 | // Use a default empty config file to not trigger config rewrite. |
@@ -176,12 +172,12 @@ describe('when it has loaded the config', () => { | |||
176 | }); | 172 | }); |
177 | await configChangedCallback(); | 173 | await configChangedCallback(); |
178 | // Do not write back the changes we have just read. | 174 | // Do not write back the changes we have just read. |
179 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 175 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
180 | expect(store.settings.themeSource).toBe('dark'); | 176 | expect(store.settings.themeSource).toBe('dark'); |
181 | }); | 177 | }); |
182 | 178 | ||
183 | it('should update the config file if new details are added', async () => { | 179 | it('should update the config file if new details are added', async () => { |
184 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 180 | mocked(repository.readConfig).mockResolvedValueOnce({ |
185 | found: true, | 181 | found: true, |
186 | data: { | 182 | data: { |
187 | themeSource: 'light', | 183 | themeSource: 'light', |
@@ -191,11 +187,11 @@ describe('when it has loaded the config', () => { | |||
191 | }, | 187 | }, |
192 | }); | 188 | }); |
193 | await configChangedCallback(); | 189 | await configChangedCallback(); |
194 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 190 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
195 | }); | 191 | }); |
196 | 192 | ||
197 | it('should not apply an invalid config file when it has changed but should not overwrite it', async () => { | 193 | it('should not apply an invalid config file when it has changed but should not overwrite it', async () => { |
198 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 194 | mocked(repository.readConfig).mockResolvedValueOnce({ |
199 | found: true, | 195 | found: true, |
200 | data: { | 196 | data: { |
201 | themeSource: -1, | 197 | themeSource: -1, |
@@ -203,11 +199,11 @@ describe('when it has loaded the config', () => { | |||
203 | }); | 199 | }); |
204 | await configChangedCallback(); | 200 | await configChangedCallback(); |
205 | expect(store.settings.themeSource).not.toBe(-1); | 201 | expect(store.settings.themeSource).not.toBe(-1); |
206 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 202 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
207 | }); | 203 | }); |
208 | 204 | ||
209 | it('should handle config reading errors gracefully', async () => { | 205 | it('should handle config reading errors gracefully', async () => { |
210 | mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); | 206 | mocked(repository.readConfig).mockRejectedValue(new Error('boo')); |
211 | await expect(configChangedCallback()).resolves.not.toThrow(); | 207 | await expect(configChangedCallback()).resolves.not.toThrow(); |
212 | }); | 208 | }); |
213 | 209 | ||
@@ -223,7 +219,7 @@ describe('when it has loaded the config', () => { | |||
223 | it('should not listen to store changes any more', () => { | 219 | it('should not listen to store changes any more', () => { |
224 | store.settings.setThemeSource('dark'); | 220 | store.settings.setThemeSource('dark'); |
225 | jest.advanceTimersByTime(2 * throttleMs); | 221 | jest.advanceTimersByTime(2 * throttleMs); |
226 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 222 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
227 | }); | 223 | }); |
228 | }); | 224 | }); |
229 | }); | 225 | }); |
diff --git a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts b/packages/main/src/reactions/__tests__/synchronizeNativeTheme.spec.ts index 9107c78..cf37568 100644 --- a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts +++ b/packages/main/src/reactions/__tests__/synchronizeNativeTheme.spec.ts | |||
@@ -21,7 +21,7 @@ | |||
21 | import { jest } from '@jest/globals'; | 21 | import { jest } from '@jest/globals'; |
22 | import { mocked } from 'jest-mock'; | 22 | import { mocked } from 'jest-mock'; |
23 | 23 | ||
24 | import { sharedStore, SharedStore } from '../../stores/SharedStore'; | 24 | import SharedStore from '../../stores/SharedStore'; |
25 | import type Disposer from '../../utils/Disposer'; | 25 | import type Disposer from '../../utils/Disposer'; |
26 | 26 | ||
27 | let shouldUseDarkColors = false; | 27 | let shouldUseDarkColors = false; |
@@ -38,14 +38,16 @@ jest.unstable_mockModule('electron', () => ({ | |||
38 | })); | 38 | })); |
39 | 39 | ||
40 | const { nativeTheme } = await import('electron'); | 40 | const { nativeTheme } = await import('electron'); |
41 | const { default: initNativeTheme } = await import('../initNativeTheme'); | 41 | const { default: synchronizeNativeTheme } = await import( |
42 | '../synchronizeNativeTheme' | ||
43 | ); | ||
42 | 44 | ||
43 | let store: SharedStore; | 45 | let store: SharedStore; |
44 | let disposeSut: Disposer; | 46 | let disposeSut: Disposer; |
45 | 47 | ||
46 | beforeEach(() => { | 48 | beforeEach(() => { |
47 | store = sharedStore.create(); | 49 | store = SharedStore.create(); |
48 | disposeSut = initNativeTheme(store); | 50 | disposeSut = synchronizeNativeTheme(store); |
49 | }); | 51 | }); |
50 | 52 | ||
51 | it('should register a nativeTheme updated listener', () => { | 53 | it('should register a nativeTheme updated listener', () => { |
diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/reactions/synchronizeConfig.ts index 55bf6df..480cc1a 100644 --- a/packages/main/src/controllers/initConfig.ts +++ b/packages/main/src/reactions/synchronizeConfig.ts | |||
@@ -23,32 +23,31 @@ import { debounce } from 'lodash-es'; | |||
23 | import { reaction } from 'mobx'; | 23 | import { reaction } from 'mobx'; |
24 | import ms from 'ms'; | 24 | import ms from 'ms'; |
25 | 25 | ||
26 | import type ConfigPersistence from '../infrastructure/ConfigPersistence'; | 26 | import type ConfigRepository from '../infrastructure/config/ConfigRepository'; |
27 | import { Config, SharedStore } from '../stores/SharedStore'; | 27 | import type SharedStore from '../stores/SharedStore'; |
28 | import type { Config } from '../stores/SharedStore'; | ||
28 | import type Disposer from '../utils/Disposer'; | 29 | import type Disposer from '../utils/Disposer'; |
29 | import { getLogger } from '../utils/log'; | 30 | import { getLogger } from '../utils/log'; |
30 | 31 | ||
31 | const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); | 32 | const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); |
32 | 33 | ||
33 | const log = getLogger('config'); | 34 | const log = getLogger('synchronizeConfig'); |
34 | 35 | ||
35 | export default async function initConfig( | 36 | export default async function synchronizeConfig( |
36 | sharedStore: SharedStore, | 37 | sharedStore: SharedStore, |
37 | persistenceService: ConfigPersistence, | 38 | repository: ConfigRepository, |
38 | debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, | 39 | debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, |
39 | ): Promise<Disposer> { | 40 | ): Promise<Disposer> { |
40 | log.trace('Initializing config controller'); | ||
41 | |||
42 | let lastConfigOnDisk: Config | undefined; | 41 | let lastConfigOnDisk: Config | undefined; |
43 | 42 | ||
44 | async function writeConfig(): Promise<void> { | 43 | async function writeConfig(): Promise<void> { |
45 | const { config } = sharedStore; | 44 | const { config } = sharedStore; |
46 | await persistenceService.writeConfig(config); | 45 | await repository.writeConfig(config); |
47 | lastConfigOnDisk = config; | 46 | lastConfigOnDisk = config; |
48 | } | 47 | } |
49 | 48 | ||
50 | async function readConfig(): Promise<boolean> { | 49 | async function readConfig(): Promise<boolean> { |
51 | const result = await persistenceService.readConfig(); | 50 | const result = await repository.readConfig(); |
52 | if (result.found) { | 51 | if (result.found) { |
53 | try { | 52 | try { |
54 | // This cast is unsound if the config file is invalid, | 53 | // This cast is unsound if the config file is invalid, |
@@ -84,7 +83,7 @@ export default async function initConfig( | |||
84 | }, debounceTime), | 83 | }, debounceTime), |
85 | ); | 84 | ); |
86 | 85 | ||
87 | const disposeWatcher = persistenceService.watchConfig(async () => { | 86 | const disposeWatcher = repository.watchConfig(async () => { |
88 | try { | 87 | try { |
89 | await readConfig(); | 88 | await readConfig(); |
90 | } catch (error) { | 89 | } catch (error) { |
@@ -93,7 +92,6 @@ export default async function initConfig( | |||
93 | }, debounceTime); | 92 | }, debounceTime); |
94 | 93 | ||
95 | return () => { | 94 | return () => { |
96 | log.trace('Disposing config controller'); | ||
97 | disposeWatcher(); | 95 | disposeWatcher(); |
98 | disposeReaction(); | 96 | disposeReaction(); |
99 | }; | 97 | }; |
diff --git a/packages/main/src/controllers/initNativeTheme.ts b/packages/main/src/reactions/synchronizeNativeTheme.ts index ce7972a..8c4edb3 100644 --- a/packages/main/src/controllers/initNativeTheme.ts +++ b/packages/main/src/reactions/synchronizeNativeTheme.ts | |||
@@ -21,15 +21,13 @@ | |||
21 | import { nativeTheme } from 'electron'; | 21 | import { nativeTheme } from 'electron'; |
22 | import { autorun } from 'mobx'; | 22 | import { autorun } from 'mobx'; |
23 | 23 | ||
24 | import type { SharedStore } from '../stores/SharedStore'; | 24 | import type SharedStore from '../stores/SharedStore'; |
25 | import type Disposer from '../utils/Disposer'; | 25 | import type Disposer from '../utils/Disposer'; |
26 | import { getLogger } from '../utils/log'; | 26 | import { getLogger } from '../utils/log'; |
27 | 27 | ||
28 | const log = getLogger('nativeTheme'); | 28 | const log = getLogger('synchronizeNativeTheme'); |
29 | 29 | ||
30 | export default function initNativeTheme(store: SharedStore): Disposer { | 30 | export default function initNativeTheme(store: SharedStore): Disposer { |
31 | log.trace('Initializing nativeTheme controller'); | ||
32 | |||
33 | const disposeThemeSourceReaction = autorun(() => { | 31 | const disposeThemeSourceReaction = autorun(() => { |
34 | nativeTheme.themeSource = store.settings.themeSource; | 32 | nativeTheme.themeSource = store.settings.themeSource; |
35 | log.debug('Set theme source:', store.settings.themeSource); | 33 | log.debug('Set theme source:', store.settings.themeSource); |
@@ -43,7 +41,6 @@ export default function initNativeTheme(store: SharedStore): Disposer { | |||
43 | nativeTheme.on('updated', shouldUseDarkColorsListener); | 41 | nativeTheme.on('updated', shouldUseDarkColorsListener); |
44 | 42 | ||
45 | return () => { | 43 | return () => { |
46 | log.trace('Disposing nativeTheme controller'); | ||
47 | nativeTheme.off('updated', shouldUseDarkColorsListener); | 44 | nativeTheme.off('updated', shouldUseDarkColorsListener); |
48 | disposeThemeSourceReaction(); | 45 | disposeThemeSourceReaction(); |
49 | }; | 46 | }; |
diff --git a/packages/main/src/stores/GlobalSettings.ts b/packages/main/src/stores/GlobalSettings.ts index 1eb13b3..0a54aa3 100644 --- a/packages/main/src/stores/GlobalSettings.ts +++ b/packages/main/src/stores/GlobalSettings.ts | |||
@@ -19,18 +19,24 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { | 21 | import { |
22 | globalSettings as originalGlobalSettings, | 22 | GlobalSettings as GlobalSettingsBase, |
23 | ThemeSource, | 23 | ThemeSource, |
24 | } from '@sophie/shared'; | 24 | } from '@sophie/shared'; |
25 | import { Instance } from 'mobx-state-tree'; | 25 | import { Instance } from 'mobx-state-tree'; |
26 | 26 | ||
27 | export const globalSettings = originalGlobalSettings.actions((self) => ({ | 27 | const GlobalSettings = GlobalSettingsBase.actions((self) => ({ |
28 | setThemeSource(mode: ThemeSource): void { | 28 | setThemeSource(mode: ThemeSource): void { |
29 | self.themeSource = mode; | 29 | self.themeSource = mode; |
30 | }, | 30 | }, |
31 | })); | 31 | })); |
32 | 32 | ||
33 | export interface GlobalSettings extends Instance<typeof globalSettings> {} | 33 | /* |
34 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
35 | Intentionally naming the type the same as the store definition. | ||
36 | */ | ||
37 | interface GlobalSettings extends Instance<typeof GlobalSettings> {} | ||
38 | |||
39 | export default GlobalSettings; | ||
34 | 40 | ||
35 | export type { | 41 | export type { |
36 | GlobalSettingsSnapshotIn, | 42 | GlobalSettingsSnapshotIn, |
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts index 18f5bf9..ff014c9 100644 --- a/packages/main/src/stores/MainStore.ts +++ b/packages/main/src/stores/MainStore.ts | |||
@@ -21,12 +21,12 @@ | |||
21 | import type { BrowserViewBounds } from '@sophie/shared'; | 21 | import type { BrowserViewBounds } from '@sophie/shared'; |
22 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; | 22 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; |
23 | 23 | ||
24 | import { GlobalSettings } from './GlobalSettings'; | 24 | import GlobalSettings from './GlobalSettings'; |
25 | import { Profile } from './Profile'; | 25 | import Profile from './Profile'; |
26 | import { Service } from './Service'; | 26 | import Service from './Service'; |
27 | import { sharedStore } from './SharedStore'; | 27 | import SharedStore from './SharedStore'; |
28 | 28 | ||
29 | export const mainStore = types | 29 | const MainStore = types |
30 | .model('MainStore', { | 30 | .model('MainStore', { |
31 | browserViewBounds: types.optional( | 31 | browserViewBounds: types.optional( |
32 | types.model('BrowserViewBounds', { | 32 | types.model('BrowserViewBounds', { |
@@ -37,7 +37,7 @@ export const mainStore = types | |||
37 | }), | 37 | }), |
38 | {}, | 38 | {}, |
39 | ), | 39 | ), |
40 | shared: types.optional(sharedStore, {}), | 40 | shared: types.optional(SharedStore, {}), |
41 | }) | 41 | }) |
42 | .views((self) => ({ | 42 | .views((self) => ({ |
43 | get settings(): GlobalSettings { | 43 | get settings(): GlobalSettings { |
@@ -56,8 +56,14 @@ export const mainStore = types | |||
56 | }, | 56 | }, |
57 | })); | 57 | })); |
58 | 58 | ||
59 | export interface MainStore extends Instance<typeof mainStore> {} | 59 | /* |
60 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
61 | Intentionally naming the type the same as the store definition. | ||
62 | */ | ||
63 | interface MainStore extends Instance<typeof MainStore> {} | ||
64 | |||
65 | export default MainStore; | ||
60 | 66 | ||
61 | export function createMainStore(): MainStore { | 67 | export function createMainStore(): MainStore { |
62 | return mainStore.create(); | 68 | return MainStore.create(); |
63 | } | 69 | } |
diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts index 5f77fe4..ec2a64b 100644 --- a/packages/main/src/stores/Profile.ts +++ b/packages/main/src/stores/Profile.ts | |||
@@ -19,7 +19,7 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { | 21 | import { |
22 | profile as originalProfile, | 22 | Profile as ProfileBase, |
23 | ProfileSettingsSnapshotIn, | 23 | ProfileSettingsSnapshotIn, |
24 | } from '@sophie/shared'; | 24 | } from '@sophie/shared'; |
25 | import { getSnapshot, Instance } from 'mobx-state-tree'; | 25 | import { getSnapshot, Instance } from 'mobx-state-tree'; |
@@ -30,14 +30,20 @@ export interface ProfileConfig extends ProfileSettingsSnapshotIn { | |||
30 | id?: string | undefined; | 30 | id?: string | undefined; |
31 | } | 31 | } |
32 | 32 | ||
33 | export const profile = originalProfile.views((self) => ({ | 33 | const Profile = ProfileBase.views((self) => ({ |
34 | get config(): ProfileConfig { | 34 | get config(): ProfileConfig { |
35 | const { id, settings } = self; | 35 | const { id, settings } = self; |
36 | return { ...getSnapshot(settings), id }; | 36 | return { ...getSnapshot(settings), id }; |
37 | }, | 37 | }, |
38 | })); | 38 | })); |
39 | 39 | ||
40 | export interface Profile extends Instance<typeof profile> {} | 40 | /* |
41 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
42 | Intentionally naming the type the same as the store definition. | ||
43 | */ | ||
44 | interface Profile extends Instance<typeof Profile> {} | ||
45 | |||
46 | export default Profile; | ||
41 | 47 | ||
42 | export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; | 48 | export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; |
43 | 49 | ||
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts index 331805b..e70caa6 100644 --- a/packages/main/src/stores/Service.ts +++ b/packages/main/src/stores/Service.ts | |||
@@ -19,14 +19,14 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { UnreadCount } from '@sophie/service-shared'; | 21 | import type { UnreadCount } from '@sophie/service-shared'; |
22 | import { service as originalService } from '@sophie/shared'; | 22 | import { Service as ServiceBase } from '@sophie/shared'; |
23 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; | 23 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; |
24 | 24 | ||
25 | import generateId from '../utils/generateId'; | 25 | import generateId from '../utils/generateId'; |
26 | import overrideProps from '../utils/overrideProps'; | 26 | import overrideProps from '../utils/overrideProps'; |
27 | 27 | ||
28 | import { ProfileSettingsSnapshotWithId } from './Profile'; | 28 | import { ProfileSettingsSnapshotWithId } from './Profile'; |
29 | import { serviceSettings, ServiceSettingsSnapshotIn } from './ServiceSettings'; | 29 | import ServiceSettings, { ServiceSettingsSnapshotIn } from './ServiceSettings'; |
30 | 30 | ||
31 | export interface ServiceConfig | 31 | export interface ServiceConfig |
32 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { | 32 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { |
@@ -35,8 +35,8 @@ export interface ServiceConfig | |||
35 | profile?: ReferenceIdentifier | undefined; | 35 | profile?: ReferenceIdentifier | undefined; |
36 | } | 36 | } |
37 | 37 | ||
38 | export const service = overrideProps(originalService, { | 38 | const Service = overrideProps(ServiceBase, { |
39 | settings: serviceSettings, | 39 | settings: ServiceSettings, |
40 | }) | 40 | }) |
41 | .views((self) => ({ | 41 | .views((self) => ({ |
42 | get config(): ServiceConfig { | 42 | get config(): ServiceConfig { |
@@ -83,7 +83,13 @@ export const service = overrideProps(originalService, { | |||
83 | }, | 83 | }, |
84 | })); | 84 | })); |
85 | 85 | ||
86 | export interface Service extends Instance<typeof service> {} | 86 | /* |
87 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
88 | Intentionally naming the type the same as the store definition. | ||
89 | */ | ||
90 | interface Service extends Instance<typeof Service> {} | ||
91 | |||
92 | export default Service; | ||
87 | 93 | ||
88 | export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; | 94 | export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; |
89 | 95 | ||
diff --git a/packages/main/src/stores/ServiceSettings.ts b/packages/main/src/stores/ServiceSettings.ts index 960de9b..e6f48c6 100644 --- a/packages/main/src/stores/ServiceSettings.ts +++ b/packages/main/src/stores/ServiceSettings.ts | |||
@@ -18,18 +18,24 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { serviceSettings as originalServiceSettings } from '@sophie/shared'; | 21 | import { ServiceSettings as ServiceSettingsBase } from '@sophie/shared'; |
22 | import { Instance, types } from 'mobx-state-tree'; | 22 | import { Instance, types } from 'mobx-state-tree'; |
23 | 23 | ||
24 | import overrideProps from '../utils/overrideProps'; | 24 | import overrideProps from '../utils/overrideProps'; |
25 | 25 | ||
26 | import { profile } from './Profile'; | 26 | import Profile from './Profile'; |
27 | 27 | ||
28 | export const serviceSettings = overrideProps(originalServiceSettings, { | 28 | const ServiceSettings = overrideProps(ServiceSettingsBase, { |
29 | profile: types.reference(profile), | 29 | profile: types.reference(Profile), |
30 | }); | 30 | }); |
31 | 31 | ||
32 | export interface ServiceSettings extends Instance<typeof serviceSettings> {} | 32 | /* |
33 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
34 | Intentionally naming the type the same as the store definition. | ||
35 | */ | ||
36 | interface ServiceSettings extends Instance<typeof ServiceSettings> {} | ||
37 | |||
38 | export default ServiceSettings; | ||
33 | 39 | ||
34 | export type { | 40 | export type { |
35 | ServiceSettingsSnapshotIn, | 41 | ServiceSettingsSnapshotIn, |
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index 499d1ee..c34af75 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts | |||
@@ -18,7 +18,7 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { sharedStore as originalSharedStore } from '@sophie/shared'; | 21 | import { SharedStore as SharedStoreBase } from '@sophie/shared'; |
22 | import { | 22 | import { |
23 | applySnapshot, | 23 | applySnapshot, |
24 | getSnapshot, | 24 | getSnapshot, |
@@ -28,18 +28,16 @@ import { | |||
28 | IReferenceType, | 28 | IReferenceType, |
29 | IStateTreeNode, | 29 | IStateTreeNode, |
30 | IType, | 30 | IType, |
31 | resolveIdentifier, | ||
32 | types, | 31 | types, |
33 | } from 'mobx-state-tree'; | 32 | } from 'mobx-state-tree'; |
34 | 33 | ||
35 | import { getLogger } from '../utils/log'; | 34 | import { getLogger } from '../utils/log'; |
36 | import overrideProps from '../utils/overrideProps'; | 35 | import overrideProps from '../utils/overrideProps'; |
37 | 36 | ||
38 | import { globalSettings, GlobalSettingsSnapshotIn } from './GlobalSettings'; | 37 | import GlobalSettings, { GlobalSettingsSnapshotIn } from './GlobalSettings'; |
39 | import { addMissingProfileIds, profile, ProfileConfig } from './Profile'; | 38 | import Profile, { addMissingProfileIds, ProfileConfig } from './Profile'; |
40 | import { | 39 | import Service, { |
41 | addMissingServiceIdsAndProfiles, | 40 | addMissingServiceIdsAndProfiles, |
42 | service, | ||
43 | ServiceConfig, | 41 | ServiceConfig, |
44 | } from './Service'; | 42 | } from './Service'; |
45 | 43 | ||
@@ -86,13 +84,13 @@ function applySettings< | |||
86 | current.push(...toApply.map(([id]) => id)); | 84 | current.push(...toApply.map(([id]) => id)); |
87 | } | 85 | } |
88 | 86 | ||
89 | export const sharedStore = overrideProps(originalSharedStore, { | 87 | const SharedStore = overrideProps(SharedStoreBase, { |
90 | settings: types.optional(globalSettings, {}), | 88 | settings: types.optional(GlobalSettings, {}), |
91 | profilesById: types.map(profile), | 89 | profilesById: types.map(Profile), |
92 | profiles: types.array(types.reference(profile)), | 90 | profiles: types.array(types.reference(Profile)), |
93 | servicesById: types.map(service), | 91 | servicesById: types.map(Service), |
94 | services: types.array(types.reference(service)), | 92 | services: types.array(types.reference(Service)), |
95 | selectedService: types.safeReference(service), | 93 | selectedService: types.safeReference(Service), |
96 | }) | 94 | }) |
97 | .views((self) => ({ | 95 | .views((self) => ({ |
98 | get config(): Config { | 96 | get config(): Config { |
@@ -142,7 +140,7 @@ export const sharedStore = overrideProps(originalSharedStore, { | |||
142 | self.shouldUseDarkColors = shouldUseDarkColors; | 140 | self.shouldUseDarkColors = shouldUseDarkColors; |
143 | }, | 141 | }, |
144 | setSelectedServiceId(serviceId: string): void { | 142 | setSelectedServiceId(serviceId: string): void { |
145 | const serviceInstance = resolveIdentifier(service, self, serviceId); | 143 | const serviceInstance = self.servicesById.get(serviceId); |
146 | if (serviceInstance === undefined) { | 144 | if (serviceInstance === undefined) { |
147 | log.warn('Trying to select unknown service', serviceId); | 145 | log.warn('Trying to select unknown service', serviceId); |
148 | return; | 146 | return; |
@@ -152,7 +150,13 @@ export const sharedStore = overrideProps(originalSharedStore, { | |||
152 | }, | 150 | }, |
153 | })); | 151 | })); |
154 | 152 | ||
155 | export interface SharedStore extends Instance<typeof sharedStore> {} | 153 | /* |
154 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
155 | Intentionally naming the type the same as the store definition. | ||
156 | */ | ||
157 | interface SharedStore extends Instance<typeof SharedStore> {} | ||
158 | |||
159 | export default SharedStore; | ||
156 | 160 | ||
157 | export type { | 161 | export type { |
158 | SharedStoreSnapshotIn, | 162 | SharedStoreSnapshotIn, |
diff --git a/packages/main/src/stores/__tests__/SharedStore.spec.ts b/packages/main/src/stores/__tests__/SharedStore.spec.ts index 3ea187c..dfd59a1 100644 --- a/packages/main/src/stores/__tests__/SharedStore.spec.ts +++ b/packages/main/src/stores/__tests__/SharedStore.spec.ts | |||
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | import type { ProfileConfig } from '../Profile'; | 21 | import type { ProfileConfig } from '../Profile'; |
22 | import type { ServiceConfig } from '../Service'; | 22 | import type { ServiceConfig } from '../Service'; |
23 | import { Config, sharedStore, SharedStore } from '../SharedStore'; | 23 | import SharedStore, { Config } from '../SharedStore'; |
24 | 24 | ||
25 | const profileProps: ProfileConfig = { | 25 | const profileProps: ProfileConfig = { |
26 | name: 'Test profile', | 26 | name: 'Test profile', |
@@ -34,7 +34,7 @@ const serviceProps: ServiceConfig = { | |||
34 | let sut: SharedStore; | 34 | let sut: SharedStore; |
35 | 35 | ||
36 | beforeEach(() => { | 36 | beforeEach(() => { |
37 | sut = sharedStore.create(); | 37 | sut = SharedStore.create(); |
38 | }); | 38 | }); |
39 | 39 | ||
40 | describe('loadConfig', () => { | 40 | describe('loadConfig', () => { |