@tanstack/db 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cjs/SortedMap.cjs.map +1 -1
  2. package/dist/cjs/collection.cjs +47 -6
  3. package/dist/cjs/collection.cjs.map +1 -1
  4. package/dist/cjs/collection.d.cts +150 -45
  5. package/dist/cjs/deferred.cjs.map +1 -1
  6. package/dist/cjs/errors.cjs.map +1 -1
  7. package/dist/cjs/optimistic-action.cjs.map +1 -1
  8. package/dist/cjs/proxy.cjs.map +1 -1
  9. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  10. package/dist/cjs/query/builder/index.cjs.map +1 -1
  11. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  12. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  13. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  14. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  15. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  16. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  17. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  18. package/dist/cjs/query/ir.cjs.map +1 -1
  19. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  20. package/dist/cjs/transactions.cjs +116 -0
  21. package/dist/cjs/transactions.cjs.map +1 -1
  22. package/dist/cjs/transactions.d.cts +179 -0
  23. package/dist/cjs/types.d.cts +169 -13
  24. package/dist/esm/SortedMap.js.map +1 -1
  25. package/dist/esm/collection.d.ts +150 -45
  26. package/dist/esm/collection.js +47 -6
  27. package/dist/esm/collection.js.map +1 -1
  28. package/dist/esm/deferred.js.map +1 -1
  29. package/dist/esm/errors.js.map +1 -1
  30. package/dist/esm/optimistic-action.js.map +1 -1
  31. package/dist/esm/proxy.js.map +1 -1
  32. package/dist/esm/query/builder/functions.js.map +1 -1
  33. package/dist/esm/query/builder/index.js.map +1 -1
  34. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  35. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  36. package/dist/esm/query/compiler/group-by.js.map +1 -1
  37. package/dist/esm/query/compiler/index.js.map +1 -1
  38. package/dist/esm/query/compiler/joins.js.map +1 -1
  39. package/dist/esm/query/compiler/order-by.js.map +1 -1
  40. package/dist/esm/query/compiler/select.js.map +1 -1
  41. package/dist/esm/query/ir.js.map +1 -1
  42. package/dist/esm/query/live-query-collection.js.map +1 -1
  43. package/dist/esm/transactions.d.ts +179 -0
  44. package/dist/esm/transactions.js +116 -0
  45. package/dist/esm/transactions.js.map +1 -1
  46. package/dist/esm/types.d.ts +169 -13
  47. package/package.json +1 -1
  48. package/src/collection.ts +165 -50
  49. package/src/proxy.ts +0 -1
  50. package/src/transactions.ts +179 -0
  51. package/src/types.ts +199 -28
@@ -1,6 +1,69 @@
1
1
  import { Deferred } from './deferred.js';
2
2
  import { MutationFn, OperationType, PendingMutation, TransactionConfig, TransactionState } from './types.js';
3
+ /**
4
+ * Creates a new transaction for grouping multiple collection operations
5
+ * @param config - Transaction configuration with mutation function
6
+ * @returns A new Transaction instance
7
+ * @example
8
+ * // Basic transaction usage
9
+ * const tx = createTransaction({
10
+ * mutationFn: async ({ transaction }) => {
11
+ * // Send all mutations to API
12
+ * await api.saveChanges(transaction.mutations)
13
+ * }
14
+ * })
15
+ *
16
+ * tx.mutate(() => {
17
+ * collection.insert({ id: "1", text: "Buy milk" })
18
+ * collection.update("2", draft => { draft.completed = true })
19
+ * })
20
+ *
21
+ * await tx.isPersisted.promise
22
+ *
23
+ * @example
24
+ * // Handle transaction errors
25
+ * try {
26
+ * const tx = createTransaction({
27
+ * mutationFn: async () => { throw new Error("API failed") }
28
+ * })
29
+ *
30
+ * tx.mutate(() => {
31
+ * collection.insert({ id: "1", text: "New item" })
32
+ * })
33
+ *
34
+ * await tx.isPersisted.promise
35
+ * } catch (error) {
36
+ * console.log('Transaction failed:', error)
37
+ * }
38
+ *
39
+ * @example
40
+ * // Manual commit control
41
+ * const tx = createTransaction({
42
+ * autoCommit: false,
43
+ * mutationFn: async () => {
44
+ * // API call
45
+ * }
46
+ * })
47
+ *
48
+ * tx.mutate(() => {
49
+ * collection.insert({ id: "1", text: "Item" })
50
+ * })
51
+ *
52
+ * // Commit later
53
+ * await tx.commit()
54
+ */
3
55
  export declare function createTransaction<TData extends object = Record<string, unknown>>(config: TransactionConfig<TData>): Transaction<TData>;
