Skip to content

Commit bc2f204

Browse files
authored
cleanup complete transactions (#371)
1 parent 233a60e commit bc2f204

File tree

3 files changed

+41
-23
lines changed

3 files changed

+41
-23
lines changed

.changeset/purple-parts-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
Cleanup transactions after they complete to prevent memory leak and performance degradation

packages/db/src/collection.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -703,12 +703,9 @@ export class CollectionImpl<
703703
this.optimisticDeletes.clear()
704704

705705
const activeTransactions: Array<Transaction<any>> = []
706-
const completedTransactions: Array<Transaction<any>> = []
707706

708707
for (const transaction of this.transactions.values()) {
709-
if (transaction.state === `completed`) {
710-
completedTransactions.push(transaction)
711-
} else if (![`completed`, `failed`].includes(transaction.state)) {
708+
if (![`completed`, `failed`].includes(transaction.state)) {
712709
activeTransactions.push(transaction)
713710
}
714711
}
@@ -761,7 +758,6 @@ export class CollectionImpl<
761758
// IMPORTANT: Skip complex filtering for user-triggered actions to prevent UI blocking
762759
if (this.pendingSyncedTransactions.length > 0 && !triggeredByUserAction) {
763760
const pendingSyncKeys = new Set<TKey>()
764-
const completedTransactionMutations = new Set<string>()
765761

766762
// Collect keys from pending sync operations
767763
for (const transaction of this.pendingSyncedTransactions) {
@@ -770,15 +766,6 @@ export class CollectionImpl<
770766
}
771767
}
772768

773-
// Collect mutation IDs from completed transactions
774-
for (const tx of completedTransactions) {
775-
for (const mutation of tx.mutations) {
776-
if (mutation.collection === this) {
777-
completedTransactionMutations.add(mutation.mutationId)
778-
}
779-
}
780-
}
781-
782769
// Only filter out delete events for keys that:
783770
// 1. Have pending sync operations AND
784771
// 2. Are from completed transactions (being cleaned up)
@@ -1290,6 +1277,30 @@ export class CollectionImpl<
12901277
}
12911278
}
12921279

1280+
/**
1281+
* Schedule cleanup of a transaction when it completes
1282+
* @private
1283+
*/
1284+
private scheduleTransactionCleanup(transaction: Transaction<any>): void {
1285+
// Only schedule cleanup for transactions that aren't already completed
1286+
if (transaction.state === `completed`) {
1287+
this.transactions.delete(transaction.id)
1288+
return
1289+
}
1290+
1291+
// Schedule cleanup when the transaction completes
1292+
transaction.isPersisted.promise
1293+
.then(() => {
1294+
// Transaction completed successfully, remove it immediately
1295+
this.transactions.delete(transaction.id)
1296+
})
1297+
.catch(() => {
1298+
// Transaction failed, but we want to keep failed transactions for reference
1299+
// so don't remove it.
1300+
// This empty catch block is necessary to prevent unhandled promise rejections.
1301+
})
1302+
}
1303+
12931304
private ensureStandardSchema(schema: unknown): StandardSchema<T> {
12941305
// If the schema already implements the standard-schema interface, return it
12951306
if (schema && `~standard` in (schema as {})) {
@@ -1650,6 +1661,7 @@ export class CollectionImpl<
16501661
ambientTransaction.applyMutations(mutations)
16511662

16521663
this.transactions.set(ambientTransaction.id, ambientTransaction)
1664+
this.scheduleTransactionCleanup(ambientTransaction)
16531665
this.recomputeOptimisticState(true)
16541666

16551667
return ambientTransaction
@@ -1675,6 +1687,7 @@ export class CollectionImpl<
16751687

16761688
// Add the transaction to the collection's transactions store
16771689
this.transactions.set(directOpTransaction.id, directOpTransaction)
1690+
this.scheduleTransactionCleanup(directOpTransaction)
16781691
this.recomputeOptimisticState(true)
16791692

16801693
return directOpTransaction
@@ -1864,6 +1877,8 @@ export class CollectionImpl<
18641877
mutationFn: async () => {},
18651878
})
18661879
emptyTransaction.commit()
1880+
// Schedule cleanup for empty transaction
1881+
this.scheduleTransactionCleanup(emptyTransaction)
18671882
return emptyTransaction
18681883
}
18691884

@@ -1872,6 +1887,7 @@ export class CollectionImpl<
18721887
ambientTransaction.applyMutations(mutations)
18731888

18741889
this.transactions.set(ambientTransaction.id, ambientTransaction)
1890+
this.scheduleTransactionCleanup(ambientTransaction)
18751891
this.recomputeOptimisticState(true)
18761892

18771893
return ambientTransaction
@@ -1901,6 +1917,7 @@ export class CollectionImpl<
19011917
// Add the transaction to the collection's transactions store
19021918

19031919
this.transactions.set(directOpTransaction.id, directOpTransaction)
1920+
this.scheduleTransactionCleanup(directOpTransaction)
19041921
this.recomputeOptimisticState(true)
19051922

19061923
return directOpTransaction
@@ -1988,6 +2005,7 @@ export class CollectionImpl<
19882005
ambientTransaction.applyMutations(mutations)
19892006

19902007
this.transactions.set(ambientTransaction.id, ambientTransaction)
2008+
this.scheduleTransactionCleanup(ambientTransaction)
19912009
this.recomputeOptimisticState(true)
19922010

19932011
return ambientTransaction
@@ -2014,6 +2032,7 @@ export class CollectionImpl<
20142032
directOpTransaction.commit()
20152033

20162034
this.transactions.set(directOpTransaction.id, directOpTransaction)
2035+
this.scheduleTransactionCleanup(directOpTransaction)
20172036
this.recomputeOptimisticState(true)
20182037

20192038
return directOpTransaction

packages/db/tests/collection.test.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,15 +264,9 @@ describe(`Collection`, () => {
264264
value: `bar`,
265265
})
266266

267-
// after mutationFn returns, check that the transaction is updated &
268-
// optimistic update is gone & synced data & comibned state are all updated.
269-
expect(
270-
// @ts-expect-error possibly undefined is ok in test
271-
Array.from(collection.transactions.values())[0].mutations[0].changes
272-
).toEqual({
273-
id: 1,
274-
value: `bar`,
275-
})
267+
// after mutationFn returns, check that the transaction is cleaned up,
268+
// optimistic update is gone & synced data & combined state are all updated.
269+
expect(collection.transactions.size).toEqual(0) // Transaction should be cleaned up
276270
expect(collection.state).toEqual(
277271
new Map([[insertedKey, { id: 1, value: `bar` }]])
278272
)

0 commit comments

Comments
 (0)