@tanstack/db 0.0.17 → 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 +47 -6
- 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.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 +168 -12
- package/dist/esm/SortedMap.js.map +1 -1
- package/dist/esm/collection.d.ts +150 -45
- package/dist/esm/collection.js +47 -6
- 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.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 +168 -12
- package/package.json +1 -1
- package/src/collection.ts +165 -50
- package/src/proxy.ts +0 -1
- package/src/transactions.ts +179 -0
- package/src/types.ts +198 -27
|
@@ -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
|
package/dist/esm/transactions.js
CHANGED
|
@@ -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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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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