@tanstack/db 0.0.16 → 0.0.18
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.
- package/dist/cjs/SortedMap.cjs.map +1 -1
- package/dist/cjs/collection.cjs +53 -8
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +150 -45
- package/dist/cjs/deferred.cjs.map +1 -1
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/optimistic-action.cjs.map +1 -1
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.cjs +1 -1
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/transactions.cjs +116 -0
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +179 -0
- package/dist/cjs/types.d.cts +170 -12
- package/dist/esm/SortedMap.js.map +1 -1
- package/dist/esm/collection.d.ts +150 -45
- package/dist/esm/collection.js +53 -8
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/deferred.js.map +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/optimistic-action.js.map +1 -1
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live-query-collection.js +1 -1
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.d.ts +179 -0
- package/dist/esm/transactions.js +116 -0
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +170 -12
- package/package.json +2 -2
- package/src/collection.ts +175 -55
- package/src/proxy.ts +0 -1
- package/src/query/live-query-collection.ts +2 -1
- package/src/transactions.ts +179 -0
- package/src/types.ts +200 -27
package/src/collection.ts
CHANGED
|
@@ -53,12 +53,52 @@ export interface Collection<
|
|
|
53
53
|
* @returns A new Collection with utilities exposed both at top level and under .utils
|
|
54
54
|
*
|
|
55
55
|
* @example
|
|
56
|
-
* //
|
|
57
|
-
* const todos = createCollection
|
|
56
|
+
* // Pattern 1: With operation handlers (direct collection calls)
|
|
57
|
+
* const todos = createCollection({
|
|
58
|
+
* id: "todos",
|
|
58
59
|
* getKey: (todo) => todo.id,
|
|
60
|
+
* schema,
|
|
61
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
62
|
+
* // Send to API
|
|
63
|
+
* await api.createTodo(transaction.mutations[0].modified)
|
|
64
|
+
* },
|
|
65
|
+
* onUpdate: async ({ transaction, collection }) => {
|
|
66
|
+
* await api.updateTodo(transaction.mutations[0].modified)
|
|
67
|
+
* },
|
|
68
|
+
* onDelete: async ({ transaction, collection }) => {
|
|
69
|
+
* await api.deleteTodo(transaction.mutations[0].key)
|
|
70
|
+
* },
|
|
59
71
|
* sync: { sync: () => {} }
|
|
60
72
|
* })
|
|
61
73
|
*
|
|
74
|
+
* // Direct usage (handlers manage transactions)
|
|
75
|
+
* const tx = todos.insert({ id: "1", text: "Buy milk", completed: false })
|
|
76
|
+
* await tx.isPersisted.promise
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // Pattern 2: Manual transaction management
|
|
80
|
+
* const todos = createCollection({
|
|
81
|
+
* getKey: (todo) => todo.id,
|
|
82
|
+
* schema: todoSchema,
|
|
83
|
+
* sync: { sync: () => {} }
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* // Explicit transaction usage
|
|
87
|
+
* const tx = createTransaction({
|
|
88
|
+
* mutationFn: async ({ transaction }) => {
|
|
89
|
+
* // Handle all mutations in transaction
|
|
90
|
+
* await api.saveChanges(transaction.mutations)
|
|
91
|
+
* }
|
|
92
|
+
* })
|
|
93
|
+
*
|
|
94
|
+
* tx.mutate(() => {
|
|
95
|
+
* todos.insert({ id: "1", text: "Buy milk" })
|
|
96
|
+
* todos.update("2", draft => { draft.completed = true })
|
|
97
|
+
* })
|
|
98
|
+
*
|
|
99
|
+
* await tx.isPersisted.promise
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
62
102
|
* // Using schema for type inference (preferred as it also gives you client side validation)
|
|
63
103
|
* const todoSchema = z.object({
|
|
64
104
|
* id: z.string(),
|
|
@@ -72,7 +112,7 @@ export interface Collection<
|
|
|
72
112
|
* sync: { sync: () => {} }
|
|
73
113
|
* })
|
|
74
114
|
*
|
|
75
|
-
* // Note: You must provide either an explicit type or a schema, but not both
|
|
115
|
+
* // Note: You must provide either an explicit type or a schema, but not both.
|
|
76
116
|
*/
|
|
77
117
|
export function createCollection<
|
|
78
118
|
TExplicit = unknown,
|
|
@@ -138,6 +178,7 @@ export class SchemaValidationError extends Error {
|
|
|
138
178
|
export class CollectionImpl<
|
|
139
179
|
T extends object = Record<string, unknown>,
|
|
140
180
|
TKey extends string | number = string | number,
|
|
181
|
+
TUtils extends UtilsRecord = {},
|
|
141
182
|
> {
|
|
142
183
|
public config: CollectionConfig<T, TKey, any>
|
|
143
184
|
|
|
@@ -187,6 +228,11 @@ export class CollectionImpl<
|
|
|
187
228
|
* Register a callback to be executed on the next commit
|
|
188
229
|
* Useful for preloading collections
|
|
189
230
|
* @param callback Function to call after the next commit
|
|
231
|
+
* @example
|
|
232
|
+
* collection.onFirstCommit(() => {
|
|
233
|
+
* console.log('Collection has received first data')
|
|
234
|
+
* // Safe to access collection.state now
|
|
235
|
+
* })
|
|
190
236
|
*/
|
|
191
237
|
public onFirstCommit(callback: () => void): void {
|
|
192
238
|
this.onFirstCommitCallbacks.push(callback)
|
|
@@ -237,7 +283,8 @@ export class CollectionImpl<
|
|
|
237
283
|
Array<CollectionStatus>
|
|
238
284
|
> = {
|
|
239
285
|
idle: [`loading`, `error`, `cleaned-up`],
|
|
240
|
-
loading: [`
|
|
286
|
+
loading: [`initialCommit`, `error`, `cleaned-up`],
|
|
287
|
+
initialCommit: [`ready`, `error`, `cleaned-up`],
|
|
241
288
|
ready: [`cleaned-up`, `error`],
|
|
242
289
|
error: [`cleaned-up`, `idle`],
|
|
243
290
|
"cleaned-up": [`loading`, `error`],
|
|
@@ -382,14 +429,18 @@ export class CollectionImpl<
|
|
|
382
429
|
|
|
383
430
|
pendingTransaction.committed = true
|
|
384
431
|
|
|
385
|
-
// Update status to
|
|
386
|
-
//
|
|
387
|
-
// be from a "ready" state.
|
|
432
|
+
// Update status to initialCommit when transitioning from loading
|
|
433
|
+
// This indicates we're in the process of committing the first transaction
|
|
388
434
|
if (this._status === `loading`) {
|
|
389
|
-
this.setStatus(`
|
|
435
|
+
this.setStatus(`initialCommit`)
|
|
390
436
|
}
|
|
391
437
|
|
|
392
438
|
this.commitPendingTransactions()
|
|
439
|
+
|
|
440
|
+
// Transition from initialCommit to ready after the first commit is complete
|
|
441
|
+
if (this._status === `initialCommit`) {
|
|
442
|
+
this.setStatus(`ready`)
|
|
443
|
+
}
|
|
393
444
|
},
|
|
394
445
|
})
|
|
395
446
|
|
|
@@ -1238,21 +1289,38 @@ export class CollectionImpl<
|
|
|
1238
1289
|
/**
|
|
1239
1290
|
* Inserts one or more items into the collection
|
|
1240
1291
|
* @param items - Single item or array of items to insert
|
|
1241
|
-
* @param config - Optional configuration including metadata
|
|
1242
|
-
* @returns A
|
|
1292
|
+
* @param config - Optional configuration including metadata
|
|
1293
|
+
* @returns A Transaction object representing the insert operation(s)
|
|
1243
1294
|
* @throws {SchemaValidationError} If the data fails schema validation
|
|
1244
1295
|
* @example
|
|
1245
|
-
* // Insert a single
|
|
1246
|
-
* insert({ text: "Buy
|
|
1296
|
+
* // Insert a single todo (requires onInsert handler)
|
|
1297
|
+
* const tx = collection.insert({ id: "1", text: "Buy milk", completed: false })
|
|
1298
|
+
* await tx.isPersisted.promise
|
|
1247
1299
|
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
* { text: "
|
|
1300
|
+
* @example
|
|
1301
|
+
* // Insert multiple todos at once
|
|
1302
|
+
* const tx = collection.insert([
|
|
1303
|
+
* { id: "1", text: "Buy milk", completed: false },
|
|
1304
|
+
* { id: "2", text: "Walk dog", completed: true }
|
|
1252
1305
|
* ])
|
|
1306
|
+
* await tx.isPersisted.promise
|
|
1307
|
+
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* // Insert with metadata
|
|
1310
|
+
* const tx = collection.insert({ id: "1", text: "Buy groceries" },
|
|
1311
|
+
* { metadata: { source: "mobile-app" } }
|
|
1312
|
+
* )
|
|
1313
|
+
* await tx.isPersisted.promise
|
|
1253
1314
|
*
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1315
|
+
* @example
|
|
1316
|
+
* // Handle errors
|
|
1317
|
+
* try {
|
|
1318
|
+
* const tx = collection.insert({ id: "1", text: "New item" })
|
|
1319
|
+
* await tx.isPersisted.promise
|
|
1320
|
+
* console.log('Insert successful')
|
|
1321
|
+
* } catch (error) {
|
|
1322
|
+
* console.log('Insert failed:', error)
|
|
1323
|
+
* }
|
|
1256
1324
|
*/
|
|
1257
1325
|
insert = (data: T | Array<T>, config?: InsertConfig) => {
|
|
1258
1326
|
this.validateCollectionUsable(`insert`)
|
|
@@ -1311,8 +1379,11 @@ export class CollectionImpl<
|
|
|
1311
1379
|
// Create a new transaction with a mutation function that calls the onInsert handler
|
|
1312
1380
|
const directOpTransaction = createTransaction<T>({
|
|
1313
1381
|
mutationFn: async (params) => {
|
|
1314
|
-
// Call the onInsert handler with the transaction
|
|
1315
|
-
return this.config.onInsert!(
|
|
1382
|
+
// Call the onInsert handler with the transaction and collection
|
|
1383
|
+
return this.config.onInsert!({
|
|
1384
|
+
...params,
|
|
1385
|
+
collection: this as unknown as Collection<T, TKey, TUtils>,
|
|
1386
|
+
})
|
|
1316
1387
|
},
|
|
1317
1388
|
})
|
|
1318
1389
|
|
|
@@ -1330,43 +1401,44 @@ export class CollectionImpl<
|
|
|
1330
1401
|
|
|
1331
1402
|
/**
|
|
1332
1403
|
* Updates one or more items in the collection using a callback function
|
|
1333
|
-
* @param
|
|
1404
|
+
* @param keys - Single key or array of keys to update
|
|
1334
1405
|
* @param configOrCallback - Either update configuration or update callback
|
|
1335
1406
|
* @param maybeCallback - Update callback if config was provided
|
|
1336
1407
|
* @returns A Transaction object representing the update operation(s)
|
|
1337
1408
|
* @throws {SchemaValidationError} If the updated data fails schema validation
|
|
1338
1409
|
* @example
|
|
1339
|
-
* // Update
|
|
1340
|
-
* update(todo, (draft) => {
|
|
1341
|
-
*
|
|
1342
|
-
* // Update multiple items
|
|
1343
|
-
* update([todo1, todo2], (drafts) => {
|
|
1344
|
-
* drafts.forEach(draft => { draft.completed = true })
|
|
1410
|
+
* // Update single item by key
|
|
1411
|
+
* const tx = collection.update("todo-1", (draft) => {
|
|
1412
|
+
* draft.completed = true
|
|
1345
1413
|
* })
|
|
1414
|
+
* await tx.isPersisted.promise
|
|
1346
1415
|
*
|
|
1347
|
-
* // Update with metadata
|
|
1348
|
-
* update(todo, { metadata: { reason: "user update" } }, (draft) => { draft.text = "Updated text" })
|
|
1349
|
-
*/
|
|
1350
|
-
|
|
1351
|
-
/**
|
|
1352
|
-
* Updates one or more items in the collection using a callback function
|
|
1353
|
-
* @param ids - Single ID or array of IDs to update
|
|
1354
|
-
* @param configOrCallback - Either update configuration or update callback
|
|
1355
|
-
* @param maybeCallback - Update callback if config was provided
|
|
1356
|
-
* @returns A Transaction object representing the update operation(s)
|
|
1357
|
-
* @throws {SchemaValidationError} If the updated data fails schema validation
|
|
1358
1416
|
* @example
|
|
1359
|
-
* // Update a single item
|
|
1360
|
-
* update("todo-1", (draft) => { draft.completed = true })
|
|
1361
|
-
*
|
|
1362
1417
|
* // Update multiple items
|
|
1363
|
-
* update(["todo-1", "todo-2"], (drafts) => {
|
|
1418
|
+
* const tx = collection.update(["todo-1", "todo-2"], (drafts) => {
|
|
1364
1419
|
* drafts.forEach(draft => { draft.completed = true })
|
|
1365
1420
|
* })
|
|
1421
|
+
* await tx.isPersisted.promise
|
|
1366
1422
|
*
|
|
1423
|
+
* @example
|
|
1367
1424
|
* // Update with metadata
|
|
1368
|
-
* update("todo-1",
|
|
1425
|
+
* const tx = collection.update("todo-1",
|
|
1426
|
+
* { metadata: { reason: "user update" } },
|
|
1427
|
+
* (draft) => { draft.text = "Updated text" }
|
|
1428
|
+
* )
|
|
1429
|
+
* await tx.isPersisted.promise
|
|
1430
|
+
*
|
|
1431
|
+
* @example
|
|
1432
|
+
* // Handle errors
|
|
1433
|
+
* try {
|
|
1434
|
+
* const tx = collection.update("item-1", draft => { draft.value = "new" })
|
|
1435
|
+
* await tx.isPersisted.promise
|
|
1436
|
+
* console.log('Update successful')
|
|
1437
|
+
* } catch (error) {
|
|
1438
|
+
* console.log('Update failed:', error)
|
|
1439
|
+
* }
|
|
1369
1440
|
*/
|
|
1441
|
+
|
|
1370
1442
|
// Overload 1: Update multiple items with a callback
|
|
1371
1443
|
update<TItem extends object = T>(
|
|
1372
1444
|
key: Array<TKey | unknown>,
|
|
@@ -1533,8 +1605,11 @@ export class CollectionImpl<
|
|
|
1533
1605
|
// Create a new transaction with a mutation function that calls the onUpdate handler
|
|
1534
1606
|
const directOpTransaction = createTransaction<T>({
|
|
1535
1607
|
mutationFn: async (params) => {
|
|
1536
|
-
// Call the onUpdate handler with the transaction
|
|
1537
|
-
return this.config.onUpdate!(
|
|
1608
|
+
// Call the onUpdate handler with the transaction and collection
|
|
1609
|
+
return this.config.onUpdate!({
|
|
1610
|
+
...params,
|
|
1611
|
+
collection: this as unknown as Collection<T, TKey, TUtils>,
|
|
1612
|
+
})
|
|
1538
1613
|
},
|
|
1539
1614
|
})
|
|
1540
1615
|
|
|
@@ -1552,18 +1627,33 @@ export class CollectionImpl<
|
|
|
1552
1627
|
|
|
1553
1628
|
/**
|
|
1554
1629
|
* Deletes one or more items from the collection
|
|
1555
|
-
* @param
|
|
1630
|
+
* @param keys - Single key or array of keys to delete
|
|
1556
1631
|
* @param config - Optional configuration including metadata
|
|
1557
|
-
* @returns A
|
|
1632
|
+
* @returns A Transaction object representing the delete operation(s)
|
|
1558
1633
|
* @example
|
|
1559
1634
|
* // Delete a single item
|
|
1560
|
-
* delete("todo-1")
|
|
1635
|
+
* const tx = collection.delete("todo-1")
|
|
1636
|
+
* await tx.isPersisted.promise
|
|
1561
1637
|
*
|
|
1638
|
+
* @example
|
|
1562
1639
|
* // Delete multiple items
|
|
1563
|
-
* delete(["todo-1", "todo-2"])
|
|
1640
|
+
* const tx = collection.delete(["todo-1", "todo-2"])
|
|
1641
|
+
* await tx.isPersisted.promise
|
|
1564
1642
|
*
|
|
1643
|
+
* @example
|
|
1565
1644
|
* // Delete with metadata
|
|
1566
|
-
* delete("todo-1", { metadata: { reason: "completed" } })
|
|
1645
|
+
* const tx = collection.delete("todo-1", { metadata: { reason: "completed" } })
|
|
1646
|
+
* await tx.isPersisted.promise
|
|
1647
|
+
*
|
|
1648
|
+
* @example
|
|
1649
|
+
* // Handle errors
|
|
1650
|
+
* try {
|
|
1651
|
+
* const tx = collection.delete("item-1")
|
|
1652
|
+
* await tx.isPersisted.promise
|
|
1653
|
+
* console.log('Delete successful')
|
|
1654
|
+
* } catch (error) {
|
|
1655
|
+
* console.log('Delete failed:', error)
|
|
1656
|
+
* }
|
|
1567
1657
|
*/
|
|
1568
1658
|
delete = (
|
|
1569
1659
|
keys: Array<TKey> | TKey,
|
|
@@ -1629,8 +1719,11 @@ export class CollectionImpl<
|
|
|
1629
1719
|
const directOpTransaction = createTransaction<T>({
|
|
1630
1720
|
autoCommit: true,
|
|
1631
1721
|
mutationFn: async (params) => {
|
|
1632
|
-
// Call the onDelete handler with the transaction
|
|
1633
|
-
return this.config.onDelete!(
|
|
1722
|
+
// Call the onDelete handler with the transaction and collection
|
|
1723
|
+
return this.config.onDelete!({
|
|
1724
|
+
...params,
|
|
1725
|
+
collection: this as unknown as Collection<T, TKey, TUtils>,
|
|
1726
|
+
})
|
|
1634
1727
|
},
|
|
1635
1728
|
})
|
|
1636
1729
|
|
|
@@ -1646,8 +1739,19 @@ export class CollectionImpl<
|
|
|
1646
1739
|
|
|
1647
1740
|
/**
|
|
1648
1741
|
* Gets the current state of the collection as a Map
|
|
1742
|
+
* @returns Map containing all items in the collection, with keys as identifiers
|
|
1743
|
+
* @example
|
|
1744
|
+
* const itemsMap = collection.state
|
|
1745
|
+
* console.log(`Collection has ${itemsMap.size} items`)
|
|
1746
|
+
*
|
|
1747
|
+
* for (const [key, item] of itemsMap) {
|
|
1748
|
+
* console.log(`${key}: ${item.title}`)
|
|
1749
|
+
* }
|
|
1649
1750
|
*
|
|
1650
|
-
*
|
|
1751
|
+
* // Check if specific item exists
|
|
1752
|
+
* if (itemsMap.has("todo-1")) {
|
|
1753
|
+
* console.log("Todo 1 exists:", itemsMap.get("todo-1"))
|
|
1754
|
+
* }
|
|
1651
1755
|
*/
|
|
1652
1756
|
get state() {
|
|
1653
1757
|
const result = new Map<TKey, T>()
|
|
@@ -1720,8 +1824,24 @@ export class CollectionImpl<
|
|
|
1720
1824
|
|
|
1721
1825
|
/**
|
|
1722
1826
|
* Subscribe to changes in the collection
|
|
1723
|
-
* @param callback -
|
|
1724
|
-
* @
|
|
1827
|
+
* @param callback - Function called when items change
|
|
1828
|
+
* @param options.includeInitialState - If true, immediately calls callback with current data
|
|
1829
|
+
* @returns Unsubscribe function - Call this to stop listening for changes
|
|
1830
|
+
* @example
|
|
1831
|
+
* // Basic subscription
|
|
1832
|
+
* const unsubscribe = collection.subscribeChanges((changes) => {
|
|
1833
|
+
* changes.forEach(change => {
|
|
1834
|
+
* console.log(`${change.type}: ${change.key}`, change.value)
|
|
1835
|
+
* })
|
|
1836
|
+
* })
|
|
1837
|
+
*
|
|
1838
|
+
* // Later: unsubscribe()
|
|
1839
|
+
*
|
|
1840
|
+
* @example
|
|
1841
|
+
* // Include current state immediately
|
|
1842
|
+
* const unsubscribe = collection.subscribeChanges((changes) => {
|
|
1843
|
+
* updateUI(changes)
|
|
1844
|
+
* }, { includeInitialState: true })
|
|
1725
1845
|
*/
|
|
1726
1846
|
public subscribeChanges(
|
|
1727
1847
|
callback: (changes: Array<ChangeMessage<T>>) => void,
|
package/src/proxy.ts
CHANGED
|
@@ -505,7 +505,6 @@ export function createChangeProxy<
|
|
|
505
505
|
if (typeof callback === `function`) {
|
|
506
506
|
// Replace the original callback with our wrapped version
|
|
507
507
|
const wrappedCallback = function (
|
|
508
|
-
// eslint-disable-next-line
|
|
509
508
|
this: unknown,
|
|
510
509
|
// eslint-disable-next-line
|
|
511
510
|
value: unknown,
|
|
@@ -162,7 +162,8 @@ export function liveQueryCollectionOptions<
|
|
|
162
162
|
|
|
163
163
|
const allCollectionsReady = () => {
|
|
164
164
|
return Object.values(collections).every(
|
|
165
|
-
(collection) =>
|
|
165
|
+
(collection) =>
|
|
166
|
+
collection.status === `ready` || collection.status === `initialCommit`
|
|
166
167
|
)
|
|
167
168
|
}
|
|
168
169
|
|
package/src/transactions.ts
CHANGED
|
@@ -14,6 +14,58 @@ let transactionStack: Array<Transaction<any>> = []
|
|
|
14
14
|
|
|
15
15
|
let sequenceNumber = 0
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new transaction for grouping multiple collection operations
|
|
19
|
+
* @param config - Transaction configuration with mutation function
|
|
20
|
+
* @returns A new Transaction instance
|
|
21
|
+
* @example
|
|
22
|
+
* // Basic transaction usage
|
|
23
|
+
* const tx = createTransaction({
|
|
24
|
+
* mutationFn: async ({ transaction }) => {
|
|
25
|
+
* // Send all mutations to API
|
|
26
|
+
* await api.saveChanges(transaction.mutations)
|
|
27
|
+
* }
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* tx.mutate(() => {
|
|
31
|
+
* collection.insert({ id: "1", text: "Buy milk" })
|
|
32
|
+
* collection.update("2", draft => { draft.completed = true })
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* await tx.isPersisted.promise
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Handle transaction errors
|
|
39
|
+
* try {
|
|
40
|
+
* const tx = createTransaction({
|
|
41
|
+
* mutationFn: async () => { throw new Error("API failed") }
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* tx.mutate(() => {
|
|
45
|
+
* collection.insert({ id: "1", text: "New item" })
|
|
46
|
+
* })
|
|
47
|
+
*
|
|
48
|
+
* await tx.isPersisted.promise
|
|
49
|
+
* } catch (error) {
|
|
50
|
+
* console.log('Transaction failed:', error)
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Manual commit control
|
|
55
|
+
* const tx = createTransaction({
|
|
56
|
+
* autoCommit: false,
|
|
57
|
+
* mutationFn: async () => {
|
|
58
|
+
* // API call
|
|
59
|
+
* }
|
|
60
|
+
* })
|
|
61
|
+
*
|
|
62
|
+
* tx.mutate(() => {
|
|
63
|
+
* collection.insert({ id: "1", text: "Item" })
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // Commit later
|
|
67
|
+
* await tx.commit()
|
|
68
|
+
*/
|
|
17
69
|
export function createTransaction<
|
|
18
70
|
TData extends object = Record<string, unknown>,
|
|
19
71
|
>(config: TransactionConfig<TData>): Transaction<TData> {
|
|
@@ -22,6 +74,17 @@ export function createTransaction<
|
|
|
22
74
|
return newTransaction
|
|
23
75
|
}
|
|
24
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Gets the currently active ambient transaction, if any
|
|
79
|
+
* Used internally by collection operations to join existing transactions
|
|
80
|
+
* @returns The active transaction or undefined if none is active
|
|
81
|
+
* @example
|
|
82
|
+
* // Check if operations will join an ambient transaction
|
|
83
|
+
* const ambientTx = getActiveTransaction()
|
|
84
|
+
* if (ambientTx) {
|
|
85
|
+
* console.log('Operations will join transaction:', ambientTx.id)
|
|
86
|
+
* }
|
|
87
|
+
*/
|
|
25
88
|
export function getActiveTransaction(): Transaction | undefined {
|
|
26
89
|
if (transactionStack.length > 0) {
|
|
27
90
|
return transactionStack.slice(-1)[0]
|
|
@@ -86,6 +149,45 @@ class Transaction<
|
|
|
86
149
|
}
|
|
87
150
|
}
|
|
88
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Execute collection operations within this transaction
|
|
154
|
+
* @param callback - Function containing collection operations to group together
|
|
155
|
+
* @returns This transaction for chaining
|
|
156
|
+
* @example
|
|
157
|
+
* // Group multiple operations
|
|
158
|
+
* const tx = createTransaction({ mutationFn: async () => {
|
|
159
|
+
* // Send to API
|
|
160
|
+
* }})
|
|
161
|
+
*
|
|
162
|
+
* tx.mutate(() => {
|
|
163
|
+
* collection.insert({ id: "1", text: "Buy milk" })
|
|
164
|
+
* collection.update("2", draft => { draft.completed = true })
|
|
165
|
+
* collection.delete("3")
|
|
166
|
+
* })
|
|
167
|
+
*
|
|
168
|
+
* await tx.isPersisted.promise
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* // Handle mutate errors
|
|
172
|
+
* try {
|
|
173
|
+
* tx.mutate(() => {
|
|
174
|
+
* collection.insert({ id: "invalid" }) // This might throw
|
|
175
|
+
* })
|
|
176
|
+
* } catch (error) {
|
|
177
|
+
* console.log('Mutation failed:', error)
|
|
178
|
+
* }
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* // Manual commit control
|
|
182
|
+
* const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })
|
|
183
|
+
*
|
|
184
|
+
* tx.mutate(() => {
|
|
185
|
+
* collection.insert({ id: "1", text: "Item" })
|
|
186
|
+
* })
|
|
187
|
+
*
|
|
188
|
+
* // Commit later when ready
|
|
189
|
+
* await tx.commit()
|
|
190
|
+
*/
|
|
89
191
|
mutate(callback: () => void): Transaction<T> {
|
|
90
192
|
if (this.state !== `pending`) {
|
|
91
193
|
throw `You can no longer call .mutate() as the transaction is no longer pending`
|
|
@@ -121,6 +223,44 @@ class Transaction<
|
|
|
121
223
|
}
|
|
122
224
|
}
|
|
123
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Rollback the transaction and any conflicting transactions
|
|
228
|
+
* @param config - Configuration for rollback behavior
|
|
229
|
+
* @returns This transaction for chaining
|
|
230
|
+
* @example
|
|
231
|
+
* // Manual rollback
|
|
232
|
+
* const tx = createTransaction({ mutationFn: async () => {
|
|
233
|
+
* // Send to API
|
|
234
|
+
* }})
|
|
235
|
+
*
|
|
236
|
+
* tx.mutate(() => {
|
|
237
|
+
* collection.insert({ id: "1", text: "Buy milk" })
|
|
238
|
+
* })
|
|
239
|
+
*
|
|
240
|
+
* // Rollback if needed
|
|
241
|
+
* if (shouldCancel) {
|
|
242
|
+
* tx.rollback()
|
|
243
|
+
* }
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* // Handle rollback cascade (automatic)
|
|
247
|
+
* const tx1 = createTransaction({ mutationFn: async () => {} })
|
|
248
|
+
* const tx2 = createTransaction({ mutationFn: async () => {} })
|
|
249
|
+
*
|
|
250
|
+
* tx1.mutate(() => collection.update("1", draft => { draft.value = "A" }))
|
|
251
|
+
* tx2.mutate(() => collection.update("1", draft => { draft.value = "B" })) // Same item
|
|
252
|
+
*
|
|
253
|
+
* tx1.rollback() // This will also rollback tx2 due to conflict
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* // Handle rollback in error scenarios
|
|
257
|
+
* try {
|
|
258
|
+
* await tx.isPersisted.promise
|
|
259
|
+
* } catch (error) {
|
|
260
|
+
* console.log('Transaction was rolled back:', error)
|
|
261
|
+
* // Transaction automatically rolled back on mutation function failure
|
|
262
|
+
* }
|
|
263
|
+
*/
|
|
124
264
|
rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {
|
|
125
265
|
const isSecondaryRollback = config?.isSecondaryRollback ?? false
|
|
126
266
|
if (this.state === `completed`) {
|
|
@@ -165,6 +305,45 @@ class Transaction<
|
|
|
165
305
|
}
|
|
166
306
|
}
|
|
167
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Commit the transaction and execute the mutation function
|
|
310
|
+
* @returns Promise that resolves to this transaction when complete
|
|
311
|
+
* @example
|
|
312
|
+
* // Manual commit (when autoCommit is false)
|
|
313
|
+
* const tx = createTransaction({
|
|
314
|
+
* autoCommit: false,
|
|
315
|
+
* mutationFn: async ({ transaction }) => {
|
|
316
|
+
* await api.saveChanges(transaction.mutations)
|
|
317
|
+
* }
|
|
318
|
+
* })
|
|
319
|
+
*
|
|
320
|
+
* tx.mutate(() => {
|
|
321
|
+
* collection.insert({ id: "1", text: "Buy milk" })
|
|
322
|
+
* })
|
|
323
|
+
*
|
|
324
|
+
* await tx.commit() // Manually commit
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* // Handle commit errors
|
|
328
|
+
* try {
|
|
329
|
+
* const tx = createTransaction({
|
|
330
|
+
* mutationFn: async () => { throw new Error("API failed") }
|
|
331
|
+
* })
|
|
332
|
+
*
|
|
333
|
+
* tx.mutate(() => {
|
|
334
|
+
* collection.insert({ id: "1", text: "Item" })
|
|
335
|
+
* })
|
|
336
|
+
*
|
|
337
|
+
* await tx.commit()
|
|
338
|
+
* } catch (error) {
|
|
339
|
+
* console.log('Commit failed, transaction rolled back:', error)
|
|
340
|
+
* }
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* // Check transaction state after commit
|
|
344
|
+
* await tx.commit()
|
|
345
|
+
* console.log(tx.state) // "completed" or "failed"
|
|
346
|
+
*/
|
|
168
347
|
async commit(): Promise<Transaction<T>> {
|
|
169
348
|
if (this.state !== `pending`) {
|
|
170
349
|
throw `You can no longer call .commit() as the transaction is no longer pending`
|