56
+ /**
57
+ * Gets the currently active ambient transaction, if any
58
+ * Used internally by collection operations to join existing transactions
59
+ * @returns The active transaction or undefined if none is active
60
+ * @example
61
+ * // Check if operations will join an ambient transaction
62
+ * const ambientTx = getActiveTransaction()
63
+ * if (ambientTx) {
64
+ * console.log('Operations will join transaction:', ambientTx.id)
65
+ * }
66
+ */
4
67
  export declare function getActiveTransaction(): Transaction | undefined;
5
68
  declare class Transaction<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> {
6
69
  id: string;
@@ -18,12 +81,128 @@ declare class Transaction<T extends object = Record<string, unknown>, TOperation
18
81
  };
19
82
  constructor(config: TransactionConfig<T>);
20
83
  setState(newState: TransactionState): void;
84
+ /**
85
+ * Execute collection operations within this transaction
86
+ * @param callback - Function containing collection operations to group together
87
+ * @returns This transaction for chaining
88
+ * @example
89
+ * // Group multiple operations
90
+ * const tx = createTransaction({ mutationFn: async () => {
91
+ * // Send to API
92
+ * }})
93
+ *
94
+ * tx.mutate(() => {
95
+ * collection.insert({ id: "1", text: "Buy milk" })
96
+ * collection.update("2", draft => { draft.completed = true })
97
+ * collection.delete("3")
98
+ * })
99
+ *
100
+ * await tx.isPersisted.promise
101
+ *
102
+ * @example
103
+ * // Handle mutate errors
104
+ * try {
105
+ * tx.mutate(() => {
106
+ * collection.insert({ id: "invalid" }) // This might throw
107
+ * })
108
+ * } catch (error) {
109
+ * console.log('Mutation failed:', error)
110
+ * }
111
+ *
112
+ * @example
113
+ * // Manual commit control
114
+ * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })
115
+ *
116
+ * tx.mutate(() => {
117
+ * collection.insert({ id: "1", text: "Item" })
118
+ * })
119
+ *
120
+ * // Commit later when ready
121
+ * await tx.commit()
122
+ */
21
123
  mutate(callback: () => void): Transaction<T>;
22
124
  applyMutations(mutations: Array<PendingMutation<any>>): void;
125
+ /**
126
+ * Rollback the transaction and any conflicting transactions
127
+ * @param config - Configuration for rollback behavior
128
+ * @returns This transaction for chaining
129
+ * @example
130
+ * // Manual rollback
131
+ * const tx = createTransaction({ mutationFn: async () => {
132
+ * // Send to API
133
+ * }})
134
+ *
135
+ * tx.mutate(() => {
136
+ * collection.insert({ id: "1", text: "Buy milk" })
137
+ * })
138
+ *
139
+ * // Rollback if needed
140
+ * if (shouldCancel) {
141
+ * tx.rollback()
142
+ * }
143
+ *
144
+ * @example
145
+ * // Handle rollback cascade (automatic)
146
+ * const tx1 = createTransaction({ mutationFn: async () => {} })
147
+ * const tx2 = createTransaction({ mutationFn: async () => {} })
148
+ *
149
+ * tx1.mutate(() => collection.update("1", draft => { draft.value = "A" }))
150
+ * tx2.mutate(() => collection.update("1", draft => { draft.value = "B" })) // Same item
151
+ *
152
+ * tx1.rollback() // This will also rollback tx2 due to conflict
153
+ *
154
+ * @example
155
+ * // Handle rollback in error scenarios
156
+ * try {
157
+ * await tx.isPersisted.promise
158
+ * } catch (error) {
159
+ * console.log('Transaction was rolled back:', error)
160
+ * // Transaction automatically rolled back on mutation function failure
161
+ * }
162
+ */
23
163
  rollback(config?: {
24
164
  isSecondaryRollback?: boolean;
25
165
  }): Transaction<T>;
