@tanstack/db 0.2.4 → 0.3.0
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/collection.cjs +23 -4
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +35 -41
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/local-only.d.cts +17 -43
- package/dist/cjs/local-storage.cjs +3 -12
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +16 -39
- package/dist/cjs/query/builder/types.d.cts +3 -10
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/transactions.cjs +76 -5
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +17 -0
- package/dist/cjs/types.d.cts +10 -31
- package/dist/esm/collection.d.ts +35 -41
- package/dist/esm/collection.js +23 -4
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/local-only.d.ts +17 -43
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/local-storage.d.ts +16 -39
- package/dist/esm/local-storage.js +3 -12
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +3 -10
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.d.ts +17 -0
- package/dist/esm/transactions.js +76 -5
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +10 -31
- package/package.json +2 -2
- package/src/collection.ts +148 -196
- package/src/local-only.ts +57 -77
- package/src/local-storage.ts +53 -85
- package/src/query/builder/types.ts +3 -12
- package/src/query/live-query-collection.ts +1 -1
- package/src/transactions.ts +121 -6
- package/src/types.ts +25 -55
package/src/local-only.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BaseCollectionConfig,
|
|
2
3
|
CollectionConfig,
|
|
3
4
|
DeleteMutationFnParams,
|
|
5
|
+
InferSchemaOutput,
|
|
4
6
|
InsertMutationFnParams,
|
|
5
7
|
OperationType,
|
|
6
|
-
ResolveType,
|
|
7
8
|
SyncConfig,
|
|
8
9
|
UpdateMutationFnParams,
|
|
9
10
|
UtilsRecord,
|
|
@@ -12,76 +13,23 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Configuration interface for Local-only collection options
|
|
15
|
-
* @template
|
|
16
|
-
* @template TSchema - The schema type for validation
|
|
17
|
-
* @template
|
|
18
|
-
* @template TKey - The type of the key returned by getKey
|
|
19
|
-
*
|
|
20
|
-
* @remarks
|
|
21
|
-
* Type resolution follows a priority order:
|
|
22
|
-
* 1. If you provide an explicit type via generic parameter, it will be used
|
|
23
|
-
* 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
|
|
24
|
-
* 3. If neither explicit type nor schema is provided, the fallback type will be used
|
|
25
|
-
*
|
|
26
|
-
* You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
|
|
16
|
+
* @template T - The type of items in the collection
|
|
17
|
+
* @template TSchema - The schema type for validation
|
|
18
|
+
* @template TKey - The type of the key returned by `getKey`
|
|
27
19
|
*/
|
|
28
20
|
export interface LocalOnlyCollectionConfig<
|
|
29
|
-
|
|
21
|
+
T extends object = object,
|
|
30
22
|
TSchema extends StandardSchemaV1 = never,
|
|
31
|
-
TFallback extends Record<string, unknown> = Record<string, unknown>,
|
|
32
23
|
TKey extends string | number = string | number,
|
|
33
|
-
>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
id?: string
|
|
38
|
-
schema?: TSchema
|
|
39
|
-
getKey: (item: ResolveType<TExplicit, TSchema, TFallback>) => TKey
|
|
40
|
-
|
|
24
|
+
> extends Omit<
|
|
25
|
+
BaseCollectionConfig<T, TKey, TSchema, LocalOnlyCollectionUtils>,
|
|
26
|
+
`gcTime` | `startSync`
|
|
27
|
+
> {
|
|
41
28
|
/**
|
|
42
29
|
* Optional initial data to populate the collection with on creation
|
|
43
30
|
* This data will be applied during the initial sync process
|
|
44
31
|
*/
|
|
45
|
-
initialData?: Array<
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Optional asynchronous handler function called after an insert operation
|
|
49
|
-
* @param params Object containing transaction and collection information
|
|
50
|
-
* @returns Promise resolving to any value
|
|
51
|
-
*/
|
|
52
|
-
onInsert?: (
|
|
53
|
-
params: InsertMutationFnParams<
|
|
54
|
-
ResolveType<TExplicit, TSchema, TFallback>,
|
|
55
|
-
TKey,
|
|
56
|
-
LocalOnlyCollectionUtils
|
|
57
|
-
>
|
|
58
|
-
) => Promise<any>
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Optional asynchronous handler function called after an update operation
|
|
62
|
-
* @param params Object containing transaction and collection information
|
|
63
|
-
* @returns Promise resolving to any value
|
|
64
|
-
*/
|
|
65
|
-
onUpdate?: (
|
|
66
|
-
params: UpdateMutationFnParams<
|
|
67
|
-
ResolveType<TExplicit, TSchema, TFallback>,
|
|
68
|
-
TKey,
|
|
69
|
-
LocalOnlyCollectionUtils
|
|
70
|
-
>
|
|
71
|
-
) => Promise<any>
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Optional asynchronous handler function called after a delete operation
|
|
75
|
-
* @param params Object containing transaction and collection information
|
|
76
|
-
* @returns Promise resolving to any value
|
|
77
|
-
*/
|
|
78
|
-
onDelete?: (
|
|
79
|
-
params: DeleteMutationFnParams<
|
|
80
|
-
ResolveType<TExplicit, TSchema, TFallback>,
|
|
81
|
-
TKey,
|
|
82
|
-
LocalOnlyCollectionUtils
|
|
83
|
-
>
|
|
84
|
-
) => Promise<any>
|
|
32
|
+
initialData?: Array<T>
|
|
85
33
|
}
|
|
86
34
|
|
|
87
35
|
/**
|
|
@@ -96,9 +44,7 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
|
|
|
96
44
|
* that immediately "syncs" all optimistic changes to the collection, making them permanent.
|
|
97
45
|
* Perfect for local-only data that doesn't need persistence or external synchronization.
|
|
98
46
|
*
|
|
99
|
-
* @template
|
|
100
|
-
* @template TSchema - The schema type for validation and type inference (second priority)
|
|
101
|
-
* @template TFallback - The fallback type if no explicit or schema type is provided
|
|
47
|
+
* @template T - The schema type if a schema is provided, otherwise the type of items in the collection
|
|
102
48
|
* @template TKey - The type of the key returned by getKey
|
|
103
49
|
* @param config - Configuration options for the Local-only collection
|
|
104
50
|
* @returns Collection options with utilities (currently empty but follows the pattern)
|
|
@@ -135,29 +81,55 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
|
|
|
135
81
|
* })
|
|
136
82
|
* )
|
|
137
83
|
*/
|
|
84
|
+
|
|
85
|
+
// Overload for when schema is provided
|
|
138
86
|
export function localOnlyCollectionOptions<
|
|
139
|
-
|
|
140
|
-
TSchema extends StandardSchemaV1 = never,
|
|
141
|
-
TFallback extends Record<string, unknown> = Record<string, unknown>,
|
|
87
|
+
T extends StandardSchemaV1,
|
|
142
88
|
TKey extends string | number = string | number,
|
|
143
89
|
>(
|
|
144
|
-
config: LocalOnlyCollectionConfig<
|
|
145
|
-
|
|
90
|
+
config: LocalOnlyCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
|
|
91
|
+
schema: T
|
|
92
|
+
}
|
|
93
|
+
): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
|
|
146
94
|
utils: LocalOnlyCollectionUtils
|
|
147
|
-
|
|
148
|
-
|
|
95
|
+
schema: T
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Overload for when no schema is provided
|
|
99
|
+
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
|
|
100
|
+
export function localOnlyCollectionOptions<
|
|
101
|
+
T extends object,
|
|
102
|
+
TKey extends string | number = string | number,
|
|
103
|
+
>(
|
|
104
|
+
config: LocalOnlyCollectionConfig<T, never, TKey> & {
|
|
105
|
+
schema?: never // prohibit schema
|
|
106
|
+
}
|
|
107
|
+
): CollectionConfig<T, TKey> & {
|
|
108
|
+
utils: LocalOnlyCollectionUtils
|
|
109
|
+
schema?: never // no schema in the result
|
|
110
|
+
}
|
|
149
111
|
|
|
112
|
+
export function localOnlyCollectionOptions(
|
|
113
|
+
config: LocalOnlyCollectionConfig<any, any, string | number>
|
|
114
|
+
): CollectionConfig<any, string | number, any> & {
|
|
115
|
+
utils: LocalOnlyCollectionUtils
|
|
116
|
+
schema?: StandardSchemaV1
|
|
117
|
+
} {
|
|
150
118
|
const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
|
|
151
119
|
|
|
152
120
|
// Create the sync configuration with transaction confirmation capability
|
|
153
|
-
const syncResult = createLocalOnlySync
|
|
121
|
+
const syncResult = createLocalOnlySync(initialData)
|
|
154
122
|
|
|
155
123
|
/**
|
|
156
124
|
* Create wrapper handlers that call user handlers first, then confirm transactions
|
|
157
125
|
* Wraps the user's onInsert handler to also confirm the transaction immediately
|
|
158
126
|
*/
|
|
159
127
|
const wrappedOnInsert = async (
|
|
160
|
-
params: InsertMutationFnParams<
|
|
128
|
+
params: InsertMutationFnParams<
|
|
129
|
+
any,
|
|
130
|
+
string | number,
|
|
131
|
+
LocalOnlyCollectionUtils
|
|
132
|
+
>
|
|
161
133
|
) => {
|
|
162
134
|
// Call user handler first if provided
|
|
163
135
|
let handlerResult
|
|
@@ -175,7 +147,11 @@ export function localOnlyCollectionOptions<
|
|
|
175
147
|
* Wrapper for onUpdate handler that also confirms the transaction immediately
|
|
176
148
|
*/
|
|
177
149
|
const wrappedOnUpdate = async (
|
|
178
|
-
params: UpdateMutationFnParams<
|
|
150
|
+
params: UpdateMutationFnParams<
|
|
151
|
+
any,
|
|
152
|
+
string | number,
|
|
153
|
+
LocalOnlyCollectionUtils
|
|
154
|
+
>
|
|
179
155
|
) => {
|
|
180
156
|
// Call user handler first if provided
|
|
181
157
|
let handlerResult
|
|
@@ -193,7 +169,11 @@ export function localOnlyCollectionOptions<
|
|
|
193
169
|
* Wrapper for onDelete handler that also confirms the transaction immediately
|
|
194
170
|
*/
|
|
195
171
|
const wrappedOnDelete = async (
|
|
196
|
-
params: DeleteMutationFnParams<
|
|
172
|
+
params: DeleteMutationFnParams<
|
|
173
|
+
any,
|
|
174
|
+
string | number,
|
|
175
|
+
LocalOnlyCollectionUtils
|
|
176
|
+
>
|
|
197
177
|
) => {
|
|
198
178
|
// Call user handler first if provided
|
|
199
179
|
let handlerResult
|
package/src/local-storage.ts
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
StorageKeyRequiredError,
|
|
8
8
|
} from "./errors"
|
|
9
9
|
import type {
|
|
10
|
+
BaseCollectionConfig,
|
|
10
11
|
CollectionConfig,
|
|
11
12
|
DeleteMutationFnParams,
|
|
13
|
+
InferSchemaOutput,
|
|
12
14
|
InsertMutationFnParams,
|
|
13
|
-
ResolveType,
|
|
14
15
|
SyncConfig,
|
|
15
16
|
UpdateMutationFnParams,
|
|
16
17
|
UtilsRecord,
|
|
@@ -46,23 +47,15 @@ interface StoredItem<T> {
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Configuration interface for localStorage collection options
|
|
49
|
-
* @template
|
|
50
|
-
* @template TSchema - The schema type for validation
|
|
51
|
-
* @template
|
|
52
|
-
*
|
|
53
|
-
* @remarks
|
|
54
|
-
* Type resolution follows a priority order:
|
|
55
|
-
* 1. If you provide an explicit type via generic parameter, it will be used
|
|
56
|
-
* 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
|
|
57
|
-
* 3. If neither explicit type nor schema is provided, the fallback type will be used
|
|
58
|
-
*
|
|
59
|
-
* You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
|
|
50
|
+
* @template T - The type of items in the collection
|
|
51
|
+
* @template TSchema - The schema type for validation
|
|
52
|
+
* @template TKey - The type of the key returned by `getKey`
|
|
60
53
|
*/
|
|
61
54
|
export interface LocalStorageCollectionConfig<
|
|
62
|
-
|
|
55
|
+
T extends object = object,
|
|
63
56
|
TSchema extends StandardSchemaV1 = never,
|
|
64
|
-
|
|
65
|
-
> {
|
|
57
|
+
TKey extends string | number = string | number,
|
|
58
|
+
> extends BaseCollectionConfig<T, TKey, TSchema> {
|
|
66
59
|
/**
|
|
67
60
|
* The key to use for storing the collection data in localStorage/sessionStorage
|
|
68
61
|
*/
|
|
@@ -79,41 +72,6 @@ export interface LocalStorageCollectionConfig<
|
|
|
79
72
|
* Can be any object that implements addEventListener/removeEventListener for storage events
|
|
80
73
|
*/
|
|
81
74
|
storageEventApi?: StorageEventApi
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Collection identifier (defaults to "local-collection:{storageKey}" if not provided)
|
|
85
|
-
*/
|
|
86
|
-
id?: string
|
|
87
|
-
schema?: TSchema
|
|
88
|
-
getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`]
|
|
89
|
-
sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`]
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Optional asynchronous handler function called before an insert operation
|
|
93
|
-
* @param params Object containing transaction and collection information
|
|
94
|
-
* @returns Promise resolving to any value
|
|
95
|
-
*/
|
|
96
|
-
onInsert?: (
|
|
97
|
-
params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
|
|
98
|
-
) => Promise<any>
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Optional asynchronous handler function called before an update operation
|
|
102
|
-
* @param params Object containing transaction and collection information
|
|
103
|
-
* @returns Promise resolving to any value
|
|
104
|
-
*/
|
|
105
|
-
onUpdate?: (
|
|
106
|
-
params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
|
|
107
|
-
) => Promise<any>
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Optional asynchronous handler function called before a delete operation
|
|
111
|
-
* @param params Object containing transaction and collection information
|
|
112
|
-
* @returns Promise resolving to any value
|
|
113
|
-
*/
|
|
114
|
-
onDelete?: (
|
|
115
|
-
params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
|
|
116
|
-
) => Promise<any>
|
|
117
75
|
}
|
|
118
76
|
|
|
119
77
|
/**
|
|
@@ -202,18 +160,43 @@ function generateUuid(): string {
|
|
|
202
160
|
* })
|
|
203
161
|
* )
|
|
204
162
|
*/
|
|
163
|
+
|
|
164
|
+
// Overload for when schema is provided
|
|
205
165
|
export function localStorageCollectionOptions<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
TFallback extends object = Record<string, unknown>,
|
|
166
|
+
T extends StandardSchemaV1,
|
|
167
|
+
TKey extends string | number = string | number,
|
|
209
168
|
>(
|
|
210
|
-
config: LocalStorageCollectionConfig<
|
|
211
|
-
|
|
169
|
+
config: LocalStorageCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
|
|
170
|
+
schema: T
|
|
171
|
+
}
|
|
172
|
+
): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
|
|
212
173
|
id: string
|
|
213
174
|
utils: LocalStorageCollectionUtils
|
|
214
|
-
|
|
215
|
-
|
|
175
|
+
schema: T
|
|
176
|
+
}
|
|
216
177
|
|
|
178
|
+
// Overload for when no schema is provided
|
|
179
|
+
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
|
|
180
|
+
export function localStorageCollectionOptions<
|
|
181
|
+
T extends object,
|
|
182
|
+
TKey extends string | number = string | number,
|
|
183
|
+
>(
|
|
184
|
+
config: LocalStorageCollectionConfig<T, never, TKey> & {
|
|
185
|
+
schema?: never // prohibit schema
|
|
186
|
+
}
|
|
187
|
+
): CollectionConfig<T, TKey> & {
|
|
188
|
+
id: string
|
|
189
|
+
utils: LocalStorageCollectionUtils
|
|
190
|
+
schema?: never // no schema in the result
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function localStorageCollectionOptions(
|
|
194
|
+
config: LocalStorageCollectionConfig<any, any, string | number>
|
|
195
|
+
): Omit<CollectionConfig<any, string | number, any>, `id`> & {
|
|
196
|
+
id: string
|
|
197
|
+
utils: LocalStorageCollectionUtils
|
|
198
|
+
schema?: StandardSchemaV1
|
|
199
|
+
} {
|
|
217
200
|
// Validate required parameters
|
|
218
201
|
if (!config.storageKey) {
|
|
219
202
|
throw new StorageKeyRequiredError()
|
|
@@ -237,10 +220,10 @@ export function localStorageCollectionOptions<
|
|
|
237
220
|
}
|
|
238
221
|
|
|
239
222
|
// Track the last known state to detect changes
|
|
240
|
-
const lastKnownData = new Map<string | number, StoredItem<
|
|
223
|
+
const lastKnownData = new Map<string | number, StoredItem<any>>()
|
|
241
224
|
|
|
242
225
|
// Create the sync configuration
|
|
243
|
-
const sync = createLocalStorageSync<
|
|
226
|
+
const sync = createLocalStorageSync<any>(
|
|
244
227
|
config.storageKey,
|
|
245
228
|
storage,
|
|
246
229
|
storageEventApi,
|
|
@@ -263,11 +246,11 @@ export function localStorageCollectionOptions<
|
|
|
263
246
|
* @param dataMap - Map of items with version tracking to save to storage
|
|
264
247
|
*/
|
|
265
248
|
const saveToStorage = (
|
|
266
|
-
dataMap: Map<string | number, StoredItem<
|
|
249
|
+
dataMap: Map<string | number, StoredItem<any>>
|
|
267
250
|
): void => {
|
|
268
251
|
try {
|
|
269
252
|
// Convert Map to object format for storage
|
|
270
|
-
const objectData: Record<string, StoredItem<
|
|
253
|
+
const objectData: Record<string, StoredItem<any>> = {}
|
|
271
254
|
dataMap.forEach((storedItem, key) => {
|
|
272
255
|
objectData[String(key)] = storedItem
|
|
273
256
|
})
|
|
@@ -302,9 +285,7 @@ export function localStorageCollectionOptions<
|
|
|
302
285
|
* Create wrapper handlers for direct persistence operations that perform actual storage operations
|
|
303
286
|
* Wraps the user's onInsert handler to also save changes to localStorage
|
|
304
287
|
*/
|
|
305
|
-
const wrappedOnInsert = async (
|
|
306
|
-
params: InsertMutationFnParams<ResolvedType>
|
|
307
|
-
) => {
|
|
288
|
+
const wrappedOnInsert = async (params: InsertMutationFnParams<any>) => {
|
|
308
289
|
// Validate that all values in the transaction can be JSON serialized
|
|
309
290
|
params.transaction.mutations.forEach((mutation) => {
|
|
310
291
|
validateJsonSerializable(mutation.modified, `insert`)
|
|
@@ -318,15 +299,12 @@ export function localStorageCollectionOptions<
|
|
|
318
299
|
|
|
319
300
|
// Always persist to storage
|
|
320
301
|
// Load current data from storage
|
|
321
|
-
const currentData = loadFromStorage<
|
|
322
|
-
config.storageKey,
|
|
323
|
-
storage
|
|
324
|
-
)
|
|
302
|
+
const currentData = loadFromStorage<any>(config.storageKey, storage)
|
|
325
303
|
|
|
326
304
|
// Add new items with version keys
|
|
327
305
|
params.transaction.mutations.forEach((mutation) => {
|
|
328
306
|
const key = config.getKey(mutation.modified)
|
|
329
|
-
const storedItem: StoredItem<
|
|
307
|
+
const storedItem: StoredItem<any> = {
|
|
330
308
|
versionKey: generateUuid(),
|
|
331
309
|
data: mutation.modified,
|
|
332
310
|
}
|
|
@@ -342,9 +320,7 @@ export function localStorageCollectionOptions<
|
|
|
342
320
|
return handlerResult
|
|
343
321
|
}
|
|
344
322
|
|
|
345
|
-
const wrappedOnUpdate = async (
|
|
346
|
-
params: UpdateMutationFnParams<ResolvedType>
|
|
347
|
-
) => {
|
|
323
|
+
const wrappedOnUpdate = async (params: UpdateMutationFnParams<any>) => {
|
|
348
324
|
// Validate that all values in the transaction can be JSON serialized
|
|
349
325
|
params.transaction.mutations.forEach((mutation) => {
|
|
350
326
|
validateJsonSerializable(mutation.modified, `update`)
|
|
@@ -358,15 +334,12 @@ export function localStorageCollectionOptions<
|
|
|
358
334
|
|
|
359
335
|
// Always persist to storage
|
|
360
336
|
// Load current data from storage
|
|
361
|
-
const currentData = loadFromStorage<
|
|
362
|
-
config.storageKey,
|
|
363
|
-
storage
|
|
364
|
-
)
|
|
337
|
+
const currentData = loadFromStorage<any>(config.storageKey, storage)
|
|
365
338
|
|
|
366
339
|
// Update items with new version keys
|
|
367
340
|
params.transaction.mutations.forEach((mutation) => {
|
|
368
341
|
const key = config.getKey(mutation.modified)
|
|
369
|
-
const storedItem: StoredItem<
|
|
342
|
+
const storedItem: StoredItem<any> = {
|
|
370
343
|
versionKey: generateUuid(),
|
|
371
344
|
data: mutation.modified,
|
|
372
345
|
}
|
|
@@ -382,9 +355,7 @@ export function localStorageCollectionOptions<
|
|
|
382
355
|
return handlerResult
|
|
383
356
|
}
|
|
384
357
|
|
|
385
|
-
const wrappedOnDelete = async (
|
|
386
|
-
params: DeleteMutationFnParams<ResolvedType>
|
|
387
|
-
) => {
|
|
358
|
+
const wrappedOnDelete = async (params: DeleteMutationFnParams<any>) => {
|
|
388
359
|
// Call the user handler BEFORE persisting changes (if provided)
|
|
389
360
|
let handlerResult: any = {}
|
|
390
361
|
if (config.onDelete) {
|
|
@@ -393,15 +364,12 @@ export function localStorageCollectionOptions<
|
|
|
393
364
|
|
|
394
365
|
// Always persist to storage
|
|
395
366
|
// Load current data from storage
|
|
396
|
-
const currentData = loadFromStorage<
|
|
397
|
-
config.storageKey,
|
|
398
|
-
storage
|
|
399
|
-
)
|
|
367
|
+
const currentData = loadFromStorage<any>(config.storageKey, storage)
|
|
400
368
|
|
|
401
369
|
// Remove items
|
|
402
370
|
params.transaction.mutations.forEach((mutation) => {
|
|
403
371
|
// For delete operations, mutation.original contains the full object
|
|
404
|
-
const key = config.getKey(mutation.original
|
|
372
|
+
const key = config.getKey(mutation.original)
|
|
405
373
|
currentData.delete(key)
|
|
406
374
|
})
|
|
407
375
|
|
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
Value,
|
|
9
9
|
} from "../ir.js"
|
|
10
10
|
import type { QueryBuilder } from "./index.js"
|
|
11
|
-
import type { ResolveType } from "../../types.js"
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Context - The central state container for query builder operations
|
|
@@ -77,19 +76,11 @@ export type Source = {
|
|
|
77
76
|
/**
|
|
78
77
|
* InferCollectionType - Extracts the TypeScript type from a CollectionImpl
|
|
79
78
|
*
|
|
80
|
-
* This helper ensures we get the same type that
|
|
81
|
-
*
|
|
82
|
-
* consistency between collection creation and query type inference.
|
|
83
|
-
*
|
|
84
|
-
* The complex generic parameters extract:
|
|
85
|
-
* - U: The base document type
|
|
86
|
-
* - TSchema: The schema definition
|
|
87
|
-
* - The resolved type combines these with any transforms
|
|
79
|
+
* This helper ensures we get the same type that was used when creating the collection itself.
|
|
80
|
+
* This can be an explicit type passed by the user or the schema output type.
|
|
88
81
|
*/
|
|
89
82
|
export type InferCollectionType<T> =
|
|
90
|
-
T extends CollectionImpl<infer
|
|
91
|
-
? ResolveType<U, TSchema, U>
|
|
92
|
-
: never
|
|
83
|
+
T extends CollectionImpl<infer TOutput, any, any, any, any> ? TOutput : never
|
|
93
84
|
|
|
94
85
|
/**
|
|
95
86
|
* SchemaFromSource - Converts a Source definition into a ContextSchema
|
|
@@ -130,7 +130,7 @@ export function createLiveQueryCollection<
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Bridge function that handles the type compatibility between query2's TResult
|
|
133
|
-
* and core collection's
|
|
133
|
+
* and core collection's output type without exposing ugly type assertions to users
|
|
134
134
|
*/
|
|
135
135
|
function bridgeToCreateCollection<
|
|
136
136
|
TResult extends object,
|
package/src/transactions.ts
CHANGED
|
@@ -19,6 +19,86 @@ let transactionStack: Array<Transaction<any>> = []
|
|
|
19
19
|
|
|
20
20
|
let sequenceNumber = 0
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Merges two pending mutations for the same item within a transaction
|
|
24
|
+
*
|
|
25
|
+
* Merge behavior truth table:
|
|
26
|
+
* - (insert, update) → insert (merge changes, keep empty original)
|
|
27
|
+
* - (insert, delete) → null (cancel both mutations)
|
|
28
|
+
* - (update, delete) → delete (delete dominates)
|
|
29
|
+
* - (update, update) → update (replace with latest, union changes)
|
|
30
|
+
* - (delete, delete) → delete (replace with latest)
|
|
31
|
+
* - (insert, insert) → insert (replace with latest)
|
|
32
|
+
*
|
|
33
|
+
* Note: (delete, update) and (delete, insert) should never occur as the collection
|
|
34
|
+
* layer prevents operations on deleted items within the same transaction.
|
|
35
|
+
*
|
|
36
|
+
* @param existing - The existing mutation in the transaction
|
|
37
|
+
* @param incoming - The new mutation being applied
|
|
38
|
+
* @returns The merged mutation, or null if both should be removed
|
|
39
|
+
*/
|
|
40
|
+
function mergePendingMutations<T extends object>(
|
|
41
|
+
existing: PendingMutation<T>,
|
|
42
|
+
incoming: PendingMutation<T>
|
|
43
|
+
): PendingMutation<T> | null {
|
|
44
|
+
// Truth table implementation
|
|
45
|
+
switch (`${existing.type}-${incoming.type}` as const) {
|
|
46
|
+
case `insert-update`: {
|
|
47
|
+
// Update after insert: keep as insert but merge changes
|
|
48
|
+
// For insert-update, the key should remain the same since collections don't allow key changes
|
|
49
|
+
return {
|
|
50
|
+
...existing,
|
|
51
|
+
type: `insert` as const,
|
|
52
|
+
original: {},
|
|
53
|
+
modified: incoming.modified,
|
|
54
|
+
changes: { ...existing.changes, ...incoming.changes },
|
|
55
|
+
// Keep existing keys (key changes not allowed in updates)
|
|
56
|
+
key: existing.key,
|
|
57
|
+
globalKey: existing.globalKey,
|
|
58
|
+
// Merge metadata (last-write-wins)
|
|
59
|
+
metadata: incoming.metadata ?? existing.metadata,
|
|
60
|
+
syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },
|
|
61
|
+
// Update tracking info
|
|
62
|
+
mutationId: incoming.mutationId,
|
|
63
|
+
updatedAt: incoming.updatedAt,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case `insert-delete`:
|
|
68
|
+
// Delete after insert: cancel both mutations
|
|
69
|
+
return null
|
|
70
|
+
|
|
71
|
+
case `update-delete`:
|
|
72
|
+
// Delete after update: delete dominates
|
|
73
|
+
return incoming
|
|
74
|
+
|
|
75
|
+
case `update-update`: {
|
|
76
|
+
// Update after update: replace with latest, union changes
|
|
77
|
+
return {
|
|
78
|
+
...incoming,
|
|
79
|
+
// Keep original from first update
|
|
80
|
+
original: existing.original,
|
|
81
|
+
// Union the changes from both updates
|
|
82
|
+
changes: { ...existing.changes, ...incoming.changes },
|
|
83
|
+
// Merge metadata
|
|
84
|
+
metadata: incoming.metadata ?? existing.metadata,
|
|
85
|
+
syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case `delete-delete`:
|
|
90
|
+
case `insert-insert`:
|
|
91
|
+
// Same type: replace with latest
|
|
92
|
+
return incoming
|
|
93
|
+
|
|
94
|
+
default: {
|
|
95
|
+
// Exhaustiveness check
|
|
96
|
+
const _exhaustive: never = `${existing.type}-${incoming.type}` as never
|
|
97
|
+
throw new Error(`Unhandled mutation combination: ${_exhaustive}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
22
102
|
/**
|
|
23
103
|
* Creates a new transaction for grouping multiple collection operations
|
|
24
104
|
* @param config - Transaction configuration with mutation function
|
|
@@ -203,12 +283,32 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
203
283
|
}
|
|
204
284
|
|
|
205
285
|
if (this.autoCommit) {
|
|
206
|
-
this.commit()
|
|
286
|
+
this.commit().catch(() => {
|
|
287
|
+
// Errors from autoCommit are handled via isPersisted.promise
|
|
288
|
+
// This catch prevents unhandled promise rejections
|
|
289
|
+
})
|
|
207
290
|
}
|
|
208
291
|
|
|
209
292
|
return this
|
|
210
293
|
}
|
|
211
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Apply new mutations to this transaction, intelligently merging with existing mutations
|
|
297
|
+
*
|
|
298
|
+
* When mutations operate on the same item (same globalKey), they are merged according to
|
|
299
|
+
* the following rules:
|
|
300
|
+
*
|
|
301
|
+
* - **insert + update** → insert (merge changes, keep empty original)
|
|
302
|
+
* - **insert + delete** → removed (mutations cancel each other out)
|
|
303
|
+
* - **update + delete** → delete (delete dominates)
|
|
304
|
+
* - **update + update** → update (union changes, keep first original)
|
|
305
|
+
* - **same type** → replace with latest
|
|
306
|
+
*
|
|
307
|
+
* This merging reduces over-the-wire churn and keeps the optimistic local view
|
|
308
|
+
* aligned with user intent.
|
|
309
|
+
*
|
|
310
|
+
* @param mutations - Array of new mutations to apply
|
|
311
|
+
*/
|
|
212
312
|
applyMutations(mutations: Array<PendingMutation<any>>): void {
|
|
213
313
|
for (const newMutation of mutations) {
|
|
214
314
|
const existingIndex = this.mutations.findIndex(
|
|
@@ -216,8 +316,16 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
216
316
|
)
|
|
217
317
|
|
|
218
318
|
if (existingIndex >= 0) {
|
|
219
|
-
|
|
220
|
-
|
|
319
|
+
const existingMutation = this.mutations[existingIndex]!
|
|
320
|
+
const mergeResult = mergePendingMutations(existingMutation, newMutation)
|
|
321
|
+
|
|
322
|
+
if (mergeResult === null) {
|
|
323
|
+
// Remove the mutation (e.g., delete after insert cancels both)
|
|
324
|
+
this.mutations.splice(existingIndex, 1)
|
|
325
|
+
} else {
|
|
326
|
+
// Replace with merged mutation
|
|
327
|
+
this.mutations[existingIndex] = mergeResult
|
|
328
|
+
}
|
|
221
329
|
} else {
|
|
222
330
|
// Insert new mutation
|
|
223
331
|
this.mutations.push(newMutation)
|
|
@@ -374,14 +482,21 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
374
482
|
|
|
375
483
|
this.isPersisted.resolve(this)
|
|
376
484
|
} catch (error) {
|
|
485
|
+
// Preserve the original error for rethrowing
|
|
486
|
+
const originalError =
|
|
487
|
+
error instanceof Error ? error : new Error(String(error))
|
|
488
|
+
|
|
377
489
|
// Update transaction with error information
|
|
378
490
|
this.error = {
|
|
379
|
-
message:
|
|
380
|
-
error:
|
|
491
|
+
message: originalError.message,
|
|
492
|
+
error: originalError,
|
|
381
493
|
}
|
|
382
494
|
|
|
383
495
|
// rollback the transaction
|
|
384
|
-
|
|
496
|
+
this.rollback()
|
|
497
|
+
|
|
498
|
+
// Re-throw the original error to preserve identity and stack
|
|
499
|
+
throw originalError
|
|
385
500
|
}
|
|
386
501
|
|
|
387
502
|
return this
|