@tanstack/electric-db-collection 0.4.10 → 0.5.1

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.
@@ -1,4 +1,4 @@
1
- import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncMode, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
1
+ import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncMode, UpdateMutationFnParams } from '@tanstack/db';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
3
  import { GetExtensions, Message, Row, ShapeStreamOptions } from '@electric-sql/client';
4
4
  export { isChangeMessage, isControlMessage } from '@electric-sql/client';
@@ -28,11 +28,11 @@ export type MatchFunction<T extends Row<unknown>> = (message: Message<T>) => boo
28
28
  /**
29
29
  * Matching strategies for Electric synchronization
30
30
  * Handlers can return:
31
- * - Txid strategy: { txid: number | number[], timeout?: number } (recommended)
31
+ * - Txid strategy: { txid: number | number[], timeout?: number } (deprecated compatibility behavior)
32
32
  * - Void (no return value) - mutation completes without waiting
33
33
  *
34
34
  * The optional timeout property specifies how long to wait for the txid(s) in milliseconds.
35
- * If not specified, defaults to 5000ms.
35
+ * If not specified, defaults to 15000ms.
36
36
  */
37
37
  export type MatchingStrategy = {
38
38
  txid: Txid | Array<Txid>;
@@ -61,7 +61,7 @@ export type ElectricSyncMode = SyncMode | `progressive`;
61
61
  * @template T - The type of items in the collection
62
62
  * @template TSchema - The schema type for validation
63
63
  */
64
- export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, ElectricCollectionUtils<T>, any>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> {
64
+ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, ElectricCollectionUtils<T>>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> {
65
65
  /**
66
66
  * Configuration options for the ElectricSQL ShapeStream
67
67
  */
@@ -74,43 +74,82 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
74
74
  [ELECTRIC_TEST_HOOKS]?: ElectricTestHooks;
75
75
  /**
76
76
  * Optional asynchronous handler function called before an insert operation
77
+ *
78
+ * **IMPORTANT - Electric Synchronization:**
79
+ * This handler **must not resolve** until synchronization is confirmed.
80
+ * Await one of these synchronization utilities before the handler completes:
81
+ * 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
82
+ * 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
83
+ *
84
+ * Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
85
+ *
77
86
  * @param params Object containing transaction and collection information
78
- * @returns Promise resolving to { txid, timeout? } or void
87
+ * @returns Promise that should resolve after synchronization is complete
88
+ *
89
+ * **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
90
+ * Use `await collection.utils.awaitTxId(txid)` within the handler instead.
91
+ *
79
92
  * @example
80
- * // Basic Electric insert handler with txid (recommended)
81
- * onInsert: async ({ transaction }) => {
93
+ * // Recommended: Wait for txid to sync
94
+ * onInsert: async ({ transaction, collection }) => {
82
95
  * const newItem = transaction.mutations[0].modified
83
96
  * const result = await api.todos.create({
84
97
  * data: newItem
85
98
  * })
86
- * return { txid: result.txid }
99
+ * // Wait for txid to sync before handler completes
100
+ * await collection.utils.awaitTxId(result.txid)
87
101
  * }
88
102
  *
89
103
  * @example
90
104
  * // Insert handler with custom timeout
91
- * onInsert: async ({ transaction }) => {
105
+ * onInsert: async ({ transaction, collection }) => {
106
+ * const newItem = transaction.mutations[0].modified
107
+ * const result = await api.todos.create({
108
+ * data: newItem
109
+ * })
110
+ * // Wait up to 10 seconds for txid
111
+ * await collection.utils.awaitTxId(result.txid, 10000)
112
+ * }
113
+ *
114
+ * @example
115
+ * // Insert handler with timeout error handling
116
+ * onInsert: async ({ transaction, collection }) => {
92
117
  * const newItem = transaction.mutations[0].modified
93
118
  * const result = await api.todos.create({
94
119
  * data: newItem
95
120
  * })
96
- * return { txid: result.txid, timeout: 10000 } // Wait up to 10 seconds
121
+ *
122
+ * try {
123
+ * await collection.utils.awaitTxId(result.txid, 5000)
124
+ * } catch (error) {
125
+ * // Decide sync timeout policy:
126
+ * // - Throw to rollback optimistic state
127
+ * // - Catch to keep optimistic state (eventual consistency)
128
+ * // - Schedule background retry
129
+ * console.warn('Sync timeout, keeping optimistic state:', error)
130
+ * // Don't throw - allow optimistic state to persist
131
+ * }
97
132
  * }
98
133
  *
99
134
  * @example
100
- * // Insert handler with multiple items - return array of txids
101
- * onInsert: async ({ transaction }) => {
135
+ * // Insert handler with multiple items
136
+ * onInsert: async ({ transaction, collection }) => {
102
137
  * const items = transaction.mutations.map(m => m.modified)
103
138
  * const results = await Promise.all(
104
139
  * items.map(item => api.todos.create({ data: item }))
105
140
  * )
106
- * return { txid: results.map(r => r.txid) }
141
+ * // Wait for all txids to sync
142
+ * await Promise.all(
143
+ * results.map(r => collection.utils.awaitTxId(r.txid))
144
+ * )
107
145
  * }
108
146
  *
109
147
  * @example
110
- * // Use awaitMatch utility for custom matching
148
+ * // Alternative: Use awaitMatch utility for custom matching logic
111
149
  * onInsert: async ({ transaction, collection }) => {
112
150
  * const newItem = transaction.mutations[0].modified
113
151
  * await api.todos.create({ data: newItem })
152
+ * // Wait for specific change to appear in sync stream
114
153
  * await collection.utils.awaitMatch(
115
154
  * (message) => isChangeMessage(message) &&
116
155
  * message.headers.operation === 'insert' &&
@@ -121,24 +160,39 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
121
160
  onInsert?: (params: InsertMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>;
122
161
  /**
123
162
  * Optional asynchronous handler function called before an update operation
163
+ *
164
+ * **IMPORTANT - Electric Synchronization:**
165
+ * This handler **must not resolve** until synchronization is confirmed.
166
+ * Await one of these synchronization utilities before the handler completes:
167
+ * 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
168
+ * 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
169
+ *
170
+ * Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
171
+ *
124
172
  * @param params Object containing transaction and collection information
125
- * @returns Promise resolving to { txid, timeout? } or void
173
+ * @returns Promise that should resolve after synchronization is complete
174
+ *
175
+ * **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
176
+ * Use `await collection.utils.awaitTxId(txid)` within the handler instead.
177
+ *
126
178
  * @example
127
- * // Basic Electric update handler with txid (recommended)
128
- * onUpdate: async ({ transaction }) => {
179
+ * // Recommended: Wait for txid to sync
180
+ * onUpdate: async ({ transaction, collection }) => {
129
181
  * const { original, changes } = transaction.mutations[0]
130
182
  * const result = await api.todos.update({
131
183
  * where: { id: original.id },
132
184
  * data: changes
133
185
  * })
134
- * return { txid: result.txid }
186
+ * // Wait for txid to sync before handler completes
187
+ * await collection.utils.awaitTxId(result.txid)
135
188
  * }
136
189
  *
137
190
  * @example
138
- * // Use awaitMatch utility for custom matching
191
+ * // Alternative: Use awaitMatch utility for custom matching logic
139
192
  * onUpdate: async ({ transaction, collection }) => {
140
193
  * const { original, changes } = transaction.mutations[0]
141
194
  * await api.todos.update({ where: { id: original.id }, data: changes })
195
+ * // Wait for specific change to appear in sync stream
142
196
  * await collection.utils.awaitMatch(
143
197
  * (message) => isChangeMessage(message) &&
144
198
  * message.headers.operation === 'update' &&
@@ -149,23 +203,38 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
149
203
  onUpdate?: (params: UpdateMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>;
150
204
  /**
151
205
  * Optional asynchronous handler function called before a delete operation
206
+ *
207
+ * **IMPORTANT - Electric Synchronization:**
208
+ * This handler **must not resolve** until synchronization is confirmed.
209
+ * Await one of these synchronization utilities before the handler completes:
210
+ * 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
211
+ * 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
212
+ *
213
+ * Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
214
+ *
152
215
  * @param params Object containing transaction and collection information
153
- * @returns Promise resolving to { txid, timeout? } or void
216
+ * @returns Promise that should resolve after synchronization is complete
217
+ *
218
+ * **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
219
+ * Use `await collection.utils.awaitTxId(txid)` within the handler instead.
220
+ *
154
221
  * @example
155
- * // Basic Electric delete handler with txid (recommended)
156
- * onDelete: async ({ transaction }) => {
222
+ * // Recommended: Wait for txid to sync
223
+ * onDelete: async ({ transaction, collection }) => {
157
224
  * const mutation = transaction.mutations[0]
158
225
  * const result = await api.todos.delete({
159
226
  * id: mutation.original.id
160
227
  * })
161
- * return { txid: result.txid }
228
+ * // Wait for txid to sync before handler completes
229
+ * await collection.utils.awaitTxId(result.txid)
162
230
  * }
163
231
  *
164
232
  * @example
165
- * // Use awaitMatch utility for custom matching
233
+ * // Alternative: Use awaitMatch utility for custom matching logic
166
234
  * onDelete: async ({ transaction, collection }) => {
167
235
  * const mutation = transaction.mutations[0]
168
236
  * await api.todos.delete({ id: mutation.original.id })
237
+ * // Wait for specific change to appear in sync stream
169
238
  * await collection.utils.awaitMatch(
170
239
  * (message) => isChangeMessage(message) &&
171
240
  * message.headers.operation === 'delete' &&
@@ -186,7 +255,7 @@ export type AwaitMatchFn<T extends Row<unknown>> = (matchFn: MatchFunction<T>, t
186
255
  /**
187
256
  * Electric collection utilities type
188
257
  */
189
- export interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>> extends UtilsRecord {
258
+ export interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>> {
190
259
  awaitTxId: AwaitTxIdFn;
191
260
  awaitMatch: AwaitMatchFn<T>;
192
261
  }