aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/main/src/index.ts16
-rw-r--r--packages/main/src/infrastructure/config/ConfigFile.ts (renamed from packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts)70
-rw-r--r--packages/main/src/infrastructure/config/ConfigRepository.ts (renamed from packages/main/src/infrastructure/ConfigPersistence.ts)4
-rw-r--r--packages/main/src/infrastructure/config/ReadConfigResult.ts23
-rw-r--r--packages/main/src/initReactions.ts (renamed from packages/main/src/init.ts)22
-rw-r--r--packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts (renamed from packages/main/src/controllers/__tests__/initConfig.spec.ts)84
-rw-r--r--packages/main/src/reactions/__tests__/synchronizeNativeTheme.spec.ts (renamed from packages/main/src/controllers/__tests__/initNativeTheme.spec.ts)10
-rw-r--r--packages/main/src/reactions/synchronizeConfig.ts (renamed from packages/main/src/controllers/initConfig.ts)20
-rw-r--r--packages/main/src/reactions/synchronizeNativeTheme.ts (renamed from packages/main/src/controllers/initNativeTheme.ts)7
-rw-r--r--packages/main/src/stores/GlobalSettings.ts12
-rw-r--r--packages/main/src/stores/MainStore.ts22
-rw-r--r--packages/main/src/stores/Profile.ts12
-rw-r--r--packages/main/src/stores/Service.ts16
-rw-r--r--packages/main/src/stores/ServiceSettings.ts16
-rw-r--r--packages/main/src/stores/SharedStore.ts34
-rw-r--r--packages/main/src/stores/__tests__/SharedStore.spec.ts4
-rw-r--r--packages/preload/src/contextBridge/createSophieRenderer.ts22
-rw-r--r--packages/renderer/src/components/StoreProvider.tsx3
-rw-r--r--packages/renderer/src/stores/RendererStore.ts24
-rw-r--r--packages/renderer/vite.config.js3
-rw-r--r--packages/service-preload/src/index.ts4
-rw-r--r--packages/service-shared/src/index.ts3
-rw-r--r--packages/service-shared/src/schemas.ts16
-rw-r--r--packages/shared/src/index.ts21
-rw-r--r--packages/shared/src/schemas.ts44
-rw-r--r--packages/shared/src/stores/GlobalSettings.ts18
-rw-r--r--packages/shared/src/stores/Profile.ts14
-rw-r--r--packages/shared/src/stores/ProfileSettings.ts14
-rw-r--r--packages/shared/src/stores/Service.ts14
-rw-r--r--packages/shared/src/stores/ServiceSettings.ts18
-rw-r--r--packages/shared/src/stores/SharedStore.ts34
31 files changed, 366 insertions, 258 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
22import { readFileSync } from 'node:fs';
23import { readFile } from 'node:fs/promises';
22import { arch } from 'node:os'; 24import { arch } from 'node:os';
23import path from 'node:path'; 25import path from 'node:path';
24import { URL } from 'node:url'; 26import { URL } from 'node:url';
25 27
26import { 28import {
27 ServiceToMainIpcMessage, 29 ServiceToMainIpcMessage,
28 unreadCount, 30 UnreadCount,
29 WebSource, 31 WebSource,
30} from '@sophie/service-shared'; 32} from '@sophie/service-shared';
31import { 33import {
32 action, 34 Action,
33 MainToRendererIpcMessage, 35 MainToRendererIpcMessage,
34 RendererToMainIpcMessage, 36 RendererToMainIpcMessage,
35} from '@sophie/shared'; 37} from '@sophie/shared';
36import { app, BrowserView, BrowserWindow, ipcMain } from 'electron'; 38import { app, BrowserView, BrowserWindow, ipcMain } from 'electron';
37import { ensureDirSync, readFile, readFileSync } from 'fs-extra'; 39import { ensureDirSync } from 'fs-extra';
38import { autorun } from 'mobx'; 40import { autorun } from 'mobx';
39import { getSnapshot, onAction, onPatch } from 'mobx-state-tree'; 41import { getSnapshot, onAction, onPatch } from 'mobx-state-tree';
40import osName from 'os-name'; 42import osName from 'os-name';
@@ -45,7 +47,7 @@ import {
45 installDevToolsExtensions, 47 installDevToolsExtensions,
46 openDevToolsWhenReady, 48 openDevToolsWhenReady,
47} from './devTools'; 49} from './devTools';
48import init from './init'; 50import initReactions from './initReactions';
49import { createMainStore } from './stores/MainStore'; 51import { createMainStore } from './stores/MainStore';
50import { getLogger } from './utils/log'; 52import { getLogger } from './utils/log';
51 53
@@ -128,7 +130,7 @@ let mainWindow: BrowserWindow | undefined;
128 130
129const store = createMainStore(); 131const store = createMainStore();
130 132
131init(store) 133initReactions(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
20import { watch } from 'node:fs'; 21import { watch } from 'node:fs';
21import { readFile, stat, writeFile } from 'node:fs/promises'; 22import { readFile, stat, writeFile } from 'node:fs/promises';
22import path from 'node:path'; 23import path from 'node:path';
23 24
24import JSON5 from 'json5'; 25import JSON5 from 'json5';
25import throttle from 'lodash-es/throttle'; 26import { throttle } from 'lodash-es';
26 27
27import type { Config } from '../../stores/SharedStore'; 28import type { Config } from '../../stores/SharedStore';
28import type Disposer from '../../utils/Disposer'; 29import type Disposer from '../../utils/Disposer';
29import { getLogger } from '../../utils/log'; 30import { getLogger } from '../../utils/log';
30import type ConfigPersistence from '../ConfigPersistence';
31import type { ReadConfigResult } from '../ConfigPersistence';
32 31
33const log = getLogger('fileBasedConfigPersistence'); 32import type ConfigRepository from './ConfigRepository';
33import type ReadConfigResult from './ReadConfigResult';
34
35const log = getLogger('ConfigFile');
36
37export default class ConfigFile implements ConfigRepository {
38 readonly #userDataDir: string;
39
40 readonly #configFileName: string;
34 41
35export 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
21import type { Config } from '../stores/SharedStore'; 21import type { Config } from '../../stores/SharedStore';
22import type Disposer from '../utils/Disposer'; 22import type Disposer from '../../utils/Disposer';
23 23
24export type ReadConfigResult = 24export 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
21type ReadConfigResult = { found: true; data: unknown } | { found: false };
22
23export 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
21import { app } from 'electron'; 21import { app } from 'electron';
22 22
23import initConfig from './controllers/initConfig'; 23import ConfigFile from './infrastructure/config/ConfigFile';
24import initNativeTheme from './controllers/initNativeTheme'; 24import synchronizeConfig from './reactions/synchronizeConfig';
25import FileBasedConfigPersistence from './infrastructure/impl/FileBasedConfigPersistence'; 25import synchronizeNativeTheme from './reactions/synchronizeNativeTheme';
26import { MainStore } from './stores/MainStore'; 26import type MainStore from './stores/MainStore';
27import type Disposer from './utils/Disposer'; 27import type Disposer from './utils/Disposer';
28 28
29export default async function init(store: MainStore): Promise<Disposer> { 29export 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';
22import { mocked } from 'jest-mock'; 22import { mocked } from 'jest-mock';
23import ms from 'ms'; 23import ms from 'ms';
24 24
25import type ConfigPersistence from '../../infrastructure/ConfigPersistence'; 25import type ConfigRepository from '../../infrastructure/config/ConfigRepository';
26import { sharedStore, SharedStore } from '../../stores/SharedStore'; 26import SharedStore from '../../stores/SharedStore';
27import type Disposer from '../../utils/Disposer'; 27import type Disposer from '../../utils/Disposer';
28import { silenceLogger } from '../../utils/log'; 28import { silenceLogger } from '../../utils/log';
29import initConfig from '../initConfig'; 29import synchronizeConfig from '../synchronizeConfig';
30 30
31let store: SharedStore; 31let store: SharedStore;
32const persistenceService: ConfigPersistence = { 32const 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
45beforeEach(() => { 45beforeEach(() => {
46 store = sharedStore.create(); 46 store = SharedStore.create();
47}); 47});
48 48
49describe('when initializing', () => { 49describe('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 @@
21import { jest } from '@jest/globals'; 21import { jest } from '@jest/globals';
22import { mocked } from 'jest-mock'; 22import { mocked } from 'jest-mock';
23 23
24import { sharedStore, SharedStore } from '../../stores/SharedStore'; 24import SharedStore from '../../stores/SharedStore';
25import type Disposer from '../../utils/Disposer'; 25import type Disposer from '../../utils/Disposer';
26 26
27let shouldUseDarkColors = false; 27let shouldUseDarkColors = false;
@@ -38,14 +38,16 @@ jest.unstable_mockModule('electron', () => ({
38})); 38}));
39 39
40const { nativeTheme } = await import('electron'); 40const { nativeTheme } = await import('electron');
41const { default: initNativeTheme } = await import('../initNativeTheme'); 41const { default: synchronizeNativeTheme } = await import(
42 '../synchronizeNativeTheme'
43);
42 44
43let store: SharedStore; 45let store: SharedStore;
44let disposeSut: Disposer; 46let disposeSut: Disposer;
45 47
46beforeEach(() => { 48beforeEach(() => {
47 store = sharedStore.create(); 49 store = SharedStore.create();
48 disposeSut = initNativeTheme(store); 50 disposeSut = synchronizeNativeTheme(store);
49}); 51});
50 52
51it('should register a nativeTheme updated listener', () => { 53it('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';
23import { reaction } from 'mobx'; 23import { reaction } from 'mobx';
24import ms from 'ms'; 24import ms from 'ms';
25 25
26import type ConfigPersistence from '../infrastructure/ConfigPersistence'; 26import type ConfigRepository from '../infrastructure/config/ConfigRepository';
27import { Config, SharedStore } from '../stores/SharedStore'; 27import type SharedStore from '../stores/SharedStore';
28import type { Config } from '../stores/SharedStore';
28import type Disposer from '../utils/Disposer'; 29import type Disposer from '../utils/Disposer';
29import { getLogger } from '../utils/log'; 30import { getLogger } from '../utils/log';
30 31
31const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); 32const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s');
32 33
33const log = getLogger('config'); 34const log = getLogger('synchronizeConfig');
34 35
35export default async function initConfig( 36export 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 @@
21import { nativeTheme } from 'electron'; 21import { nativeTheme } from 'electron';
22import { autorun } from 'mobx'; 22import { autorun } from 'mobx';
23 23
24import type { SharedStore } from '../stores/SharedStore'; 24import type SharedStore from '../stores/SharedStore';
25import type Disposer from '../utils/Disposer'; 25import type Disposer from '../utils/Disposer';
26import { getLogger } from '../utils/log'; 26import { getLogger } from '../utils/log';
27 27
28const log = getLogger('nativeTheme'); 28const log = getLogger('synchronizeNativeTheme');
29 29
30export default function initNativeTheme(store: SharedStore): Disposer { 30export 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
21import { 21import {
22 globalSettings as originalGlobalSettings, 22 GlobalSettings as GlobalSettingsBase,
23 ThemeSource, 23 ThemeSource,
24} from '@sophie/shared'; 24} from '@sophie/shared';
25import { Instance } from 'mobx-state-tree'; 25import { Instance } from 'mobx-state-tree';
26 26
27export const globalSettings = originalGlobalSettings.actions((self) => ({ 27const 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
33export 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*/
37interface GlobalSettings extends Instance<typeof GlobalSettings> {}
38
39export default GlobalSettings;
34 40
35export type { 41export 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 @@
21import type { BrowserViewBounds } from '@sophie/shared'; 21import type { BrowserViewBounds } from '@sophie/shared';
22import { applySnapshot, Instance, types } from 'mobx-state-tree'; 22import { applySnapshot, Instance, types } from 'mobx-state-tree';
23 23
24import { GlobalSettings } from './GlobalSettings'; 24import GlobalSettings from './GlobalSettings';
25import { Profile } from './Profile'; 25import Profile from './Profile';
26import { Service } from './Service'; 26import Service from './Service';
27import { sharedStore } from './SharedStore'; 27import SharedStore from './SharedStore';
28 28
29export const mainStore = types 29const 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
59export 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*/
63interface MainStore extends Instance<typeof MainStore> {}
64
65export default MainStore;
60 66
61export function createMainStore(): MainStore { 67export 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
21import { 21import {
22 profile as originalProfile, 22 Profile as ProfileBase,
23 ProfileSettingsSnapshotIn, 23 ProfileSettingsSnapshotIn,
24} from '@sophie/shared'; 24} from '@sophie/shared';
25import { getSnapshot, Instance } from 'mobx-state-tree'; 25import { 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
33export const profile = originalProfile.views((self) => ({ 33const 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
40export 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*/
44interface Profile extends Instance<typeof Profile> {}
45
46export default Profile;
41 47
42export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; 48export 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
21import type { UnreadCount } from '@sophie/service-shared'; 21import type { UnreadCount } from '@sophie/service-shared';
22import { service as originalService } from '@sophie/shared'; 22import { Service as ServiceBase } from '@sophie/shared';
23import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; 23import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree';
24 24
25import generateId from '../utils/generateId'; 25import generateId from '../utils/generateId';
26import overrideProps from '../utils/overrideProps'; 26import overrideProps from '../utils/overrideProps';
27 27
28import { ProfileSettingsSnapshotWithId } from './Profile'; 28import { ProfileSettingsSnapshotWithId } from './Profile';
29import { serviceSettings, ServiceSettingsSnapshotIn } from './ServiceSettings'; 29import ServiceSettings, { ServiceSettingsSnapshotIn } from './ServiceSettings';
30 30
31export interface ServiceConfig 31export 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
38export const service = overrideProps(originalService, { 38const 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
86export 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*/
90interface Service extends Instance<typeof Service> {}
91
92export default Service;
87 93
88export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; 94export 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
21import { serviceSettings as originalServiceSettings } from '@sophie/shared'; 21import { ServiceSettings as ServiceSettingsBase } from '@sophie/shared';
22import { Instance, types } from 'mobx-state-tree'; 22import { Instance, types } from 'mobx-state-tree';
23 23
24import overrideProps from '../utils/overrideProps'; 24import overrideProps from '../utils/overrideProps';
25 25
26import { profile } from './Profile'; 26import Profile from './Profile';
27 27
28export const serviceSettings = overrideProps(originalServiceSettings, { 28const ServiceSettings = overrideProps(ServiceSettingsBase, {
29 profile: types.reference(profile), 29 profile: types.reference(Profile),
30}); 30});
31 31
32export 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*/
36interface ServiceSettings extends Instance<typeof ServiceSettings> {}
37
38export default ServiceSettings;
33 39
34export type { 40export 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
21import { sharedStore as originalSharedStore } from '@sophie/shared'; 21import { SharedStore as SharedStoreBase } from '@sophie/shared';
22import { 22import {
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
35import { getLogger } from '../utils/log'; 34import { getLogger } from '../utils/log';
36import overrideProps from '../utils/overrideProps'; 35import overrideProps from '../utils/overrideProps';
37 36
38import { globalSettings, GlobalSettingsSnapshotIn } from './GlobalSettings'; 37import GlobalSettings, { GlobalSettingsSnapshotIn } from './GlobalSettings';
39import { addMissingProfileIds, profile, ProfileConfig } from './Profile'; 38import Profile, { addMissingProfileIds, ProfileConfig } from './Profile';
40import { 39import 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
89export const sharedStore = overrideProps(originalSharedStore, { 87const 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
155export 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*/
157interface SharedStore extends Instance<typeof SharedStore> {}
158
159export default SharedStore;
156 160
157export type { 161export 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
21import type { ProfileConfig } from '../Profile'; 21import type { ProfileConfig } from '../Profile';
22import type { ServiceConfig } from '../Service'; 22import type { ServiceConfig } from '../Service';
23import { Config, sharedStore, SharedStore } from '../SharedStore'; 23import SharedStore, { Config } from '../SharedStore';
24 24
25const profileProps: ProfileConfig = { 25const profileProps: ProfileConfig = {
26 name: 'Test profile', 26 name: 'Test profile',
@@ -34,7 +34,7 @@ const serviceProps: ServiceConfig = {
34let sut: SharedStore; 34let sut: SharedStore;
35 35
36beforeEach(() => { 36beforeEach(() => {
37 sut = sharedStore.create(); 37 sut = SharedStore.create();
38}); 38});
39 39
40describe('loadConfig', () => { 40describe('loadConfig', () => {
diff --git a/packages/preload/src/contextBridge/createSophieRenderer.ts b/packages/preload/src/contextBridge/createSophieRenderer.ts
index 6003c8b..8bdf07e 100644
--- a/packages/preload/src/contextBridge/createSophieRenderer.ts
+++ b/packages/preload/src/contextBridge/createSophieRenderer.ts
@@ -20,7 +20,6 @@
20 20
21import { 21import {
22 Action, 22 Action,
23 action,
24 MainToRendererIpcMessage, 23 MainToRendererIpcMessage,
25 RendererToMainIpcMessage, 24 RendererToMainIpcMessage,
26 SharedStoreListener, 25 SharedStoreListener,
@@ -32,30 +31,33 @@ import log from 'loglevel';
32import type { IJsonPatch } from 'mobx-state-tree'; 31import type { IJsonPatch } from 'mobx-state-tree';
33 32
34class SharedStoreConnector { 33class SharedStoreConnector {
35 private onSharedStoreChangeCalled = false; 34 readonly #allowReplaceListener: boolean;
36 35
37 private listener: SharedStoreListener | undefined; 36 #onSharedStoreChangeCalled = false;
38 37
39 constructor(private readonly allowReplaceListener: boolean) { 38 #listener: SharedStoreListener | undefined;
39
40 constructor(allowReplaceListener: boolean) {
41 this.#allowReplaceListener = allowReplaceListener;
40 ipcRenderer.on( 42 ipcRenderer.on(
41 MainToRendererIpcMessage.SharedStorePatch, 43 MainToRendererIpcMessage.SharedStorePatch,
42 (_event, patch) => { 44 (_event, patch) => {
43 try { 45 try {
44 // `mobx-state-tree` will validate the patch, so we can safely cast here. 46 // `mobx-state-tree` will validate the patch, so we can safely cast here.
45 this.listener?.onPatch(patch as IJsonPatch[]); 47 this.#listener?.onPatch(patch as IJsonPatch[]);
46 } catch (error) { 48 } catch (error) {
47 log.error('Shared store listener onPatch failed', error); 49 log.error('Shared store listener onPatch failed', error);
48 this.listener = undefined; 50 this.#listener = undefined;
49 } 51 }
50 }, 52 },
51 ); 53 );
52 } 54 }
53 55
54 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { 56 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> {
55 if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) { 57 if (this.#onSharedStoreChangeCalled && !this.#allowReplaceListener) {
56 throw new Error('Shared store change listener was already set'); 58 throw new Error('Shared store change listener was already set');
57 } 59 }
58 this.onSharedStoreChangeCalled = true; 60 this.#onSharedStoreChangeCalled = true;
59 let success = false; 61 let success = false;
60 let snapshot: unknown; 62 let snapshot: unknown;
61 try { 63 try {
@@ -71,14 +73,14 @@ class SharedStoreConnector {
71 } 73 }
72 // `mobx-state-tree` will validate the snapshot, so we can safely cast here. 74 // `mobx-state-tree` will validate the snapshot, so we can safely cast here.
73 listener.onSnapshot(snapshot as SharedStoreSnapshotIn); 75 listener.onSnapshot(snapshot as SharedStoreSnapshotIn);
74 this.listener = listener; 76 this.#listener = listener;
75 } 77 }
76} 78}
77 79
78function dispatchAction(actionToDispatch: Action): void { 80function dispatchAction(actionToDispatch: Action): void {
79 // Let the full zod parse error bubble up to the main world, 81 // Let the full zod parse error bubble up to the main world,
80 // since all data it may contain was provided from the main world. 82 // since all data it may contain was provided from the main world.
81 const parsedAction = action.parse(actionToDispatch); 83 const parsedAction = Action.parse(actionToDispatch);
82 try { 84 try {
83 ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction); 85 ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction);
84 } catch (error) { 86 } catch (error) {
diff --git a/packages/renderer/src/components/StoreProvider.tsx b/packages/renderer/src/components/StoreProvider.tsx
index 3360a43..de63083 100644
--- a/packages/renderer/src/components/StoreProvider.tsx
+++ b/packages/renderer/src/components/StoreProvider.tsx
@@ -20,9 +20,8 @@
20 20
21import React, { createContext, useContext } from 'react'; 21import React, { createContext, useContext } from 'react';
22 22
23import type { RendererStore } from '../stores/RendererStore'; 23import type RendererStore from '../stores/RendererStore';
24 24
25// eslint-disable-next-line unicorn/no-useless-undefined -- `createContext` expects 1 parameter.
26const StoreContext = createContext<RendererStore | undefined>(undefined); 25const StoreContext = createContext<RendererStore | undefined>(undefined);
27 26
28export function useStore(): RendererStore { 27export function useStore(): RendererStore {
diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts
index 4cbf6aa..c5a94df 100644
--- a/packages/renderer/src/stores/RendererStore.ts
+++ b/packages/renderer/src/stores/RendererStore.ts
@@ -20,18 +20,12 @@
20 20
21import { 21import {
22 BrowserViewBounds, 22 BrowserViewBounds,
23 sharedStore, 23 SharedStore,
24 Service, 24 Service,
25 SophieRenderer, 25 SophieRenderer,
26 ThemeSource, 26 ThemeSource,
27} from '@sophie/shared'; 27} from '@sophie/shared';
28import { 28import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree';
29 applySnapshot,
30 applyPatch,
31 Instance,
32 types,
33 IJsonPatch,
34} from 'mobx-state-tree';
35 29
36import RendererEnv from '../env/RendererEnv'; 30import RendererEnv from '../env/RendererEnv';
37import getEnv from '../env/getEnv'; 31import getEnv from '../env/getEnv';
@@ -39,9 +33,9 @@ import { getLogger } from '../utils/log';
39 33
40const log = getLogger('RendererStore'); 34const log = getLogger('RendererStore');
41 35
42export const rendererStore = types 36const RendererStore = types
43 .model('RendererStore', { 37 .model('RendererStore', {
44 shared: types.optional(sharedStore, {}), 38 shared: types.optional(SharedStore, {}),
45 }) 39 })
46 .views((self) => ({ 40 .views((self) => ({
47 get services(): Service[] { 41 get services(): Service[] {
@@ -79,7 +73,13 @@ export const rendererStore = types
79 }, 73 },
80 })); 74 }));
81 75
82export interface RendererStore extends Instance<typeof rendererStore> {} 76/*
77 eslint-disable-next-line @typescript-eslint/no-redeclare --
78 Intentionally naming the type the same as the store definition.
79*/
80interface RendererStore extends Instance<typeof RendererStore> {}
81
82export default RendererStore;
83 83
84/** 84/**
85 * Creates a new `RootStore` with a new environment and connects it to `ipc`. 85 * Creates a new `RootStore` with a new environment and connects it to `ipc`.
@@ -95,7 +95,7 @@ export function createAndConnectRendererStore(
95 const env: RendererEnv = { 95 const env: RendererEnv = {
96 dispatchMainAction: ipc.dispatchAction, 96 dispatchMainAction: ipc.dispatchAction,
97 }; 97 };
98 const store = rendererStore.create({}, env); 98 const store = RendererStore.create({}, env);
99 99
100 ipc 100 ipc
101 .onSharedStoreChange({ 101 .onSharedStoreChange({
diff --git a/packages/renderer/vite.config.js b/packages/renderer/vite.config.js
index cb0203c..63c4f77 100644
--- a/packages/renderer/vite.config.js
+++ b/packages/renderer/vite.config.js
@@ -48,6 +48,9 @@ export default {
48 optimizeDeps: { 48 optimizeDeps: {
49 exclude: ['@sophie/shared'], 49 exclude: ['@sophie/shared'],
50 }, 50 },
51 define: {
52 __DEV__: JSON.stringify(isDevelopment), // For mobx
53 },
51 build: { 54 build: {
52 target: chrome, 55 target: chrome,
53 assetsDir: '.', 56 assetsDir: '.',
diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts
index 8b6630a..5383f42 100644
--- a/packages/service-preload/src/index.ts
+++ b/packages/service-preload/src/index.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
21import { ServiceToMainIpcMessage, webSource } from '@sophie/service-shared'; 21import { ServiceToMainIpcMessage, WebSource } from '@sophie/service-shared';
22import { ipcRenderer, webFrame } from 'electron'; 22import { ipcRenderer, webFrame } from 'electron';
23 23
24if (webFrame.parent === null) { 24if (webFrame.parent === null) {
@@ -52,7 +52,7 @@ async function fetchAndExecuteInjectScript(): Promise<void> {
52 const apiExposedResponse: unknown = await ipcRenderer.invoke( 52 const apiExposedResponse: unknown = await ipcRenderer.invoke(
53 ServiceToMainIpcMessage.ApiExposedInMainWorld, 53 ServiceToMainIpcMessage.ApiExposedInMainWorld,
54 ); 54 );
55 const injectSource = webSource.parse(apiExposedResponse); 55 const injectSource = WebSource.parse(apiExposedResponse);
56 // Isolated world 0 is the main world. 56 // Isolated world 0 is the main world.
57 await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); 57 await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]);
58} 58}
diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts
index 94be734..a2e5ee5 100644
--- a/packages/service-shared/src/index.ts
+++ b/packages/service-shared/src/index.ts
@@ -20,5 +20,4 @@
20 20
21export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; 21export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc';
22 22
23export type { UnreadCount, WebSource } from './schemas'; 23export { UnreadCount, WebSource } from './schemas';
24export { unreadCount, webSource } from './schemas';
diff --git a/packages/service-shared/src/schemas.ts b/packages/service-shared/src/schemas.ts
index 586750c..0b31eb7 100644
--- a/packages/service-shared/src/schemas.ts
+++ b/packages/service-shared/src/schemas.ts
@@ -20,16 +20,24 @@
20 20
21import { z } from 'zod'; 21import { z } from 'zod';
22 22
23export const unreadCount = z.object({ 23export const UnreadCount = z.object({
24 direct: z.number().nonnegative().optional(), 24 direct: z.number().nonnegative().optional(),
25 indirect: z.number().nonnegative().optional(), 25 indirect: z.number().nonnegative().optional(),
26}); 26});
27 27
28export type UnreadCount = z.infer<typeof unreadCount>; 28/*
29 eslint-disable-next-line @typescript-eslint/no-redeclare --
30 Intentionally naming the type the same as the schema definition.
31*/
32export type UnreadCount = z.infer<typeof UnreadCount>;
29 33
30export const webSource = z.object({ 34export const WebSource = z.object({
31 code: z.string(), 35 code: z.string(),
32 url: z.string().nonempty(), 36 url: z.string().nonempty(),
33}); 37});
34 38
35export type WebSource = z.infer<typeof webSource>; 39/*
40 eslint-disable-next-line @typescript-eslint/no-redeclare --
41 Intentionally naming the type the same as the schema definition.
42*/
43export type WebSource = z.infer<typeof WebSource>;
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 55cf5ce..3d30488 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -22,40 +22,33 @@ export type { default as SophieRenderer } from './contextBridge/SophieRenderer';
22 22
23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; 23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc';
24 24
25export type { Action, BrowserViewBounds, ThemeSource } from './schemas'; 25export { Action, BrowserViewBounds, ThemeSource } from './schemas';
26export { action, browserViewBounds, themeSource } from './schemas';
27 26
28export type { 27export type {
29 GlobalSettings,
30 GlobalSettingsSnapshotIn, 28 GlobalSettingsSnapshotIn,
31 GlobalSettingsSnapshotOut, 29 GlobalSettingsSnapshotOut,
32} from './stores/GlobalSettings'; 30} from './stores/GlobalSettings';
33export { globalSettings } from './stores/GlobalSettings'; 31export { default as GlobalSettings } from './stores/GlobalSettings';
34 32
35export type { Profile } from './stores/Profile'; 33export { default as Profile } from './stores/Profile';
36export { profile } from './stores/Profile';
37 34
38export type { 35export type {
39 ProfileSettings,
40 ProfileSettingsSnapshotIn, 36 ProfileSettingsSnapshotIn,
41 ProfileSettingsSnapshotOut, 37 ProfileSettingsSnapshotOut,
42} from './stores/ProfileSettings'; 38} from './stores/ProfileSettings';
43export { profileSettings } from './stores/ProfileSettings'; 39export { default as ProfileSettings } from './stores/ProfileSettings';
44 40
45export type { Service } from './stores/Service'; 41export { default as Service } from './stores/Service';
46export { service } from './stores/Service';
47 42
48export type { 43export type {
49 ServiceSettings,
50 ServiceSettingsSnapshotIn, 44 ServiceSettingsSnapshotIn,
51 ServiceSettingsSnapshotOut, 45 ServiceSettingsSnapshotOut,
52} from './stores/ServiceSettings'; 46} from './stores/ServiceSettings';
53export { serviceSettings } from './stores/ServiceSettings'; 47export { default as ServiceSettings } from './stores/ServiceSettings';
54 48
55export type { 49export type {
56 SharedStore,
57 SharedStoreListener, 50 SharedStoreListener,
58 SharedStoreSnapshotIn, 51 SharedStoreSnapshotIn,
59 SharedStoreSnapshotOut, 52 SharedStoreSnapshotOut,
60} from './stores/SharedStore'; 53} from './stores/SharedStore';
61export { sharedStore } from './stores/SharedStore'; 54export { default as SharedStore } from './stores/SharedStore';
diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts
index 7fb9717..edf3741 100644
--- a/packages/shared/src/schemas.ts
+++ b/packages/shared/src/schemas.ts
@@ -20,43 +20,55 @@
20 20
21import { z } from 'zod'; 21import { z } from 'zod';
22 22
23const setSelectedServiceId = z.object({ 23const SetSelectedServiceId = z.object({
24 action: z.literal('set-selected-service-id'), 24 action: z.literal('set-selected-service-id'),
25 serviceId: z.string(), 25 serviceId: z.string(),
26}); 26});
27 27
28export const browserViewBounds = z.object({ 28export const BrowserViewBounds = z.object({
29 x: z.number().int().nonnegative(), 29 x: z.number().int().nonnegative(),
30 y: z.number().int().nonnegative(), 30 y: z.number().int().nonnegative(),
31 width: z.number().int().nonnegative(), 31 width: z.number().int().nonnegative(),
32 height: z.number().int().nonnegative(), 32 height: z.number().int().nonnegative(),
33}); 33});
34 34
35export type BrowserViewBounds = z.infer<typeof browserViewBounds>; 35/*
36 eslint-disable-next-line @typescript-eslint/no-redeclare --
37 Intentionally naming the type the same as the schema definition.
38*/
39export type BrowserViewBounds = z.infer<typeof BrowserViewBounds>;
36 40
37const setBrowserViewBoundsAction = z.object({ 41const SetBrowserViewBoundsAction = z.object({
38 action: z.literal('set-browser-view-bounds'), 42 action: z.literal('set-browser-view-bounds'),
39 browserViewBounds, 43 browserViewBounds: BrowserViewBounds,
40}); 44});
41 45
42export const themeSource = z.enum(['system', 'light', 'dark']); 46export const ThemeSource = z.enum(['system', 'light', 'dark']);
43 47
44export type ThemeSource = z.infer<typeof themeSource>; 48/*
49 eslint-disable-next-line @typescript-eslint/no-redeclare --
50 Intentionally naming the type the same as the schema definition.
51*/
52export type ThemeSource = z.infer<typeof ThemeSource>;
45 53
46const setThemeSourceAction = z.object({ 54const SetThemeSourceAction = z.object({
47 action: z.literal('set-theme-source'), 55 action: z.literal('set-theme-source'),
48 themeSource, 56 themeSource: ThemeSource,
49}); 57});
50 58
51const reloadAllServicesAction = z.object({ 59const ReloadAllServicesAction = z.object({
52 action: z.literal('reload-all-services'), 60 action: z.literal('reload-all-services'),
53}); 61});
54 62
55export const action = z.union([ 63export const Action = z.union([
56 setSelectedServiceId, 64 SetSelectedServiceId,
57 setBrowserViewBoundsAction, 65 SetBrowserViewBoundsAction,
58 setThemeSourceAction, 66 SetThemeSourceAction,
59 reloadAllServicesAction, 67 ReloadAllServicesAction,
60]); 68]);
61 69
62export type Action = z.infer<typeof action>; 70/*
71 eslint-disable-next-line @typescript-eslint/no-redeclare --
72 Intentionally naming the type the same as the schema definition.
73*/
74export type Action = z.infer<typeof Action>;
diff --git a/packages/shared/src/stores/GlobalSettings.ts b/packages/shared/src/stores/GlobalSettings.ts
index bd0155a..3a813b8 100644
--- a/packages/shared/src/stores/GlobalSettings.ts
+++ b/packages/shared/src/stores/GlobalSettings.ts
@@ -20,16 +20,22 @@
20 20
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; 21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22 22
23import { themeSource } from '../schemas'; 23import { ThemeSource } from '../schemas';
24 24
25export const globalSettings = types.model('GlobalSettings', { 25const GlobalSettings = types.model('GlobalSettings', {
26 themeSource: types.optional(types.enumeration(themeSource.options), 'system'), 26 themeSource: types.optional(types.enumeration(ThemeSource.options), 'system'),
27}); 27});
28 28
29export interface GlobalSettings extends Instance<typeof globalSettings> {} 29/*
30 eslint-disable-next-line @typescript-eslint/no-redeclare --
31 Intentionally naming the type the same as the store definition.
32*/
33interface GlobalSettings extends Instance<typeof GlobalSettings> {}
34
35export default GlobalSettings;
30 36
31export interface GlobalSettingsSnapshotIn 37export interface GlobalSettingsSnapshotIn
32 extends SnapshotIn<typeof globalSettings> {} 38 extends SnapshotIn<typeof GlobalSettings> {}
33 39
34export interface GlobalSettingsSnapshotOut 40export interface GlobalSettingsSnapshotOut
35 extends SnapshotOut<typeof globalSettings> {} 41 extends SnapshotOut<typeof GlobalSettings> {}
diff --git a/packages/shared/src/stores/Profile.ts b/packages/shared/src/stores/Profile.ts
index bb058f6..256c33e 100644
--- a/packages/shared/src/stores/Profile.ts
+++ b/packages/shared/src/stores/Profile.ts
@@ -20,11 +20,17 @@
20 20
21import { Instance, types } from 'mobx-state-tree'; 21import { Instance, types } from 'mobx-state-tree';
22 22
23import { profileSettings } from './ProfileSettings'; 23import ProfileSettings from './ProfileSettings';
24 24
25export const profile = types.model('Profile', { 25const Profile = types.model('Profile', {
26 id: types.identifier, 26 id: types.identifier,
27 settings: profileSettings, 27 settings: ProfileSettings,
28}); 28});
29 29
30export interface Profile extends Instance<typeof profile> {} 30/*
31 eslint-disable-next-line @typescript-eslint/no-redeclare --
32 Intentionally naming the type the same as the store definition.
33*/
34interface Profile extends Instance<typeof Profile> {}
35
36export default Profile;
diff --git a/packages/shared/src/stores/ProfileSettings.ts b/packages/shared/src/stores/ProfileSettings.ts
index ec8da5f..9f2b27c 100644
--- a/packages/shared/src/stores/ProfileSettings.ts
+++ b/packages/shared/src/stores/ProfileSettings.ts
@@ -20,14 +20,20 @@
20 20
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; 21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22 22
23export const profileSettings = types.model('ProfileSettings', { 23const ProfileSettings = types.model('ProfileSettings', {
24 name: types.string, 24 name: types.string,
25}); 25});
26 26
27export interface ProfileSettings extends Instance<typeof profileSettings> {} 27/*
28 eslint-disable-next-line @typescript-eslint/no-redeclare --
29 Intentionally naming the type the same as the store definition.
30*/
31interface ProfileSettings extends Instance<typeof ProfileSettings> {}
32
33export default ProfileSettings;
28 34
29export interface ProfileSettingsSnapshotIn 35export interface ProfileSettingsSnapshotIn
30 extends SnapshotIn<typeof profileSettings> {} 36 extends SnapshotIn<typeof ProfileSettings> {}
31 37
32export interface ProfileSettingsSnapshotOut 38export interface ProfileSettingsSnapshotOut
33 extends SnapshotOut<typeof profileSettings> {} 39 extends SnapshotOut<typeof ProfileSettings> {}
diff --git a/packages/shared/src/stores/Service.ts b/packages/shared/src/stores/Service.ts
index 36acd3d..4a7334d 100644
--- a/packages/shared/src/stores/Service.ts
+++ b/packages/shared/src/stores/Service.ts
@@ -20,11 +20,11 @@
20 20
21import { Instance, types } from 'mobx-state-tree'; 21import { Instance, types } from 'mobx-state-tree';
22 22
23import { serviceSettings } from './ServiceSettings'; 23import ServiceSettings from './ServiceSettings';
24 24
25export const service = types.model('Service', { 25const Service = types.model('Service', {
26 id: types.identifier, 26 id: types.identifier,
27 settings: serviceSettings, 27 settings: ServiceSettings,
28 currentUrl: types.maybe(types.string), 28 currentUrl: types.maybe(types.string),
29 canGoBack: false, 29 canGoBack: false,
30 canGoForward: false, 30 canGoForward: false,
@@ -37,4 +37,10 @@ export const service = types.model('Service', {
37 indirectMessageCount: 0, 37 indirectMessageCount: 0,
38}); 38});
39 39
40export interface Service extends Instance<typeof service> {} 40/*
41 eslint-disable-next-line @typescript-eslint/no-redeclare --
42 Intentionally naming the type the same as the store definition.
43*/
44interface Service extends Instance<typeof Service> {}
45
46export default Service;
diff --git a/packages/shared/src/stores/ServiceSettings.ts b/packages/shared/src/stores/ServiceSettings.ts
index 54cd7eb..6ba1dfa 100644
--- a/packages/shared/src/stores/ServiceSettings.ts
+++ b/packages/shared/src/stores/ServiceSettings.ts
@@ -20,19 +20,25 @@
20 20
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; 21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22 22
23import { profile } from './Profile'; 23import Profile from './Profile';
24 24
25export const serviceSettings = types.model('ServiceSettings', { 25const ServiceSettings = types.model('ServiceSettings', {
26 name: types.string, 26 name: types.string,
27 profile: types.reference(profile), 27 profile: types.reference(Profile),
28 // TODO: Remove this once recipes are added. 28 // TODO: Remove this once recipes are added.
29 url: types.string, 29 url: types.string,
30}); 30});
31 31
32export 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*/
36interface ServiceSettings extends Instance<typeof ServiceSettings> {}
37
38export default ServiceSettings;
33 39
34export interface ServiceSettingsSnapshotIn 40export interface ServiceSettingsSnapshotIn
35 extends SnapshotIn<typeof serviceSettings> {} 41 extends SnapshotIn<typeof ServiceSettings> {}
36 42
37export interface ServiceSettingsSnapshotOut 43export interface ServiceSettingsSnapshotOut
38 extends SnapshotOut<typeof serviceSettings> {} 44 extends SnapshotOut<typeof ServiceSettings> {}
diff --git a/packages/shared/src/stores/SharedStore.ts b/packages/shared/src/stores/SharedStore.ts
index f301b9d..0cac3a5 100644
--- a/packages/shared/src/stores/SharedStore.ts
+++ b/packages/shared/src/stores/SharedStore.ts
@@ -26,26 +26,32 @@ import {
26 SnapshotOut, 26 SnapshotOut,
27} from 'mobx-state-tree'; 27} from 'mobx-state-tree';
28 28
29import { globalSettings } from './GlobalSettings'; 29import GlobalSettings from './GlobalSettings';
30import { profile } from './Profile'; 30import Profile from './Profile';
31import { service } from './Service'; 31import Service from './Service';
32 32
33export const sharedStore = types.model('SharedStore', { 33const SharedStore = types.model('SharedStore', {
34 settings: types.optional(globalSettings, {}), 34 settings: types.optional(GlobalSettings, {}),
35 profilesById: types.map(profile), 35 profilesById: types.map(Profile),
36 profiles: types.array(types.reference(profile)), 36 profiles: types.array(types.reference(Profile)),
37 servicesById: types.map(service), 37 servicesById: types.map(Service),
38 services: types.array(types.reference(service)), 38 services: types.array(types.reference(Service)),
39 selectedService: types.safeReference(service), 39 selectedService: types.safeReference(Service),
40 shouldUseDarkColors: false, 40 shouldUseDarkColors: false,
41}); 41});
42 42
43export interface SharedStore extends Instance<typeof sharedStore> {} 43/*
44 eslint-disable-next-line @typescript-eslint/no-redeclare --
45 Intentionally naming the type the same as the store definition.
46*/
47interface SharedStore extends Instance<typeof SharedStore> {}
48
49export default SharedStore;
44 50
45export interface SharedStoreSnapshotIn extends SnapshotIn<typeof sharedStore> {} 51export interface SharedStoreSnapshotIn extends SnapshotIn<typeof SharedStore> {}
46 52
47export interface SharedStoreSnapshotOut 53export interface SharedStoreSnapshotOut
48 extends SnapshotOut<typeof sharedStore> {} 54 extends SnapshotOut<typeof SharedStore> {}
49 55
50export interface SharedStoreListener { 56export interface SharedStoreListener {
51 onSnapshot(snapshot: SharedStoreSnapshotIn): void; 57 onSnapshot(snapshot: SharedStoreSnapshotIn): void;