26
166
  touchCollection(): void;
167
+ /**
168
+ * Commit the transaction and execute the mutation function
169
+ * @returns Promise that resolves to this transaction when complete
170
+ * @example
171
+ * // Manual commit (when autoCommit is false)
172
+ * const tx = createTransaction({
173
+ * autoCommit: false,
174
+ * mutationFn: async ({ transaction }) => {
175
+ * await api.saveChanges(transaction.mutations)
176
+ * }
177
+ * })
178
+ *
179
+ * tx.mutate(() => {
180
+ * collection.insert({ id: "1", text: "Buy milk" })
181
+ * })
182
+ *
183
+ * await tx.commit() // Manually commit
184
+ *
185
+ * @example
186
+ * // Handle commit errors
187
+ * try {
188
+ * const tx = createTransaction({
189
+ * mutationFn: async () => { throw new Error("API failed") }
190
+ * })
191
+ *
192
+ * tx.mutate(() => {
193
+ * collection.insert({ id: "1", text: "Item" })
194
+ * })
195
+ *
196
+ * await tx.commit()
197
+ * } catch (error) {
198
+ * console.log('Commit failed, transaction rolled back:', error)
199
+ * }
200
+ *
201
+ * @example
202
+ * // Check transaction state after commit
203
+ * await tx.commit()
204
+ * console.log(tx.state) // "completed" or "failed"
205
+ */
27
206
  commit(): Promise<Transaction<T>>;
28
207
  /**
29
208
  * Compare two transactions by their createdAt time and sequence number in order
@@ -47,6 +47,45 @@ class Transaction {
47
47
  removeFromPendingList(this);
48
48
  }
49
49
  }
50
+ /**
51
+ * Execute collection operations within this transaction
52
+ * @param callback - Function containing collection operations to group together
53
+ * @returns This transaction for chaining
54
+ * @example
55
+ * // Group multiple operations
56
+ * const tx = createTransaction({ mutationFn: async () => {
57
+ * // Send to API
58
+ * }})
59
+ *
60
+ * tx.mutate(() => {
61
+ * collection.insert({ id: "1", text: "Buy milk" })
62
+ * collection.update("2", draft => { draft.completed = true })
63
+ * collection.delete("3")
64
+ * })
65
+ *
66
+ * await tx.isPersisted.promise
67
+ *
68
+ * @example
69
+ * // Handle mutate errors
70
+ * try {
71
+ * tx.mutate(() => {
72
+ * collection.insert({ id: "invalid" }) // This might throw
73
+ * })
74
+ * } catch (error) {
75
+ * console.log('Mutation failed:', error)
76
+ * }
77
+ *
78
+ * @example
79
+ * // Manual commit control
80
+ * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })
81
+ *
82
+ * tx.mutate(() => {
83
+ * collection.insert({ id: "1", text: "Item" })
84
+ * })
85
+ *
86
+ * // Commit later when ready
87
+ * await tx.commit()
88
+ */
50
89
  mutate(callback) {
51
90
  if (this.state !== `pending`) {
52
91
  throw `You can no longer call .mutate() as the transaction is no longer pending`;
@@ -74,6 +113,44 @@ class Transaction {
74
113
  }
75
114
  }
76
115
  }
116
+ /**
117
+ * Rollback the transaction and any conflicting transactions
118
+ * @param config - Configuration for rollback behavior
119
+ * @returns This transaction for chaining
120
+ * @example
121
+ * // Manual rollback
122
+ * const tx = createTransaction({ mutationFn: async () => {
123
+ * // Send to API
124
+ * }})
125
+ *
126
+ * tx.mutate(() => {
127
+ * collection.insert({ id: "1", text: "Buy milk" })
128
+ * })
129
+ *
130
+ * // Rollback if needed
131
+ * if (shouldCancel) {
132
+ * tx.rollback()
133
+ * }
134
+ *
135
+ * @example
136
+ * // Handle rollback cascade (automatic)
137
+ * const tx1 = createTransaction({ mutationFn: async () => {} })
138
+ * const tx2 = createTransaction({ mutationFn: async () => {} })
139
+ *
140
+ * tx1.mutate(() => collection.update("1", draft => { draft.value = "A" }))
141
+ * tx2.mutate(() => collection.update("1", draft => { draft.value = "B" })) // Same item
142
+ *
143
+ * tx1.rollback() // This will also rollback tx2 due to conflict
144
+ *
145
+ * @example
146
+ * // Handle rollback in error scenarios
147
+ * try {
148
+ * await tx.isPersisted.promise
149
+ * } catch (error) {
150
+ * console.log('Transaction was rolled back:', error)
151
+ * // Transaction automatically rolled back on mutation function failure
152
+ * }
153
+ */
77
154
  rollback(config) {
78
155
  var _a;
79
156
  const isSecondaryRollback = (config == null ? void 0 : config.isSecondaryRollback) ?? false;
@@ -105,6 +182,45 @@ class Transaction {
105
182
  }
106
183
  }
