@tanstack/db 0.0.19 → 0.0.21

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/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export * from "./errors"
7
7
  export * from "./proxy"
8
8
  export * from "./query/index.js"
9
9
  export * from "./optimistic-action"
10
+ export * from "./local-only"
11
+ export * from "./local-storage"
10
12
 
11
13
  // Re-export some stuff explicitly to ensure the type & value is exported
12
14
  export type { Collection } from "./collection"
@@ -0,0 +1,302 @@
1
+ import type {
2
+ DeleteMutationFnParams,
3
+ InsertMutationFnParams,
4
+ OperationType,
5
+ ResolveType,
6
+ SyncConfig,
7
+ UpdateMutationFnParams,
8
+ UtilsRecord,
9
+ } from "./types"
10
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
11
+
12
+ /**
13
+ * Configuration interface for Local-only collection options
14
+ * @template TExplicit - The explicit type of items in the collection (highest priority)
15
+ * @template TSchema - The schema type for validation and type inference (second priority)
16
+ * @template TFallback - The fallback type if no explicit or schema type is provided
17
+ * @template TKey - The type of the key returned by getKey
18
+ *
19
+ * @remarks
20
+ * Type resolution follows a priority order:
21
+ * 1. If you provide an explicit type via generic parameter, it will be used
22
+ * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
23
+ * 3. If neither explicit type nor schema is provided, the fallback type will be used
24
+ *
25
+ * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
26
+ */
27
+ export interface LocalOnlyCollectionConfig<
28
+ TExplicit = unknown,
29
+ TSchema extends StandardSchemaV1 = never,
30
+ TFallback extends Record<string, unknown> = Record<string, unknown>,
31
+ TKey extends string | number = string | number,
32
+ > {
33
+ /**
34
+ * Standard Collection configuration properties
35
+ */
36
+ id?: string
37
+ schema?: TSchema
38
+ getKey: (item: ResolveType<TExplicit, TSchema, TFallback>) => TKey
39
+
40
+ /**
41
+ * Optional initial data to populate the collection with on creation
42
+ * This data will be applied during the initial sync process
43
+ */
44
+ initialData?: Array<ResolveType<TExplicit, TSchema, TFallback>>
45
+
46
+ /**
47
+ * Optional asynchronous handler function called after an insert operation
48
+ * @param params Object containing transaction and collection information
49
+ * @returns Promise resolving to any value
50
+ */
51
+ onInsert?: (
52
+ params: InsertMutationFnParams<
53
+ ResolveType<TExplicit, TSchema, TFallback>,
54
+ TKey,
55
+ LocalOnlyCollectionUtils
56
+ >
57
+ ) => Promise<any>
58
+
59
+ /**
60
+ * Optional asynchronous handler function called after an update operation
61
+ * @param params Object containing transaction and collection information
62
+ * @returns Promise resolving to any value
63
+ */
64
+ onUpdate?: (
65
+ params: UpdateMutationFnParams<
66
+ ResolveType<TExplicit, TSchema, TFallback>,
67
+ TKey,
68
+ LocalOnlyCollectionUtils
69
+ >
70
+ ) => Promise<any>
71
+
72
+ /**
73
+ * Optional asynchronous handler function called after a delete operation
74
+ * @param params Object containing transaction and collection information
75
+ * @returns Promise resolving to any value
76
+ */
77
+ onDelete?: (
78
+ params: DeleteMutationFnParams<
79
+ ResolveType<TExplicit, TSchema, TFallback>,
80
+ TKey,
81
+ LocalOnlyCollectionUtils
82
+ >
83
+ ) => Promise<any>
84
+ }
85
+
86
+ /**
87
+ * Local-only collection utilities type (currently empty but matches the pattern)
88
+ */
89
+ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
90
+
91
+ /**
92
+ * Creates Local-only collection options for use with a standard Collection
93
+ *
94
+ * This is an in-memory collection that doesn't sync with external sources but uses a loopback sync config
95
+ * that immediately "syncs" all optimistic changes to the collection, making them permanent.
96
+ * Perfect for local-only data that doesn't need persistence or external synchronization.
97
+ *
98
+ * @template TExplicit - The explicit type of items in the collection (highest priority)
99
+ * @template TSchema - The schema type for validation and type inference (second priority)
100
+ * @template TFallback - The fallback type if no explicit or schema type is provided
101
+ * @template TKey - The type of the key returned by getKey
102
+ * @param config - Configuration options for the Local-only collection
103
+ * @returns Collection options with utilities (currently empty but follows the pattern)
104
+ *
105
+ * @example
106
+ * // Basic local-only collection
107
+ * const collection = createCollection(
108
+ * localOnlyCollectionOptions({
109
+ * getKey: (item) => item.id,
110
+ * })
111
+ * )
112
+ *
113
+ * @example
114
+ * // Local-only collection with initial data
115
+ * const collection = createCollection(
116
+ * localOnlyCollectionOptions({
117
+ * getKey: (item) => item.id,
118
+ * initialData: [
119
+ * { id: 1, name: 'Item 1' },
120
+ * { id: 2, name: 'Item 2' },
121
+ * ],
122
+ * })
123
+ * )
124
+ *
125
+ * @example
126
+ * // Local-only collection with mutation handlers
127
+ * const collection = createCollection(
128
+ * localOnlyCollectionOptions({
129
+ * getKey: (item) => item.id,
130
+ * onInsert: async ({ transaction }) => {
131
+ * console.log('Item inserted:', transaction.mutations[0].modified)
132
+ * // Custom logic after insert
133
+ * },
134
+ * })
135
+ * )
136
+ */
137
+ export function localOnlyCollectionOptions<
138
+ TExplicit = unknown,
139
+ TSchema extends StandardSchemaV1 = never,
140
+ TFallback extends Record<string, unknown> = Record<string, unknown>,
141
+ TKey extends string | number = string | number,
142
+ >(config: LocalOnlyCollectionConfig<TExplicit, TSchema, TFallback, TKey>) {
143
+ type ResolvedType = ResolveType<TExplicit, TSchema, TFallback>
144
+
145
+ const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
146
+
147
+ // Create the sync configuration with transaction confirmation capability
148
+ const syncResult = createLocalOnlySync<ResolvedType, TKey>(initialData)
149
+
150
+ /**
151
+ * Create wrapper handlers that call user handlers first, then confirm transactions
152
+ * Wraps the user's onInsert handler to also confirm the transaction immediately
153
+ */
154
+ const wrappedOnInsert = async (
155
+ params: InsertMutationFnParams<ResolvedType, TKey, LocalOnlyCollectionUtils>
156
+ ) => {
157
+ // Call user handler first if provided
158
+ let handlerResult
159
+ if (onInsert) {
160
+ handlerResult = (await onInsert(params)) ?? {}
161
+ }
162
+
163
+ // Then synchronously confirm the transaction by looping through mutations
164
+ syncResult.confirmOperationsSync(params.transaction.mutations)
165
+
166
+ return handlerResult
167
+ }
168
+
169
+ /**
170
+ * Wrapper for onUpdate handler that also confirms the transaction immediately
171
+ */
172
+ const wrappedOnUpdate = async (
173
+ params: UpdateMutationFnParams<ResolvedType, TKey, LocalOnlyCollectionUtils>
174
+ ) => {
175
+ // Call user handler first if provided
176
+ let handlerResult
177
+ if (onUpdate) {
178
+ handlerResult = (await onUpdate(params)) ?? {}
179
+ }
180
+
181
+ // Then synchronously confirm the transaction by looping through mutations
182
+ syncResult.confirmOperationsSync(params.transaction.mutations)
183
+
184
+ return handlerResult
185
+ }
186
+
187
+ /**
188
+ * Wrapper for onDelete handler that also confirms the transaction immediately
189
+ */
190
+ const wrappedOnDelete = async (
191
+ params: DeleteMutationFnParams<ResolvedType, TKey, LocalOnlyCollectionUtils>
192
+ ) => {
193
+ // Call user handler first if provided
194
+ let handlerResult
195
+ if (onDelete) {
196
+ handlerResult = (await onDelete(params)) ?? {}
197
+ }
198
+
199
+ // Then synchronously confirm the transaction by looping through mutations
200
+ syncResult.confirmOperationsSync(params.transaction.mutations)
201
+
202
+ return handlerResult
203
+ }
204
+
205
+ return {
206
+ ...restConfig,
207
+ sync: syncResult.sync,
208
+ onInsert: wrappedOnInsert,
209
+ onUpdate: wrappedOnUpdate,
210
+ onDelete: wrappedOnDelete,
211
+ utils: {} as LocalOnlyCollectionUtils,
212
+ startSync: true,
213
+ gcTime: 0,
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Internal function to create Local-only sync configuration with transaction confirmation
219
+ *
220
+ * This captures the sync functions and provides synchronous confirmation of operations.
221
+ * It creates a loopback sync that immediately confirms all optimistic operations,
222
+ * making them permanent in the collection.
223
+ *
224
+ * @param initialData - Optional array of initial items to populate the collection
225
+ * @returns Object with sync configuration and confirmOperationsSync function
226
+ */
227
+ function createLocalOnlySync<T extends object, TKey extends string | number>(
228
+ initialData?: Array<T>
229
+ ) {
230
+ // Capture sync functions for transaction confirmation
231
+ let syncBegin: (() => void) | null = null
232
+ let syncWrite: ((message: { type: OperationType; value: T }) => void) | null =
233
+ null
234
+ let syncCommit: (() => void) | null = null
235
+
236
+ const sync: SyncConfig<T, TKey> = {
237
+ /**
238
+ * Sync function that captures sync parameters and applies initial data
239
+ * @param params - Sync parameters containing begin, write, and commit functions
240
+ * @returns Unsubscribe function (empty since no ongoing sync is needed)
241
+ */
242
+ sync: (params) => {
243
+ const { begin, write, commit } = params
244
+
245
+ // Capture sync functions for later use by confirmOperationsSync
246
+ syncBegin = begin
247
+ syncWrite = write
248
+ syncCommit = commit
249
+
250
+ // Apply initial data if provided
251
+ if (initialData && initialData.length > 0) {
252
+ begin()
253
+ initialData.forEach((item) => {
254
+ write({
255
+ type: `insert`,
256
+ value: item,
257
+ })
258
+ })
259
+ commit()
260
+ }
261
+
262
+ // Return empty unsubscribe function - no ongoing sync needed
263
+ return () => {}
264
+ },
265
+ /**
266
+ * Get sync metadata - returns empty object for local-only collections
267
+ * @returns Empty metadata object
268
+ */
269
+ getSyncMetadata: () => ({}),
270
+ }
271
+
272
+ /**
273
+ * Synchronously confirms optimistic operations by immediately writing through sync
274
+ *
275
+ * This loops through transaction mutations and applies them to move from optimistic to synced state.
276
+ * It's called after user handlers to make optimistic changes permanent.
277
+ *
278
+ * @param mutations - Array of mutation objects from the transaction
279
+ */
280
+ const confirmOperationsSync = (mutations: Array<any>) => {
281
+ if (!syncBegin || !syncWrite || !syncCommit) {
282
+ return // Sync not initialized yet, which is fine
283
+ }
284
+
285
+ // Immediately write back through sync interface
286
+ syncBegin()
287
+ mutations.forEach((mutation) => {
288
+ if (syncWrite) {
289
+ syncWrite({
290
+ type: mutation.type,
291
+ value: mutation.modified,
292
+ })
293
+ }
294
+ })
295
+ syncCommit()
296
+ }
297
+
298
+ return {
299
+ sync,
300
+ confirmOperationsSync,
301
+ }
302
+ }