Skip to content

Commit 6e57ac3

Browse files
authored
fix transaction sorting order and enforce them having an id (#230)
1 parent 13041b4 commit 6e57ac3

File tree

4 files changed

+45
-23
lines changed

4 files changed

+45
-23
lines changed

.changeset/khaki-cougars-give.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+
add a sequence number to transactions to when sorting we can ensure that those created in the same ms are sorted in the correct order

.changeset/open-foxes-say.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+
Ensure that all transactions are given an id, fixes a potential bug with direct mutations

packages/db/src/collection.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Store } from "@tanstack/store"
22
import { withArrayChangeTracking, withChangeTracking } from "./proxy"
3-
import { Transaction, getActiveTransaction } from "./transactions"
3+
import { createTransaction, getActiveTransaction } from "./transactions"
44
import { SortedMap } from "./SortedMap"
5+
import type { Transaction } from "./transactions"
56
import type {
67
ChangeListener,
78
ChangeMessage,
@@ -277,8 +278,8 @@ export class CollectionImpl<
277278
throw new Error(`Collection requires a sync config`)
278279
}
279280

280-
this.transactions = new SortedMap<string, Transaction<any>>(
281-
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
281+
this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>
282+
a.compareCreatedAt(b)
282283
)
283284

284285
this.config = config
@@ -1242,7 +1243,7 @@ export class CollectionImpl<
12421243
return ambientTransaction
12431244
} else {
12441245
// Create a new transaction with a mutation function that calls the onInsert handler
1245-
const directOpTransaction = new Transaction<T>({
1246+
const directOpTransaction = createTransaction<T>({
12461247
mutationFn: async (params) => {
12471248
// Call the onInsert handler with the transaction
12481249
return this.config.onInsert!(params)
@@ -1444,7 +1445,7 @@ export class CollectionImpl<
14441445

14451446
// If no changes were made, return an empty transaction early
14461447
if (mutations.length === 0) {
1447-
const emptyTransaction = new Transaction({
1448+
const emptyTransaction = createTransaction({
14481449
mutationFn: async () => {},
14491450
})
14501451
emptyTransaction.commit()
@@ -1464,7 +1465,7 @@ export class CollectionImpl<
14641465
// No need to check for onUpdate handler here as we've already checked at the beginning
14651466

14661467
// Create a new transaction with a mutation function that calls the onUpdate handler
1467-
const directOpTransaction = new Transaction<T>({
1468+
const directOpTransaction = createTransaction<T>({
14681469
mutationFn: async (params) => {
14691470
// Call the onUpdate handler with the transaction
14701471
return this.config.onUpdate!(params)
@@ -1559,7 +1560,7 @@ export class CollectionImpl<
15591560
}
15601561

15611562
// Create a new transaction with a mutation function that calls the onDelete handler
1562-
const directOpTransaction = new Transaction<T>({
1563+
const directOpTransaction = createTransaction<T>({
15631564
autoCommit: true,
15641565
mutationFn: async (params) => {
15651566
// Call the onDelete handler with the transaction

packages/db/src/transactions.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,13 @@ import type {
1212
const transactions: Array<Transaction<any>> = []
1313
let transactionStack: Array<Transaction<any>> = []
1414

15+
let sequenceNumber = 0
16+
1517
export function createTransaction<
1618
TData extends object = Record<string, unknown>,
1719
>(config: TransactionConfig<TData>): Transaction<TData> {
18-
if (typeof config.mutationFn === `undefined`) {
19-
throw `mutationFn is required when creating a transaction`
20-
}
21-
22-
let transactionId = config.id
23-
if (!transactionId) {
24-
transactionId = crypto.randomUUID()
25-
}
26-
const newTransaction = new Transaction<TData>({
27-
...config,
28-
id: transactionId,
29-
})
30-
20+
const newTransaction = new Transaction<TData>(config)
3121
transactions.push(newTransaction)
32-
3322
return newTransaction
3423
}
3524

@@ -56,7 +45,7 @@ function removeFromPendingList(tx: Transaction<any>) {
5645
}
5746
}
5847

59-
export class Transaction<
48+
class Transaction<
6049
T extends object = Record<string, unknown>,
6150
TOperation extends OperationType = OperationType,
6251
> {
@@ -67,20 +56,25 @@ export class Transaction<
6756
public isPersisted: Deferred<Transaction<T, TOperation>>
6857
public autoCommit: boolean
6958
public createdAt: Date
59+
public sequenceNumber: number
7060
public metadata: Record<string, unknown>
7161
public error?: {
7262
message: string
7363
error: Error
7464
}
7565

7666
constructor(config: TransactionConfig<T>) {
77-
this.id = config.id!
67+
if (typeof config.mutationFn === `undefined`) {
68+
throw `mutationFn is required when creating a transaction`
69+
}
70+
this.id = config.id ?? crypto.randomUUID()
7871
this.mutationFn = config.mutationFn
7972
this.state = `pending`
8073
this.mutations = []
8174
this.isPersisted = createDeferred<Transaction<T, TOperation>>()
8275
this.autoCommit = config.autoCommit ?? true
8376
this.createdAt = new Date()
77+
this.sequenceNumber = sequenceNumber++
8478
this.metadata = config.metadata ?? {}
8579
}
8680

@@ -210,4 +204,21 @@ export class Transaction<
210204

211205
return this
212206
}
207+
208+
/**
209+
* Compare two transactions by their createdAt time and sequence number in order
210+
* to sort them in the order they were created.
211+
* @param other - The other transaction to compare to
212+
* @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time
213+
*/
214+
compareCreatedAt(other: Transaction<any>): number {
215+
const createdAtComparison =
216+
this.createdAt.getTime() - other.createdAt.getTime()
217+
if (createdAtComparison !== 0) {
218+
return createdAtComparison
219+
}
220+
return this.sequenceNumber - other.sequenceNumber
221+
}
213222
}
223+
224+
export type { Transaction }

0 commit comments

Comments
 (0)