107
184
  }
185
+ /**
186
+ * Commit the transaction and execute the mutation function
187
+ * @returns Promise that resolves to this transaction when complete
188
+ * @example
189
+ * // Manual commit (when autoCommit is false)
190
+ * const tx = createTransaction({
191
+ * autoCommit: false,
192
+ * mutationFn: async ({ transaction }) => {
193
+ * await api.saveChanges(transaction.mutations)
194
+ * }
195
+ * })
196
+ *
197
+ * tx.mutate(() => {
198
+ * collection.insert({ id: "1", text: "Buy milk" })
199
+ * })
200
+ *
201
+ * await tx.commit() // Manually commit
202
+ *
203
+ * @example
204
+ * // Handle commit errors
205
+ * try {
206
+ * const tx = createTransaction({
207
+ * mutationFn: async () => { throw new Error("API failed") }
208
+ * })
209
+ *
210
+ * tx.mutate(() => {
211
+ * collection.insert({ id: "1", text: "Item" })
212
+ * })
213
+ *
214
+ * await tx.commit()
215
+ * } catch (error) {
216
+ * console.log('Commit failed, transaction rolled back:', error)
217
+ * }
218
+ *
219
+ * @example
220
+ * // Check transaction state after commit
221
+ * await tx.commit()
222
+ * console.log(tx.state) // "completed" or "failed"
223
+ */
108
224
  async commit() {
109
225
  if (this.state !== `pending`) {
110
226
  throw `You can no longer call .commit() as the transaction is no longer pending`;
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n OperationType,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\nexport function createTransaction<\n TData extends object = Record<string, unknown>,\n>(config: TransactionConfig<TData>): Transaction<TData> {\n const newTransaction = new Transaction<TData>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<\n T extends object = Record<string, unknown>,\n TOperation extends OperationType = OperationType,\n> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T, TOperation>>\n public isPersisted: Deferred<Transaction<T, TOperation>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T, TOperation>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @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\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n OperationType,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<\n TData extends object = Record<string, unknown>,\n>(config: TransactionConfig<TData>): Transaction<TData> {\n const newTransaction = new Transaction<TData>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<\n T extends object = Record<string, unknown>,\n TOperation extends OperationType = OperationType,\n> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T, TOperation>>\n public isPersisted: Deferred<Transaction<T, TOperation>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T, TOperation>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @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\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
@@ -44,7 +44,7 @@ export interface PendingMutation<T extends object = Record<string, unknown>, TOp
44
44
  syncMetadata: Record<string, unknown>;
45
45
  createdAt: Date;
46
46
  updatedAt: Date;
47
- collection: Collection<T, any>;
47
+ collection: Collection<T, any, any>;
48
48
  }
49
49
  /**
50
50
  * Configuration options for creating a new transaction
@@ -140,20 +140,35 @@ export interface OperationConfig {
140
140
  export interface InsertConfig {
141
141
  metadata?: Record<string, unknown>;
142
142
  }
143
- export type UpdateMutationFnParams<T extends object = Record<string, unknown>> = {
143
+ export type UpdateMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = {
144
144
  transaction: TransactionWithMutations<T, `update`>;
145
+ collection: Collection<T, TKey, TUtils>;
145
146
  };
146
- export type InsertMutationFnParams<T extends object = Record<string, unknown>> = {
147
+ export type InsertMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = {
147
148
  transaction: TransactionWithMutations<T, `insert`>;
149
+ collection: Collection<T, TKey, TUtils>;
148
150
  };
149
- export type DeleteMutationFnParams<T extends object = Record<string, unknown>> = {
151
+ export type DeleteMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = {
150
152
  transaction: TransactionWithMutations<T, `delete`>;
153
+ collection: Collection<T, TKey, TUtils>;
151
154
  };
152
- export type InsertMutationFn<T extends object = Record<string, unknown>> = (params: InsertMutationFnParams<T>) => Promise<any>;
153
- export type UpdateMutationFn<T extends object = Record<string, unknown>> = (params: UpdateMutationFnParams<T>) => Promise<any>;
154
- export type DeleteMutationFn<T extends object = Record<string, unknown>> = (params: DeleteMutationFnParams<T>) => Promise<any>;
155
+ export type InsertMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<any>;
156
+ export type UpdateMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<any>;
157
+ export type DeleteMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<any>;
155
158
  /**
156
159
  * Collection status values for lifecycle management
160
+ * @example
161
+ * // Check collection status
162
+ * if (collection.status === "loading") {
163
+ * console.log("Collection is loading initial data")
164
+ * } else if (collection.status === "ready") {
165
+ * console.log("Collection is ready for use")
166
+ * }
167
+ *
168
+ * @example
169
+ * // Status transitions
170
+ * // idle → loading → initialCommit → ready
171
+ * // Any status can transition to → error or cleaned-up
157
172
  */
158
173
  export type CollectionStatus =
159
174
  /** Collection is created but sync hasn't started yet (when startSync config is false) */
@@ -205,22 +220,132 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
205
220
  compare?: (x: T, y: T) => number;
206
221
  /**
207
222
  * Optional asynchronous handler function called before an insert operation
208
- * @param params Object containing transaction and mutation information
223
+ * @param params Object containing transaction and collection information
209
224
  * @returns Promise resolving to any value
225
+ * @example
226
+ * // Basic insert handler
227
+ * onInsert: async ({ transaction, collection }) => {
228
+ * const newItem = transaction.mutations[0].modified
229
+ * await api.createTodo(newItem)
230
+ * }
231
+ *
232
+ * @example
233
+ * // Insert handler with multiple items
234
+ * onInsert: async ({ transaction, collection }) => {
235
+ * const items = transaction.mutations.map(m => m.modified)
236
+ * await api.createTodos(items)
237
+ * }
238
+ *
239
+ * @example
240
+ * // Insert handler with error handling
241
+ * onInsert: async ({ transaction, collection }) => {
242
+ * try {
243
+ * const newItem = transaction.mutations[0].modified
244
+ * const result = await api.createTodo(newItem)
245
+ * return result
246
+ * } catch (error) {
247
+ * console.error('Insert failed:', error)
248
+ * throw error // This will cause the transaction to fail
249
+ * }
250
+ * }
251
+ *
252
+ * @example
253
+ * // Insert handler with metadata
254
+ * onInsert: async ({ transaction, collection }) => {
255
+ * const mutation = transaction.mutations[0]
256
+ * await api.createTodo(mutation.modified, {
257
+ * source: mutation.metadata?.source,
258
+ * timestamp: mutation.createdAt
259
+ * })
260
+ * }
210
261
  */
211
- onInsert?: InsertMutationFn<T>;
262
+ onInsert?: InsertMutationFn<T, TKey>;
212
263
  /**
213
264
  * Optional asynchronous handler function called before an update operation
214
- * @param params Object containing transaction and mutation information
265
+ * @param params Object containing transaction and collection information
215
266
  * @returns Promise resolving to any value
267
+ * @example
268
+ * // Basic update handler
269
+ * onUpdate: async ({ transaction, collection }) => {
270
+ * const updatedItem = transaction.mutations[0].modified
271
+ * await api.updateTodo(updatedItem.id, updatedItem)
272
+ * }
273
+ *
274
+ * @example
275
+ * // Update handler with partial updates
276
+ * onUpdate: async ({ transaction, collection }) => {
277
+ * const mutation = transaction.mutations[0]
278
+ * const changes = mutation.changes // Only the changed fields
279
+ * await api.updateTodo(mutation.original.id, changes)
280
+ * }
281
+ *
282
+ * @example
283
+ * // Update handler with multiple items
284
+ * onUpdate: async ({ transaction, collection }) => {
285
+ * const updates = transaction.mutations.map(m => ({
286
+ * id: m.key,
287
+ * changes: m.changes
288
+ * }))
289
+ * await api.updateTodos(updates)
290
+ * }
291
+ *
292
+ * @example
293
+ * // Update handler with optimistic rollback
294
+ * onUpdate: async ({ transaction, collection }) => {
295
+ * const mutation = transaction.mutations[0]
296
+ * try {
297
+ * await api.updateTodo(mutation.original.id, mutation.changes)
298
+ * } catch (error) {
299
+ * // Transaction will automatically rollback optimistic changes
300
+ * console.error('Update failed, rolling back:', error)
301
+ * throw error
302
+ * }
303
+ * }
216
304
  */
217
- onUpdate?: UpdateMutationFn<T>;
305
+ onUpdate?: UpdateMutationFn<T, TKey>;
218
306
  /**
219
307
  * Optional asynchronous handler function called before a delete operation
220
- * @param params Object containing transaction and mutation information
308
+ * @param params Object containing transaction and collection information
221
309
  * @returns Promise resolving to any value
310
+ * @example
311
+ * // Basic delete handler
312
+ * onDelete: async ({ transaction, collection }) => {
313
+ * const deletedKey = transaction.mutations[0].key
314
+ * await api.deleteTodo(deletedKey)
315
+ * }
316
+ *
317
+ * @example
318
+ * // Delete handler with multiple items
319
+ * onDelete: async ({ transaction, collection }) => {
320
+ * const keysToDelete = transaction.mutations.map(m => m.key)
321
+ * await api.deleteTodos(keysToDelete)
322
+ * }
323
+ *
324
+ * @example
325
+ * // Delete handler with confirmation
326
+ * onDelete: async ({ transaction, collection }) => {
327
+ * const mutation = transaction.mutations[0]
328
+ * const shouldDelete = await confirmDeletion(mutation.original)
329
+ * if (!shouldDelete) {
330
+ * throw new Error('Delete cancelled by user')
331
+ * }
332
+ * await api.deleteTodo(mutation.original.id)
333
+ * }
334
+ *
335
+ * @example
336
+ * // Delete handler with optimistic rollback
337
+ * onDelete: async ({ transaction, collection }) => {
338
+ * const mutation = transaction.mutations[0]
339
+ * try {
340
+ * await api.deleteTodo(mutation.original.id)
341
+ * } catch (error) {
342
+ * // Transaction will automatically rollback optimistic changes
343
+ * console.error('Delete failed, rolling back:', error)
344
+ * throw error
345
+ * }
346
+ * }
222
347
  */
223
- onDelete?: DeleteMutationFn<T>;
348
+ onDelete?: DeleteMutationFn<T, TKey>;
224
349
  }
225
350
  export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
226
351
  /**
@@ -252,4 +377,35 @@ export type KeyedNamespacedRow = [unknown, NamespacedRow];
252
377
  * a `select` clause.
253
378
  */
254
379
  export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>;
380
+ /**
381
+ * Function type for listening to collection changes
382
+ * @param changes - Array of change messages describing what happened
383
+ * @example
384
+ * // Basic change listener
385
+ * const listener: ChangeListener = (changes) => {
386
+ * changes.forEach(change => {
387
+ * console.log(`${change.type}: ${change.key}`, change.value)
388
+ * })
389
+ * }
390
+ *
391
+ * collection.subscribeChanges(listener)
392
+ *
393
+ * @example
394
+ * // Handle different change types
395
+ * const listener: ChangeListener<Todo> = (changes) => {
396
+ * for (const change of changes) {
397
+ * switch (change.type) {
398
+ * case 'insert':
399
+ * addToUI(change.value)
400
+ * break
401
+ * case 'update':
402
+ * updateInUI(change.key, change.value, change.previousValue)
403
+ * break
404
+ * case 'delete':
405
+ * removeFromUI(change.key)
406
+ * break
407
+ * }
408
+ * }
409
+ * }
410
+ */
255
411
  export type ChangeListener<T extends object = Record<string, unknown>, TKey extends string | number = string | number> = (changes: Array<ChangeMessage<T, TKey>>) => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.0.17",
4
+ "version": "0.0.19",
5
5
  "dependencies": {
6
6
  "@electric-sql/d2mini": "^0.1.6",
7
7
  "@standard-schema/spec": "^1.0.0"