@@ -16,6 +16,19 @@ import '../types.d';
1616
1717const resetLogger = getLogger ( [ 'wcpos' , 'db' , 'reset' ] ) ;
1818
19+ // Track removal counts for debugging
20+ const removalCounts : Record < string , number > = { } ;
21+
22+ // Track in-progress re-additions to prevent double execution
23+ const pendingReAdditions = new Set < string > ( ) ;
24+
25+ /**
26+ * Collections currently being swapped by swapCollections().
27+ * When a collection is in this set, the reset plugin will re-add it but NOT emit on reset$.
28+ * The swapCollections function handles the reset$ emission after the swap is complete.
29+ */
30+ export const swappingCollections = new Set < string > ( ) ;
31+
1932/**
2033 * Set of collection names that this plugin manages.
2134 * Only these collections will be auto-recreated after removal.
@@ -42,6 +55,18 @@ function isManagedCollection(collectionName: string, databaseName: string): bool
4255const storeReset = new Subject < RxCollection > ( ) ;
4356const syncReset = new Subject < RxCollection > ( ) ;
4457
58+ /**
59+ * Manually emit a reset event for a collection.
60+ * Used by swapCollections after swap is complete.
61+ */
62+ export function emitCollectionReset ( collection : RxCollection , databaseName : string ) : void {
63+ if ( databaseName . startsWith ( 'fast_store' ) ) {
64+ syncReset . next ( collection ) ;
65+ } else if ( databaseName . startsWith ( 'store' ) ) {
66+ storeReset . next ( collection ) ;
67+ }
68+ }
69+
4570/**
4671 * Reset Collection Plugin
4772 *
@@ -97,6 +122,17 @@ export const resetCollectionPlugin: RxPlugin = {
97122 after : async ( collection ) => {
98123 const database = collection . database ;
99124 const collectionName = collection . name ;
125+
126+ // Capture stack trace immediately to debug what triggers removal
127+ const triggerStack = new Error ( ) . stack ;
128+ resetLogger . debug ( 'postCloseRxCollection triggered' , {
129+ context : {
130+ collection : collectionName ,
131+ database : database . name ,
132+ isDestroyed : ( collection as any ) . destroyed ,
133+ trigger : triggerStack ?. split ( '\n' ) . slice ( 2 , 10 ) . join ( ' | ' ) ,
134+ } ,
135+ } ) ;
100136
101137 // Only re-add collections we manage (not FlexSearch, etc.)
102138 if ( ! isManagedCollection ( collectionName , database . name ) ) {
@@ -106,10 +142,65 @@ export const resetCollectionPlugin: RxPlugin = {
106142 return ;
107143 }
108144
145+ // Guard against re-entrance: if we're already re-adding this collection, skip
146+ const reAddKey = `${ database . name } :${ collectionName } ` ;
147+ if ( pendingReAdditions . has ( reAddKey ) ) {
148+ resetLogger . debug ( 'Skipping re-addition - already in progress' , {
149+ context : { collection : collectionName , database : database . name , reAddKey } ,
150+ } ) ;
151+ return ;
152+ }
153+
154+ resetLogger . debug ( 'Setting pending re-addition flag' , {
155+ context : { reAddKey, pendingCount : pendingReAdditions . size } ,
156+ } ) ;
157+ pendingReAdditions . add ( reAddKey ) ;
158+
159+ // Track removal count for debugging
160+ const key = `${ database . name } :${ collectionName } ` ;
161+ removalCounts [ key ] = ( removalCounts [ key ] || 0 ) + 1 ;
162+ const removalNumber = removalCounts [ key ] ;
163+
164+ // Capture stack trace to debug double-removal issues
165+ const stackTrace = new Error ( ) . stack ;
166+
167+ // Check if this is a stale collection reference being closed
168+ // If a DIFFERENT collection instance already exists, this is likely a stale
169+ // reference from a previous swap, and we shouldn't re-add
170+ const existingCollection = database . collections [ collectionName ] ;
171+ const isStaleReference = existingCollection && existingCollection !== collection ;
172+ const collectionAlreadyExists = ! ! existingCollection && ! ( existingCollection as any ) . destroyed ;
173+
109174 resetLogger . debug ( 'Re-adding collection after removal' , {
110- context : { collection : collectionName , database : database . name } ,
175+ context : {
176+ collection : collectionName ,
177+ database : database . name ,
178+ removalNumber,
179+ collectionRef : ( collection as any ) . _instanceId || 'unknown' ,
180+ isStaleReference,
181+ collectionAlreadyExists,
182+ stack : stackTrace ?. split ( '\n' ) . slice ( 2 , 8 ) . join ( ' | ' ) ,
183+ } ,
111184 } ) ;
112185
186+ // If this is a stale reference being closed while a new collection exists, skip
187+ if ( isStaleReference ) {
188+ resetLogger . debug ( 'Skipping re-addition - stale reference, new collection exists' , {
189+ context : { collection : collectionName , database : database . name } ,
190+ } ) ;
191+ pendingReAdditions . delete ( reAddKey ) ;
192+ return ;
193+ }
194+
195+ // If collection already exists and is not destroyed, skip re-addition
196+ if ( collectionAlreadyExists ) {
197+ resetLogger . debug ( 'Collection already exists, skipping re-addition' , {
198+ context : { collection : collectionName , database : database . name } ,
199+ } ) ;
200+ pendingReAdditions . delete ( reAddKey ) ;
201+ return ;
202+ }
203+
113204 try {
114205 if ( database . name . startsWith ( 'fast_store' ) ) {
115206 const schema = syncCollections [ collectionName as keyof SyncCollections ] ;
@@ -126,11 +217,19 @@ export const resetCollectionPlugin: RxPlugin = {
126217 }
127218
128219 const cols = await database . addCollections ( { [ collectionName ] : schema } ) ;
129- syncReset . next ( cols [ collectionName ] ) ;
130-
131- resetLogger . debug ( 'Sync collection re-added successfully' , {
132- context : { collection : collectionName } ,
133- } ) ;
220+
221+ // Only emit on reset$ if NOT being swapped by swapCollections
222+ // (swapCollections handles emission after swap completes)
223+ if ( ! swappingCollections . has ( collectionName ) ) {
224+ syncReset . next ( cols [ collectionName ] ) ;
225+ resetLogger . debug ( 'Sync collection re-added and emitted reset$' , {
226+ context : { collection : collectionName } ,
227+ } ) ;
228+ } else {
229+ resetLogger . debug ( 'Sync collection re-added (no emit - in swap)' , {
230+ context : { collection : collectionName } ,
231+ } ) ;
232+ }
134233 } else if ( database . name . startsWith ( 'store' ) ) {
135234 const schema = storeCollections [ collectionName as keyof StoreCollections ] ;
136235 if ( ! schema ) {
@@ -146,11 +245,19 @@ export const resetCollectionPlugin: RxPlugin = {
146245 }
147246
148247 const cols = await database . addCollections ( { [ collectionName ] : schema } ) ;
149- storeReset . next ( cols [ collectionName ] ) ;
150-
151- resetLogger . debug ( 'Store collection re-added successfully' , {
152- context : { collection : collectionName } ,
153- } ) ;
248+
249+ // Only emit on reset$ if NOT being swapped by swapCollections
250+ // (swapCollections handles emission after swap completes)
251+ if ( ! swappingCollections . has ( collectionName ) ) {
252+ storeReset . next ( cols [ collectionName ] ) ;
253+ resetLogger . debug ( 'Store collection re-added and emitted reset$' , {
254+ context : { collection : collectionName } ,
255+ } ) ;
256+ } else {
257+ resetLogger . debug ( 'Store collection re-added (no emit - in swap)' , {
258+ context : { collection : collectionName } ,
259+ } ) ;
260+ }
154261 }
155262 } catch ( error : any ) {
156263 resetLogger . error ( 'Failed to re-add collection' , {
@@ -163,6 +270,12 @@ export const resetCollectionPlugin: RxPlugin = {
163270 error : error . message ,
164271 } ,
165272 } ) ;
273+ } finally {
274+ // Clear the pending flag after completion (success or failure)
275+ resetLogger . debug ( 'Clearing pending re-addition flag' , {
276+ context : { reAddKey, pendingCount : pendingReAdditions . size - 1 } ,
277+ } ) ;
278+ pendingReAdditions . delete ( reAddKey ) ;
166279 }
167280 } ,
168281 } ,
0 commit comments