@@ -159,13 +159,180 @@ function parseOfflineQueuePayload(raw: string): SyncQueueEntry[] {
159159 throw new Error ( 'Invalid offline queue payload' ) ;
160160 }
161161
162- return payload . queue
162+ const normalized = payload . queue
163163 . filter ( isQueueEntry )
164164 . map ( entry => ( {
165165 ...entry ,
166166 path : normalizePath ( entry . path ) ,
167167 oldPath : entry . oldPath ? normalizePath ( entry . oldPath ) : undefined ,
168168 } ) ) ;
169+ return compactQueueEntries ( normalized ) ;
170+ }
171+
172+ function findLastQueuedEntryByPath ( queue : SyncQueueEntry [ ] , path : string ) : number {
173+ for ( let index = queue . length - 1 ; index >= 0 ; index -= 1 ) {
174+ if ( queue [ index ] ?. path === path ) {
175+ return index ;
176+ }
177+ }
178+ return - 1 ;
179+ }
180+
181+ function compactQueueEntries ( queue : SyncQueueEntry [ ] ) : SyncQueueEntry [ ] {
182+ const compacted : SyncQueueEntry [ ] = [ ] ;
183+
184+ const appendDelete = ( entry : SyncQueueEntry ) : void => {
185+ let targetPath = entry . path ;
186+ while ( true ) {
187+ const existingIndex = findLastQueuedEntryByPath ( compacted , targetPath ) ;
188+ if ( existingIndex < 0 ) {
189+ compacted . push ( {
190+ ...entry ,
191+ action : 'delete' ,
192+ path : targetPath ,
193+ oldPath : undefined ,
194+ } ) ;
195+ return ;
196+ }
197+
198+ const existing = compacted [ existingIndex ] ! ;
199+ if ( existing . action === 'create' ) {
200+ compacted . splice ( existingIndex , 1 ) ;
201+ return ;
202+ }
203+ if ( existing . action === 'update' ) {
204+ compacted [ existingIndex ] = {
205+ ...entry ,
206+ action : 'delete' ,
207+ path : targetPath ,
208+ oldPath : undefined ,
209+ } ;
210+ return ;
211+ }
212+ if ( existing . action === 'delete' ) {
213+ return ;
214+ }
215+ if ( existing . action === 'rename' && existing . oldPath ) {
216+ targetPath = existing . oldPath ;
217+ compacted . splice ( existingIndex , 1 ) ;
218+ continue ;
219+ }
220+
221+ compacted [ existingIndex ] = {
222+ ...entry ,
223+ action : 'delete' ,
224+ path : targetPath ,
225+ oldPath : undefined ,
226+ } ;
227+ return ;
228+ }
229+ } ;
230+
231+ for ( const rawEntry of queue ) {
232+ const entry : SyncQueueEntry = {
233+ ...rawEntry ,
234+ path : normalizePath ( rawEntry . path ) ,
235+ oldPath : rawEntry . oldPath ? normalizePath ( rawEntry . oldPath ) : undefined ,
236+ } ;
237+
238+ if ( entry . action === 'create' || entry . action === 'update' ) {
239+ const existingIndex = findLastQueuedEntryByPath ( compacted , entry . path ) ;
240+ if ( existingIndex < 0 ) {
241+ compacted . push ( {
242+ ...entry ,
243+ oldPath : undefined ,
244+ } ) ;
245+ continue ;
246+ }
247+
248+ const existing = compacted [ existingIndex ] ! ;
249+ if ( existing . action === 'create' ) {
250+ compacted [ existingIndex ] = {
251+ ...existing ,
252+ timestamp : entry . timestamp ,
253+ retryCount : entry . retryCount ,
254+ localHash : entry . localHash ,
255+ } ;
256+ continue ;
257+ }
258+ if ( existing . action === 'update' || existing . action === 'delete' ) {
259+ compacted [ existingIndex ] = {
260+ ...entry ,
261+ action : 'update' ,
262+ oldPath : undefined ,
263+ } ;
264+ continue ;
265+ }
266+ if ( existing . action === 'rename' ) {
267+ // Rename already includes content hash of destination path.
268+ continue ;
269+ }
270+
271+ compacted . push ( {
272+ ...entry ,
273+ oldPath : undefined ,
274+ } ) ;
275+ continue ;
276+ }
277+
278+ if ( entry . action === 'delete' ) {
279+ appendDelete ( entry ) ;
280+ continue ;
281+ }
282+
283+ const oldPath = entry . oldPath ;
284+ if ( ! oldPath || oldPath === entry . path ) {
285+ continue ;
286+ }
287+
288+ const duplicateDestinationIndex = findLastQueuedEntryByPath ( compacted , entry . path ) ;
289+ if ( duplicateDestinationIndex >= 0 ) {
290+ const duplicateDestination = compacted [ duplicateDestinationIndex ] ! ;
291+ if ( duplicateDestination . action === 'create' || duplicateDestination . action === 'update' ) {
292+ compacted . splice ( duplicateDestinationIndex , 1 ) ;
293+ }
294+ }
295+
296+ const sourceIndex = findLastQueuedEntryByPath ( compacted , oldPath ) ;
297+ if ( sourceIndex < 0 ) {
298+ compacted . push ( entry ) ;
299+ continue ;
300+ }
301+
302+ const source = compacted [ sourceIndex ] ! ;
303+ if ( source . action === 'create' ) {
304+ compacted [ sourceIndex ] = {
305+ ...source ,
306+ path : entry . path ,
307+ timestamp : entry . timestamp ,
308+ retryCount : entry . retryCount ,
309+ localHash : entry . localHash ?? source . localHash ,
310+ } ;
311+ continue ;
312+ }
313+ if ( source . action === 'update' ) {
314+ compacted [ sourceIndex ] = {
315+ ...entry ,
316+ action : 'rename' ,
317+ oldPath,
318+ } ;
319+ continue ;
320+ }
321+ if ( source . action === 'rename' && source . oldPath ) {
322+ compacted [ sourceIndex ] = {
323+ ...source ,
324+ path : entry . path ,
325+ timestamp : entry . timestamp ,
326+ retryCount : entry . retryCount ,
327+ localHash : entry . localHash ?? source . localHash ,
328+ } ;
329+ continue ;
330+ }
331+
332+ compacted . push ( entry ) ;
333+ }
334+
335+ return compacted ;
169336}
170337
171338function isActivityAction ( value : unknown ) : value is ActivityAction {
@@ -1675,7 +1842,7 @@ export class SyncManager {
16751842 this . cancelModifyDebouncer ( normalizedEntry . oldPath ) ;
16761843 }
16771844
1678- this . pendingPushQueue . push ( normalizedEntry ) ;
1845+ this . pendingPushQueue = compactQueueEntries ( [ ... this . pendingPushQueue , normalizedEntry ] ) ;
16791846 this . updateStatusFromCurrentState ( ) ;
16801847 this . schedulePushQueueProcessing ( ) ;
16811848 }
@@ -1998,7 +2165,7 @@ export class SyncManager {
19982165
19992166 this . replayInFlight = true ;
20002167 try {
2001- this . pendingPushQueue = [ ...this . offlineQueue , ...this . pendingPushQueue ] ;
2168+ this . pendingPushQueue = compactQueueEntries ( [ ...this . offlineQueue , ...this . pendingPushQueue ] ) ;
20022169 this . offlineQueue = [ ] ;
20032170 await this . persistOfflineQueue ( ) ;
20042171 return this . flushPendingPushQueue ( ) ;
@@ -2012,7 +2179,7 @@ export class SyncManager {
20122179 return ;
20132180 }
20142181
2015- this . offlineQueue . push ( ...this . pendingPushQueue ) ;
2182+ this . offlineQueue = compactQueueEntries ( [ ...this . offlineQueue , ... this . pendingPushQueue ] ) ;
20162183 this . pendingPushQueue = [ ] ;
20172184 await this . persistOfflineQueue ( ) ;
20182185 }
0 commit comments