diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-01-25 17:26:32 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-02-08 21:43:17 +0100 |
commit | 038b35cc43d38d53c3a7f2c90479bb16a18084a6 (patch) | |
tree | 72f205b4d79de71b2a60f44c1461d5a1f2237f70 | |
parent | refactor: Move runtime state into shared models (diff) | |
download | sophie-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.ts | 10 | ||||
-rw-r--r-- | packages/main/src/stores/Service.ts | 21 | ||||
-rw-r--r-- | packages/main/src/stores/SharedStore.ts | 130 | ||||
-rw-r--r-- | packages/main/src/utils/SettingsWithId.ts | 25 | ||||
-rw-r--r-- | packages/shared/src/stores/SharedStore.ts | 6 |
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'; |
25 | import { getSnapshot, Instance } from 'mobx-state-tree'; | 25 | import { getSnapshot, Instance } from 'mobx-state-tree'; |
26 | 26 | ||
27 | import SettingsWithId from '../utils/SettingsWithId'; | ||
28 | import generateId from '../utils/generateId'; | 27 | import generateId from '../utils/generateId'; |
29 | 28 | ||
30 | export interface ProfileConfig extends ProfileSettingsSnapshotIn { | 29 | export interface ProfileConfig extends ProfileSettingsSnapshotIn { |
@@ -40,17 +39,16 @@ export const profile = originalProfile.views((self) => ({ | |||
40 | 39 | ||
41 | export interface Profile extends Instance<typeof profile> {} | 40 | export interface Profile extends Instance<typeof profile> {} |
42 | 41 | ||
43 | export type ProfileSettingsSnapshotWithId = | 42 | export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; |
44 | SettingsWithId<ProfileSettingsSnapshotIn>; | ||
45 | 43 | ||
46 | export function addMissingProfileIds( | 44 | export 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 | ||
21 | import type { UnreadCount } from '@sophie/service-shared'; | 21 | import type { UnreadCount } from '@sophie/service-shared'; |
22 | import { | 22 | import { service as originalService } from '@sophie/shared'; |
23 | service as originalService, | ||
24 | ServiceSettingsSnapshotIn, | ||
25 | } from '@sophie/shared'; | ||
26 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; | 23 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; |
27 | 24 | ||
28 | import SettingsWithId from '../utils/SettingsWithId'; | ||
29 | import generateId from '../utils/generateId'; | 25 | import generateId from '../utils/generateId'; |
30 | import overrideProps from '../utils/overrideProps'; | 26 | import overrideProps from '../utils/overrideProps'; |
31 | 27 | ||
32 | import { ProfileSettingsSnapshotWithId } from './Profile'; | 28 | import { ProfileSettingsSnapshotWithId } from './Profile'; |
33 | import { serviceSettings } from './ServiceSettings'; | 29 | import { serviceSettings, ServiceSettingsSnapshotIn } from './ServiceSettings'; |
34 | 30 | ||
35 | export interface ServiceConfig | 31 | export interface ServiceConfig |
36 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { | 32 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { |
@@ -89,8 +85,7 @@ export const service = overrideProps(originalService, { | |||
89 | 85 | ||
90 | export interface Service extends Instance<typeof service> {} | 86 | export interface Service extends Instance<typeof service> {} |
91 | 87 | ||
92 | export type ServiceSettingsSnapshotWithId = | 88 | export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; |
93 | SettingsWithId<ServiceSettingsSnapshotIn>; | ||
94 | 89 | ||
95 | export function addMissingServiceIdsAndProfiles( | 90 | export 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'; | |||
22 | import { | 22 | import { |
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 | ||
30 | import SettingsWithId from '../utils/SettingsWithId'; | ||
31 | import { getLogger } from '../utils/log'; | 35 | import { getLogger } from '../utils/log'; |
32 | import overrideProps from '../utils/overrideProps'; | 36 | import 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 | ||
54 | function reconcileSettings<T>( | 58 | type 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, | 64 | function 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 | |
73 | function 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 | |||
90 | function 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 | ||
71 | export const sharedStore = overrideProps(originalSharedStore, { | 97 | export 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 | |||
21 | export 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 | ||
33 | export const sharedStore = types.model('SharedStore', { | 33 | export 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 | }); |