aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-01-25 17:26:32 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-02-08 21:43:17 +0100
commit038b35cc43d38d53c3a7f2c90479bb16a18084a6 (patch)
tree72f205b4d79de71b2a60f44c1461d5a1f2237f70
parentrefactor: Move runtime state into shared models (diff)
downloadsophie-038b35cc43d38d53c3a7f2c90479bb16a18084a6.tar.gz
sophie-038b35cc43d38d53c3a7f2c90479bb16a18084a6.tar.zst
sophie-038b35cc43d38d53c3a7f2c90479bb16a18084a6.zip
refactor: Store services in a map
Makes the synchronization of references across the main/renderer process boundary more robust. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--packages/main/src/stores/Profile.ts10
-rw-r--r--packages/main/src/stores/Service.ts21
-rw-r--r--packages/main/src/stores/SharedStore.ts130
-rw-r--r--packages/main/src/utils/SettingsWithId.ts25
-rw-r--r--packages/shared/src/stores/SharedStore.ts6
5 files changed, 111 insertions, 81 deletions
diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts
index eaf23c4..5f77fe4 100644
--- a/packages/main/src/stores/Profile.ts
+++ b/packages/main/src/stores/Profile.ts
@@ -24,7 +24,6 @@ import {
24} from '@sophie/shared'; 24} from '@sophie/shared';
25import { getSnapshot, Instance } from 'mobx-state-tree'; 25import { getSnapshot, Instance } from 'mobx-state-tree';
26 26
27import SettingsWithId from '../utils/SettingsWithId';
28import generateId from '../utils/generateId'; 27import generateId from '../utils/generateId';
29 28
30export interface ProfileConfig extends ProfileSettingsSnapshotIn { 29export interface ProfileConfig extends ProfileSettingsSnapshotIn {
@@ -40,17 +39,16 @@ export const profile = originalProfile.views((self) => ({
40 39
41export interface Profile extends Instance<typeof profile> {} 40export interface Profile extends Instance<typeof profile> {}
42 41
43export type ProfileSettingsSnapshotWithId = 42export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn];
44 SettingsWithId<ProfileSettingsSnapshotIn>;
45 43
46export function addMissingProfileIds( 44export function addMissingProfileIds(
47 profileConfigs: ProfileConfig[] | undefined, 45 profileConfigs: ProfileConfig[] | undefined,
48): ProfileSettingsSnapshotWithId[] { 46): ProfileSettingsSnapshotWithId[] {
49 return (profileConfigs ?? []).map((profileConfig) => { 47 return (profileConfigs ?? []).map((profileConfig) => {
50 const { id, ...settings } = profileConfig; 48 const { id, ...settings } = profileConfig;
51 return { 49 return [
52 id: typeof id === 'undefined' ? generateId(settings.name) : id, 50 typeof id === 'undefined' ? generateId(settings.name) : id,
53 settings, 51 settings,
54 }; 52 ];
55 }); 53 });
56} 54}
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts
index 78c57cb..331805b 100644
--- a/packages/main/src/stores/Service.ts
+++ b/packages/main/src/stores/Service.ts
@@ -19,18 +19,14 @@
19 */ 19 */
20 20
21import type { UnreadCount } from '@sophie/service-shared'; 21import type { UnreadCount } from '@sophie/service-shared';
22import { 22import { service as originalService } from '@sophie/shared';
23 service as originalService,
24 ServiceSettingsSnapshotIn,
25} from '@sophie/shared';
26import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; 23import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree';
27 24
28import SettingsWithId from '../utils/SettingsWithId';
29import generateId from '../utils/generateId'; 25import generateId from '../utils/generateId';
30import overrideProps from '../utils/overrideProps'; 26import overrideProps from '../utils/overrideProps';
31 27
32import { ProfileSettingsSnapshotWithId } from './Profile'; 28import { ProfileSettingsSnapshotWithId } from './Profile';
33import { serviceSettings } from './ServiceSettings'; 29import { serviceSettings, ServiceSettingsSnapshotIn } from './ServiceSettings';
34 30
35export interface ServiceConfig 31export interface ServiceConfig
36 extends Omit<ServiceSettingsSnapshotIn, 'profile'> { 32 extends Omit<ServiceSettingsSnapshotIn, 'profile'> {
@@ -89,8 +85,7 @@ export const service = overrideProps(originalService, {
89 85
90export interface Service extends Instance<typeof service> {} 86export interface Service extends Instance<typeof service> {}
91 87
92export type ServiceSettingsSnapshotWithId = 88export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn];
93 SettingsWithId<ServiceSettingsSnapshotIn>;
94 89
95export function addMissingServiceIdsAndProfiles( 90export function addMissingServiceIdsAndProfiles(
96 serviceConfigs: ServiceConfig[] | undefined, 91 serviceConfigs: ServiceConfig[] | undefined,
@@ -102,11 +97,11 @@ export function addMissingServiceIdsAndProfiles(
102 let { profile: profileId } = settings; 97 let { profile: profileId } = settings;
103 if (profileId === undefined) { 98 if (profileId === undefined) {
104 profileId = generateId(name); 99 profileId = generateId(name);
105 profiles.push({ id: profileId, settings: { name } }); 100 profiles.push([profileId, { name }]);
106 } 101 }
107 return { 102 return [
108 id: id === undefined ? generateId(name) : id, 103 id === undefined ? generateId(name) : id,
109 settings: { ...settings, profile: profileId }, 104 { ...settings, profile: profileId },
110 }; 105 ];
111 }); 106 });
112} 107}
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts
index 861c8ce..9ec7963 100644
--- a/packages/main/src/stores/SharedStore.ts
+++ b/packages/main/src/stores/SharedStore.ts
@@ -22,12 +22,16 @@ import { sharedStore as originalSharedStore } from '@sophie/shared';
22import { 22import {
23 applySnapshot, 23 applySnapshot,
24 getSnapshot, 24 getSnapshot,
25 IMSTArray,
26 IMSTMap,
25 Instance, 27 Instance,
28 IReferenceType,
29 IStateTreeNode,
30 IType,
26 resolveIdentifier, 31 resolveIdentifier,
27 types, 32 types,
28} from 'mobx-state-tree'; 33} from 'mobx-state-tree';
29 34
30import SettingsWithId from '../utils/SettingsWithId';
31import { getLogger } from '../utils/log'; 35import { getLogger } from '../utils/log';
32import overrideProps from '../utils/overrideProps'; 36import overrideProps from '../utils/overrideProps';
33 37
@@ -51,27 +55,51 @@ function getConfigs<T>(models: { config: T }[]): T[] | undefined {
51 return models.length === 0 ? undefined : models.map((model) => model.config); 55 return models.length === 0 ? undefined : models.map((model) => model.config);
52} 56}
53 57
54function reconcileSettings<T>( 58type TypeWithSettings<C> = IType<
55 originalSnapshots: SettingsWithId<T>[], 59 { id: string; settings: C },
56 settingsSnapshotsWithId: SettingsWithId<T>[], 60 unknown,
57): SettingsWithId<T>[] { 61 { settings: IStateTreeNode<IType<C, unknown, unknown>> }
58 const idToOriginalSnapshots = new Map( 62>;
59 originalSnapshots.map((originalSnapshot) => [ 63
60 originalSnapshot.id, 64function deleteStaleModels<C, D extends TypeWithSettings<C>>(
61 originalSnapshot, 65 currentById: IMSTMap<D>,
62 ]), 66 toApplyById: Map<string, C>,
63 ); 67): void {
64 return settingsSnapshotsWithId.map(({ id, settings }) => ({ 68 const toDelete = new Set(currentById.keys());
65 ...idToOriginalSnapshots.get(id), 69 toApplyById.forEach((_settings, id) => toDelete.delete(id));
66 id, 70 toDelete.forEach((id) => currentById.delete(id));
67 settings, 71}
68 })); 72
73function applySettings<C, D extends TypeWithSettings<C>>(
74 currentById: IMSTMap<D>,
75 toApplyById: Map<string, C>,
76): void {
77 toApplyById.forEach((settingsSnapshot, id) => {
78 const model = currentById.get(id);
79 if (model === undefined) {
80 currentById.set(id, {
81 id,
82 settings: settingsSnapshot,
83 });
84 } else {
85 applySnapshot(model.settings, settingsSnapshot);
86 }
87 });
88}
89
90function pushReferences<C, D extends TypeWithSettings<C>>(
91 list: IMSTArray<IReferenceType<D>>,
92 toApply: [string, C][],
93): void {
94 list.push(...toApply.map(([id]) => id));
69} 95}
70 96
71export const sharedStore = overrideProps(originalSharedStore, { 97export const sharedStore = overrideProps(originalSharedStore, {
72 settings: types.optional(globalSettings, {}), 98 settings: types.optional(globalSettings, {}),
73 profiles: types.array(profile), 99 profilesById: types.map(profile),
74 services: types.array(service), 100 profiles: types.array(types.reference(profile)),
101 servicesById: types.map(service),
102 services: types.array(types.reference(service)),
75 selectedService: types.safeReference(service), 103 selectedService: types.safeReference(service),
76}) 104})
77 .views((self) => ({ 105 .views((self) => ({
@@ -87,25 +115,57 @@ export const sharedStore = overrideProps(originalSharedStore, {
87 })) 115 }))
88 .actions((self) => ({ 116 .actions((self) => ({
89 loadConfig(config: Config): void { 117 loadConfig(config: Config): void {
90 const snapshot = getSnapshot(self); 118 // `onPatch` will send store changes piecemeal without any attention to
91 const { profiles, services, ...settings } = config; 119 // transaction boundaries. We must make sure that any state communicated to the
92 const profileSettingsSnapshots = addMissingProfileIds(profiles); 120 // renderer process is actually valid.
93 const serviceSettingsSnapshots = addMissingServiceIdsAndProfiles( 121 const {
122 profiles,
123 profilesById,
124 selectedService,
94 services, 125 services,
95 profileSettingsSnapshots, 126 servicesById,
96 );
97 applySnapshot(self, {
98 ...snapshot,
99 settings, 127 settings,
100 profiles: reconcileSettings( 128 } = self;
101 snapshot.profiles, 129 const { id: selectedServiceId } = selectedService ?? { id: undefined };
102 profileSettingsSnapshots, 130 const {
103 ), 131 profiles: profilesConfig,
104 services: reconcileSettings( 132 services: servicesConfig,
105 snapshot.services, 133 ...settingsToApply
106 serviceSettingsSnapshots, 134 } = config;
107 ), 135 const profilesToApply = addMissingProfileIds(profilesConfig);
108 }); 136 const servicesToApply = addMissingServiceIdsAndProfiles(
137 servicesConfig,
138 profilesToApply,
139 );
140 const profilesToApplyById = new Map(profilesToApply);
141 const servicesToApplyById = new Map(servicesToApply);
142 // First remove any references to profiles and services that might be deleted.
143 self.selectedService = undefined;
144 services.clear();
145 profiles.clear();
146 // Delete all services that may depend on profiles that will be delete.
147 deleteStaleModels(servicesById, servicesToApplyById);
148 // Update existing profiles and add new profiles.
149 applySettings(profilesById, profilesToApplyById);
150 // Update existing services and add new services. This will make sure that no service
151 // depends on a profile that will be deleted.
152 applySettings(servicesById, servicesToApplyById);
153 // Now it's safe to delete stale profiles.
154 deleteStaleModels(profilesById, profilesToApplyById);
155 // We are ready to build new profile and service lists from the new models.
156 pushReferences(profiles, profilesToApply);
157 pushReferences(services, servicesToApply);
158 // Global settings may refer to particular profiles or services.
159 applySnapshot(settings, settingsToApply);
160 // Restore service selection (if applicable).
161 let newSelectedService;
162 if (selectedServiceId !== undefined) {
163 newSelectedService = servicesById.get(selectedServiceId);
164 }
165 if (newSelectedService === undefined && services.length > 0) {
166 [newSelectedService] = services;
167 }
168 self.selectedService = newSelectedService;
109 }, 169 },
110 setShouldUseDarkColors(shouldUseDarkColors: boolean): void { 170 setShouldUseDarkColors(shouldUseDarkColors: boolean): void {
111 self.shouldUseDarkColors = shouldUseDarkColors; 171 self.shouldUseDarkColors = shouldUseDarkColors;
diff --git a/packages/main/src/utils/SettingsWithId.ts b/packages/main/src/utils/SettingsWithId.ts
deleted file mode 100644
index fde3e86..0000000
--- a/packages/main/src/utils/SettingsWithId.ts
+++ /dev/null
@@ -1,25 +0,0 @@
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
21export default interface SettingsWithId<T> {
22 id: string;
23
24 settings: T;
25}
diff --git a/packages/shared/src/stores/SharedStore.ts b/packages/shared/src/stores/SharedStore.ts
index a04f4bf..fc8372e 100644
--- a/packages/shared/src/stores/SharedStore.ts
+++ b/packages/shared/src/stores/SharedStore.ts
@@ -32,8 +32,10 @@ import { service } from './Service';
32 32
33export const sharedStore = types.model('SharedStore', { 33export const sharedStore = types.model('SharedStore', {
34 settings: types.optional(globalSettings, {}), 34 settings: types.optional(globalSettings, {}),
35 profiles: types.array(profile), 35 profilesById: types.map(profile),
36 services: types.array(service), 36 profiles: types.array(types.reference(profile)),
37 servicesById: types.map(service),
38 services: types.array(types.reference(service)),
37 selectedService: types.safeReference(service), 39 selectedService: types.safeReference(service),
38 shouldUseDarkColors: false, 40 shouldUseDarkColors: false,
39}); 41});