Skip to content

Commit ddb4201

Browse files
shajan-journalclaude
authored andcommitted
fix: resolve SyncManager.dispose() race condition
Set isDisposed = true eagerly (before awaiting internal collection disposal) and add isDisposed guards in collection event handlers and async continuations so in-flight operations bail out instead of touching already-disposed collections. Fixes #2545 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 342c2c7 commit ddb4201

2 files changed

Lines changed: 15 additions & 1 deletion

File tree

packages/base/core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
1012
* Introduced the `transformAll` option when creating a `Collection`. This allows you to define a function that transform items after they are retrieved from persistence, enabling the integration of data from other collections or external sources (thanks @signalize!)
1113

14+
### Fixed
15+
16+
* Fixed a race condition in SyncManager.dispose() that could cause unhandled "Collection is disposed" errors during organization/context switches while sync operations were still in flight (thanks @shajan-journal!)
17+
1218
## [1.7.2] - 2026-01-07
1319

1420
### Changed

packages/base/sync/src/SyncManager.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export default class SyncManager<
191191
* Clears all internal data structures
192192
*/
193193
public async dispose() {
194+
this.isDisposed = true
194195
this.collections.clear()
195196
this.syncQueues.clear()
196197
this.remoteChanges.splice(0)
@@ -199,7 +200,6 @@ export default class SyncManager<
199200
this.snapshots.dispose(),
200201
this.syncOperations.dispose(),
201202
])
202-
this.isDisposed = true
203203
}
204204

205205
/**
@@ -271,6 +271,7 @@ export default class SyncManager<
271271
}
272272

273273
collection.on('added', (item) => {
274+
if (this.isDisposed) return
274275
// skip the change if it was a remote change
275276
if (hasRemoteChange({ collectionName: options.name, type: 'insert', data: item })) {
276277
removeRemoteChanges(options.name, item.id)
@@ -287,6 +288,7 @@ export default class SyncManager<
287288
this.schedulePush(options.name)
288289
})
289290
collection.on('changed', ({ id }, modifier) => {
291+
if (this.isDisposed) return
290292
const data = { id, modifier }
291293
// skip the change if it was a remote change
292294
if (hasRemoteChange({ collectionName: options.name, type: 'update', data })) {
@@ -304,6 +306,7 @@ export default class SyncManager<
304306
this.schedulePush(options.name)
305307
})
306308
collection.on('removed', ({ id }) => {
309+
if (this.isDisposed) return
307310
// skip the change if it was a remote change
308311
if (hasRemoteChange({ collectionName: options.name, type: 'remove', data: id })) {
309312
removeRemoteChanges(options.name, id)
@@ -365,6 +368,7 @@ export default class SyncManager<
365368
? await this.options.registerRemoteChange(
366369
collectionParameters.options,
367370
async (data) => {
371+
if (this.isDisposed) return
368372
if (data == null) {
369373
await this.sync(name)
370374
} else {
@@ -377,6 +381,7 @@ export default class SyncManager<
377381
})
378382
await this.syncWithData(name, data)
379383
.then(() => {
384+
if (this.isDisposed) return
380385
// clean up old sync operations
381386
this.syncOperations.removeMany({
382387
id: { $ne: syncId },
@@ -577,6 +582,7 @@ export default class SyncManager<
577582
name: string,
578583
data: LoadResponse<ItemType>,
579584
) {
585+
if (this.isDisposed) return
580586
const { collection, options: collectionOptions } = this.getCollectionProperties(name)
581587

582588
const syncTime = Date.now()
@@ -665,6 +671,8 @@ export default class SyncManager<
665671
},
666672
})
667673
.then(async (snapshot) => {
674+
if (this.isDisposed) return
675+
668676
// clean up old snapshots
669677
this.snapshots.removeMany({
670678
collectionName: name,

0 commit comments

Comments
 (0)