@@ -34,14 +34,16 @@ export type LocationViewPreferences = {
3434 } ;
3535} ;
3636export type PreferenceSyncConfig = Partial < Record < keyof LocationViewPreferences , boolean > > ;
37+ type PreferenceChange = true | Record < string , PreferenceChange > ;
38+ type PreferenceChanges = Partial < Record < keyof LocationViewPreferences , PreferenceChange > > ;
3739
3840const DEFAULT_PREFERENCES : LocationViewPreferences = {
3941 showDetails : true ,
4042 showEmpty : true ,
4143 editorAdvancedView : false ,
4244 itemDisplayView : "card" ,
4345 theme : "homebox" ,
44- itemsPerTablePage : 10 ,
46+ itemsPerTablePage : 12 ,
4547 displayLegacyHeader : false ,
4648 legacyImageFit : false ,
4749 language : null ,
@@ -84,41 +86,131 @@ function buildSyncedSettings(preferences: LocationViewPreferences): Record<strin
8486 return payload ;
8587}
8688
89+ function isPlainObject ( value : unknown ) : value is Record < string , unknown > {
90+ return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
91+ }
92+
93+ function isPrototypeKey ( key : string ) : boolean {
94+ return key === "__proto__" || key === "constructor" || key === "prototype" ;
95+ }
96+
97+ function mergeSyncedValue ( serverValue : unknown , localValue : unknown , localChange ?: PreferenceChange ) : unknown {
98+ if ( localChange === undefined ) {
99+ return serverValue ;
100+ }
101+
102+ if ( localChange === true || ! isPlainObject ( serverValue ) || ! isPlainObject ( localValue ) ) {
103+ return localValue ;
104+ }
105+
106+ const mergedValue : Record < string , unknown > = { } ;
107+ const keys = new Set ( [ ...Object . keys ( serverValue ) , ...Object . keys ( localValue ) ] ) ;
108+
109+ for ( const key of keys ) {
110+ if ( isPrototypeKey ( key ) ) {
111+ continue ;
112+ }
113+
114+ const nestedChange = localChange [ key ] ;
115+ if ( nestedChange !== undefined ) {
116+ mergedValue [ key ] = mergeSyncedValue ( serverValue [ key ] , localValue [ key ] , nestedChange ) ;
117+ continue ;
118+ }
119+
120+ if ( Object . hasOwn ( serverValue , key ) ) {
121+ mergedValue [ key ] = serverValue [ key ] ;
122+ } else {
123+ mergedValue [ key ] = localValue [ key ] ;
124+ }
125+ }
126+
127+ return mergedValue ;
128+ }
129+
87130function mergeSyncedSettings (
88131 settings : Record < string , unknown > ,
89- preferences : LocationViewPreferences
132+ preferences : LocationViewPreferences ,
133+ localChanges : PreferenceChanges = { }
90134) : LocationViewPreferences {
91135 const nextPreferences = { ...preferences } ;
92136
93137 forEachSyncedPreference ( key => {
94138 if ( key in settings ) {
95- nextPreferences [ key ] = settings [ key ] as never ;
139+ nextPreferences [ key ] = mergeSyncedValue ( settings [ key ] , preferences [ key ] , localChanges [ key ] ) as never ;
96140 }
97141 } ) ;
98142
99143 return nextPreferences ;
100144}
101145
146+ function cloneSyncedSettings ( settings : Record < string , unknown > ) : Record < string , unknown > {
147+ return JSON . parse ( JSON . stringify ( settings ) ) as Record < string , unknown > ;
148+ }
149+
150+ function getPreferenceChange ( previousValue : unknown , nextValue : unknown ) : PreferenceChange | null {
151+ if ( JSON . stringify ( previousValue ) === JSON . stringify ( nextValue ) ) {
152+ return null ;
153+ }
154+
155+ if ( isPlainObject ( previousValue ) && isPlainObject ( nextValue ) ) {
156+ const changedFields : Record < string , PreferenceChange > = { } ;
157+ const keys = new Set ( [ ...Object . keys ( previousValue ) , ...Object . keys ( nextValue ) ] ) ;
158+
159+ for ( const key of keys ) {
160+ if ( isPrototypeKey ( key ) ) {
161+ continue ;
162+ }
163+
164+ const nestedChange = getPreferenceChange ( previousValue [ key ] , nextValue [ key ] ) ;
165+ if ( nestedChange !== null ) {
166+ changedFields [ key ] = nestedChange ;
167+ }
168+ }
169+
170+ if ( Object . keys ( changedFields ) . length > 0 ) {
171+ return changedFields ;
172+ }
173+ }
174+
175+ return true ;
176+ }
177+
178+ function getChangedPreferences (
179+ previousSettings : Record < string , unknown > ,
180+ preferences : LocationViewPreferences
181+ ) : PreferenceChanges {
182+ const changedPreferences : PreferenceChanges = { } ;
183+
184+ forEachSyncedPreference ( key => {
185+ const change = getPreferenceChange ( previousSettings [ key ] , preferences [ key ] ) ;
186+ if ( change !== null ) {
187+ changedPreferences [ key ] = change ;
188+ }
189+ } ) ;
190+
191+ return changedPreferences ;
192+ }
193+
102194export function configureViewPreferenceSync ( config : PreferenceSyncConfig ) {
103195 syncConfig = {
104196 ...syncConfig ,
105197 ...config ,
106198 } ;
107199}
108200
109- async function refreshViewPreferencesFromServer ( preferences : Ref < LocationViewPreferences > ) {
201+ async function fetchViewPreferencesFromServer ( ) : Promise < Record < string , unknown > | null > {
110202 const auth = useAuthContext ( ) ;
111203 if ( ! auth . isAuthorized ( ) ) {
112- return ;
204+ return null ;
113205 }
114206
115207 const api = useUserApi ( ) ;
116208 const { data, error } = await api . user . getSettings ( ) ;
117209 if ( error || ! data ?. item ) {
118- return ;
210+ return null ;
119211 }
120212
121- preferences . value = mergeSyncedSettings ( data . item , preferences . value ) ;
213+ return data . item ;
122214}
123215export function useViewPreferencesSync ( ) {
124216 if ( syncInitialized || ! import . meta. client ) {
@@ -155,7 +247,7 @@ export function useViewPreferencesSync() {
155247 } ;
156248
157249 const saveToServer = async ( ) => {
158- if ( saveInFlight || pauseServerSaves || ! auth . isAuthorized ( ) ) {
250+ if ( saveInFlight || retryTimer !== null || pauseServerSaves || ! auth . isAuthorized ( ) ) {
159251 return ;
160252 }
161253
@@ -165,7 +257,14 @@ export function useViewPreferencesSync() {
165257 try {
166258 while ( syncedRevision < localRevision && ! pauseServerSaves && auth . isAuthorized ( ) ) {
167259 const targetRevision = localRevision ;
168- const { error } = await api . user . setSettings ( buildSyncedSettings ( preferences . value ) ) ;
260+ let error = false ;
261+ try {
262+ ( { error } = await api . user . setSettings ( buildSyncedSettings ( preferences . value ) ) ) ;
263+ } catch {
264+ scheduleRetry ( ) ;
265+ return ;
266+ }
267+
169268 if ( error ) {
170269 scheduleRetry ( ) ;
171270 return ;
@@ -176,7 +275,7 @@ export function useViewPreferencesSync() {
176275 } finally {
177276 saveInFlight = false ;
178277
179- if ( syncedRevision < localRevision && ! pauseServerSaves ) {
278+ if ( syncedRevision < localRevision && retryTimer === null && ! pauseServerSaves ) {
180279 void saveToServer ( ) ;
181280 }
182281 }
@@ -196,15 +295,22 @@ export function useViewPreferencesSync() {
196295 try {
197296 while ( refreshRequested ) {
198297 refreshRequested = false ;
298+ const refreshRevision = localRevision ;
299+ const refreshSettings = cloneSyncedSettings ( buildSyncedSettings ( preferences . value ) ) ;
199300
200301 pauseServerSaves = true ;
201- applyingServerSnapshot = true ;
202302 try {
203- await refreshViewPreferencesFromServer ( preferences ) ;
303+ const settings = await fetchViewPreferencesFromServer ( ) ;
304+ if ( settings ) {
305+ const localChanges =
306+ localRevision === refreshRevision ? { } : getChangedPreferences ( refreshSettings , preferences . value ) ;
307+ applyingServerSnapshot = true ;
308+ preferences . value = mergeSyncedSettings ( settings , preferences . value , localChanges ) ;
309+ }
204310 } finally {
205311 applyingServerSnapshot = false ;
312+ pauseServerSaves = false ;
206313 }
207- pauseServerSaves = false ;
208314
209315 if ( syncedRevision < localRevision ) {
210316 await saveToServer ( ) ;
@@ -224,7 +330,7 @@ export function useViewPreferencesSync() {
224330
225331 markDirty ( ) ;
226332 } ,
227- { deep : true }
333+ { deep : true , flush : "sync" }
228334 ) ;
229335
230336 watch (
0 commit comments