@@ -24,6 +24,8 @@ export class FirestoreWatcher {
2424 // Track processed document IDs to prevent duplicates
2525 private processedIds = new Set < string > ( ) ;
2626 private processingIds = new Set < string > ( ) ;
27+ private processedIdTimers = new Map < string , NodeJS . Timeout > ( ) ;
28+ private readonly duplicateBlockDurationMs = 5000 ; // Only block duplicates for 5s
2729
2830 // Clean up old processed IDs periodically to prevent memory leaks
2931 private cleanupInterval : NodeJS . Timeout | null = null ;
@@ -150,6 +152,9 @@ export class FirestoreWatcher {
150152 this . unsubscribe = null ;
151153 console . log ( `Successfully stopped watcher for ${ collectionPath } ` ) ;
152154 }
155+
156+ // Clear any in-memory duplicate tracking
157+ this . clearProcessedIds ( ) ;
153158
154159 // Stop cleanup interval
155160 if ( this . cleanupInterval ) {
@@ -174,7 +179,7 @@ export class FirestoreWatcher {
174179 // Clean up processed IDs every 5 minutes to prevent memory leaks
175180 this . cleanupInterval = setInterval ( ( ) => {
176181 const beforeSize = this . processedIds . size ;
177- this . processedIds . clear ( ) ;
182+ this . clearProcessedIds ( ) ;
178183 const afterSize = this . processedIds . size ;
179184 console . log ( `Cleaned up processed IDs: ${ beforeSize } -> ${ afterSize } ` ) ;
180185 } , 5 * 60 * 1000 ) ; // 5 minutes
@@ -309,6 +314,11 @@ export class FirestoreWatcher {
309314 clearProcessedIds ( ) : void {
310315 const beforeSize = this . processedIds . size ;
311316 this . processedIds . clear ( ) ;
317+ // Clear and reset any pending expiry timers
318+ for ( const timer of this . processedIdTimers . values ( ) ) {
319+ clearTimeout ( timer ) ;
320+ }
321+ this . processedIdTimers . clear ( ) ;
312322 console . log ( `Manually cleared processed IDs: ${ beforeSize } -> 0` ) ;
313323 }
314324
@@ -419,7 +429,7 @@ export class FirestoreWatcher {
419429 await this . handleCreateOrUpdate ( doc , data ) ;
420430
421431 // Mark as processed
422- this . processedIds . add ( docId ) ;
432+ this . markAsProcessed ( docId ) ;
423433 } finally {
424434 this . processingIds . delete ( docId ) ;
425435 }
@@ -429,6 +439,11 @@ export class FirestoreWatcher {
429439 console . log ( `Document removed: ${ docId } ` ) ;
430440 // Remove from processed IDs when document is deleted
431441 this . processedIds . delete ( docId ) ;
442+ const timer = this . processedIdTimers . get ( docId ) ;
443+ if ( timer ) {
444+ clearTimeout ( timer ) ;
445+ this . processedIdTimers . delete ( docId ) ;
446+ }
432447 this . processingIds . delete ( docId ) ;
433448 break ;
434449 }
@@ -452,6 +467,26 @@ export class FirestoreWatcher {
452467 await this . processChanges ( changes ) ;
453468 }
454469
470+ /**
471+ * Marks a document as processed and schedules its removal after a short window.
472+ */
473+ private markAsProcessed ( docId : string ) : void {
474+ // Reset any existing timer for this doc
475+ const existingTimer = this . processedIdTimers . get ( docId ) ;
476+ if ( existingTimer ) {
477+ clearTimeout ( existingTimer ) ;
478+ }
479+
480+ this . processedIds . add ( docId ) ;
481+
482+ const timer = setTimeout ( ( ) => {
483+ this . processedIds . delete ( docId ) ;
484+ this . processedIdTimers . delete ( docId ) ;
485+ } , this . duplicateBlockDurationMs ) ;
486+
487+ this . processedIdTimers . set ( docId , timer ) ;
488+ }
489+
455490
456491 private async handleCreateOrUpdate (
457492 doc : FirebaseFirestore . QueryDocumentSnapshot < DocumentData > ,
0 commit comments