@tanstack/db 0.4.18 → 0.4.20
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/index.cjs +1 -1
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/sync.cjs +7 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +9 -4
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +4 -1
- package/dist/cjs/local-storage.cjs +15 -28
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +15 -2
- package/dist/cjs/query/live/collection-config-builder.cjs +21 -2
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +6 -1
- package/dist/cjs/query/live/collection-registry.cjs +2 -1
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
- package/dist/cjs/query/live/collection-registry.d.cts +1 -1
- package/dist/cjs/query/live/internal.cjs +5 -0
- package/dist/cjs/query/live/internal.cjs.map +1 -0
- package/dist/cjs/query/live/internal.d.cts +13 -0
- package/dist/cjs/types.d.cts +2 -2
- package/dist/esm/collection/index.js +1 -1
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/sync.js +7 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +4 -1
- package/dist/esm/errors.js +9 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/local-storage.js +15 -28
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +15 -2
- package/dist/esm/query/live/collection-config-builder.d.ts +6 -1
- package/dist/esm/query/live/collection-config-builder.js +21 -2
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +1 -1
- package/dist/esm/query/live/collection-registry.js +2 -1
- package/dist/esm/query/live/collection-registry.js.map +1 -1
- package/dist/esm/query/live/internal.d.ts +13 -0
- package/dist/esm/query/live/internal.js +5 -0
- package/dist/esm/query/live/internal.js.map +1 -0
- package/dist/esm/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/collection/index.ts +2 -2
- package/src/collection/sync.ts +9 -1
- package/src/errors.ts +20 -4
- package/src/local-storage.ts +28 -45
- package/src/query/builder/types.ts +16 -2
- package/src/query/live/collection-config-builder.ts +27 -2
- package/src/query/live/collection-registry.ts +3 -2
- package/src/query/live/internal.ts +15 -0
- package/src/types.ts +2 -2
package/src/collection/sync.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SyncTransactionAlreadyCommittedWriteError,
|
|
10
10
|
} from "../errors"
|
|
11
11
|
import { deepEquals } from "../utils"
|
|
12
|
+
import { LIVE_QUERY_INTERNAL } from "../query/live/internal.js"
|
|
12
13
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
13
14
|
import type {
|
|
14
15
|
ChangeMessage,
|
|
@@ -21,6 +22,7 @@ import type { CollectionImpl } from "./index.js"
|
|
|
21
22
|
import type { CollectionStateManager } from "./state"
|
|
22
23
|
import type { CollectionLifecycleManager } from "./lifecycle"
|
|
23
24
|
import type { CollectionEventsManager } from "./events.js"
|
|
25
|
+
import type { LiveQueryCollectionUtils } from "../query/live/collection-config-builder.js"
|
|
24
26
|
|
|
25
27
|
export class CollectionSyncManager<
|
|
26
28
|
TOutput extends object = Record<string, unknown>,
|
|
@@ -127,7 +129,13 @@ export class CollectionSyncManager<
|
|
|
127
129
|
// throwing a duplicate-key error during reconciliation.
|
|
128
130
|
messageType = `update`
|
|
129
131
|
} else {
|
|
130
|
-
|
|
132
|
+
const utils = this.config
|
|
133
|
+
.utils as Partial<LiveQueryCollectionUtils>
|
|
134
|
+
const internal = utils[LIVE_QUERY_INTERNAL]
|
|
135
|
+
throw new DuplicateKeySyncError(key, this.id, {
|
|
136
|
+
hasCustomGetKey: internal?.hasCustomGetKey ?? false,
|
|
137
|
+
hasJoins: internal?.hasJoins ?? false,
|
|
138
|
+
})
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
141
|
}
|
package/src/errors.ts
CHANGED
|
@@ -160,10 +160,26 @@ export class DuplicateKeyError extends CollectionOperationError {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
export class DuplicateKeySyncError extends CollectionOperationError {
|
|
163
|
-
constructor(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
constructor(
|
|
164
|
+
key: string | number,
|
|
165
|
+
collectionId: string,
|
|
166
|
+
options?: { hasCustomGetKey?: boolean; hasJoins?: boolean }
|
|
167
|
+
) {
|
|
168
|
+
const baseMessage = `Cannot insert document with key "${key}" from sync because it already exists in the collection "${collectionId}"`
|
|
169
|
+
|
|
170
|
+
// Provide enhanced guidance when custom getKey is used with joins
|
|
171
|
+
if (options?.hasCustomGetKey && options.hasJoins) {
|
|
172
|
+
super(
|
|
173
|
+
`${baseMessage}. ` +
|
|
174
|
+
`This collection uses a custom getKey with joined queries. ` +
|
|
175
|
+
`Joined queries can produce multiple rows with the same key when relationships are not 1:1. ` +
|
|
176
|
+
`Consider: (1) using a composite key in your getKey function (e.g., \`\${item.key1}-\${item.key2}\`), ` +
|
|
177
|
+
`(2) ensuring your join produces unique rows per key, or (3) removing the custom getKey ` +
|
|
178
|
+
`to use the default composite key behavior.`
|
|
179
|
+
)
|
|
180
|
+
} else {
|
|
181
|
+
super(baseMessage)
|
|
182
|
+
}
|
|
167
183
|
}
|
|
168
184
|
}
|
|
169
185
|
|
package/src/local-storage.ts
CHANGED
|
@@ -346,16 +346,6 @@ export function localStorageCollectionOptions(
|
|
|
346
346
|
lastKnownData
|
|
347
347
|
)
|
|
348
348
|
|
|
349
|
-
/**
|
|
350
|
-
* Manual trigger function for local sync updates
|
|
351
|
-
* Forces a check for storage changes and updates the collection if needed
|
|
352
|
-
*/
|
|
353
|
-
const triggerLocalSync = () => {
|
|
354
|
-
if (sync.manualTrigger) {
|
|
355
|
-
sync.manualTrigger()
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
349
|
/**
|
|
360
350
|
* Save data to storage
|
|
361
351
|
* @param dataMap - Map of items with version tracking to save to storage
|
|
@@ -413,24 +403,24 @@ export function localStorageCollectionOptions(
|
|
|
413
403
|
}
|
|
414
404
|
|
|
415
405
|
// Always persist to storage
|
|
416
|
-
//
|
|
417
|
-
const currentData = loadFromStorage<any>(config.storageKey, storage, parser)
|
|
418
|
-
|
|
406
|
+
// Use lastKnownData (in-memory cache) instead of reading from storage
|
|
419
407
|
// Add new items with version keys
|
|
420
408
|
params.transaction.mutations.forEach((mutation) => {
|
|
421
|
-
|
|
409
|
+
// Use the engine's pre-computed key for consistency
|
|
410
|
+
const key = mutation.key
|
|
422
411
|
const storedItem: StoredItem<any> = {
|
|
423
412
|
versionKey: generateUuid(),
|
|
424
413
|
data: mutation.modified,
|
|
425
414
|
}
|
|
426
|
-
|
|
415
|
+
lastKnownData.set(key, storedItem)
|
|
427
416
|
})
|
|
428
417
|
|
|
429
418
|
// Save to storage
|
|
430
|
-
saveToStorage(
|
|
419
|
+
saveToStorage(lastKnownData)
|
|
431
420
|
|
|
432
|
-
//
|
|
433
|
-
|
|
421
|
+
// Confirm mutations through sync interface (moves from optimistic to synced state)
|
|
422
|
+
// without reloading from storage
|
|
423
|
+
sync.confirmOperationsSync(params.transaction.mutations)
|
|
434
424
|
|
|
435
425
|
return handlerResult
|
|
436
426
|
}
|
|
@@ -448,24 +438,24 @@ export function localStorageCollectionOptions(
|
|
|
448
438
|
}
|
|
449
439
|
|
|
450
440
|
// Always persist to storage
|
|
451
|
-
//
|
|
452
|
-
const currentData = loadFromStorage<any>(config.storageKey, storage, parser)
|
|
453
|
-
|
|
441
|
+
// Use lastKnownData (in-memory cache) instead of reading from storage
|
|
454
442
|
// Update items with new version keys
|
|
455
443
|
params.transaction.mutations.forEach((mutation) => {
|
|
456
|
-
|
|
444
|
+
// Use the engine's pre-computed key for consistency
|
|
445
|
+
const key = mutation.key
|
|
457
446
|
const storedItem: StoredItem<any> = {
|
|
458
447
|
versionKey: generateUuid(),
|
|
459
448
|
data: mutation.modified,
|
|
460
449
|
}
|
|
461
|
-
|
|
450
|
+
lastKnownData.set(key, storedItem)
|
|
462
451
|
})
|
|
463
452
|
|
|
464
453
|
// Save to storage
|
|
465
|
-
saveToStorage(
|
|
454
|
+
saveToStorage(lastKnownData)
|
|
466
455
|
|
|
467
|
-
//
|
|
468
|
-
|
|
456
|
+
// Confirm mutations through sync interface (moves from optimistic to synced state)
|
|
457
|
+
// without reloading from storage
|
|
458
|
+
sync.confirmOperationsSync(params.transaction.mutations)
|
|
469
459
|
|
|
470
460
|
return handlerResult
|
|
471
461
|
}
|
|
@@ -478,21 +468,20 @@ export function localStorageCollectionOptions(
|
|
|
478
468
|
}
|
|
479
469
|
|
|
480
470
|
// Always persist to storage
|
|
481
|
-
//
|
|
482
|
-
const currentData = loadFromStorage<any>(config.storageKey, storage, parser)
|
|
483
|
-
|
|
471
|
+
// Use lastKnownData (in-memory cache) instead of reading from storage
|
|
484
472
|
// Remove items
|
|
485
473
|
params.transaction.mutations.forEach((mutation) => {
|
|
486
|
-
//
|
|
487
|
-
const key =
|
|
488
|
-
|
|
474
|
+
// Use the engine's pre-computed key for consistency
|
|
475
|
+
const key = mutation.key
|
|
476
|
+
lastKnownData.delete(key)
|
|
489
477
|
})
|
|
490
478
|
|
|
491
479
|
// Save to storage
|
|
492
|
-
saveToStorage(
|
|
480
|
+
saveToStorage(lastKnownData)
|
|
493
481
|
|
|
494
|
-
//
|
|
495
|
-
|
|
482
|
+
// Confirm mutations through sync interface (moves from optimistic to synced state)
|
|
483
|
+
// without reloading from storage
|
|
484
|
+
sync.confirmOperationsSync(params.transaction.mutations)
|
|
496
485
|
|
|
497
486
|
return handlerResult
|
|
498
487
|
}
|
|
@@ -546,13 +535,7 @@ export function localStorageCollectionOptions(
|
|
|
546
535
|
}
|
|
547
536
|
}
|
|
548
537
|
|
|
549
|
-
//
|
|
550
|
-
const currentData = loadFromStorage<Record<string, unknown>>(
|
|
551
|
-
config.storageKey,
|
|
552
|
-
storage,
|
|
553
|
-
parser
|
|
554
|
-
)
|
|
555
|
-
|
|
538
|
+
// Use lastKnownData (in-memory cache) instead of reading from storage
|
|
556
539
|
// Apply each mutation
|
|
557
540
|
for (const mutation of collectionMutations) {
|
|
558
541
|
// Use the engine's pre-computed key to avoid key derivation issues
|
|
@@ -565,18 +548,18 @@ export function localStorageCollectionOptions(
|
|
|
565
548
|
versionKey: generateUuid(),
|
|
566
549
|
data: mutation.modified,
|
|
567
550
|
}
|
|
568
|
-
|
|
551
|
+
lastKnownData.set(key, storedItem)
|
|
569
552
|
break
|
|
570
553
|
}
|
|
571
554
|
case `delete`: {
|
|
572
|
-
|
|
555
|
+
lastKnownData.delete(key)
|
|
573
556
|
break
|
|
574
557
|
}
|
|
575
558
|
}
|
|
576
559
|
}
|
|
577
560
|
|
|
578
561
|
// Save to storage
|
|
579
|
-
saveToStorage(
|
|
562
|
+
saveToStorage(lastKnownData)
|
|
580
563
|
|
|
581
564
|
// Confirm the mutations in the collection to move them from optimistic to synced state
|
|
582
565
|
// This writes them through the sync interface to make them "synced" instead of "optimistic"
|
|
@@ -530,6 +530,20 @@ export type RefLeaf<T = any> = { readonly [RefBrand]?: T }
|
|
|
530
530
|
type WithoutRefBrand<T> =
|
|
531
531
|
T extends Record<string, any> ? Omit<T, typeof RefBrand> : T
|
|
532
532
|
|
|
533
|
+
/**
|
|
534
|
+
* PreserveSingleResultFlag - Conditionally includes the singleResult flag
|
|
535
|
+
*
|
|
536
|
+
* This helper type ensures the singleResult flag is only added to the context when it's
|
|
537
|
+
* explicitly true. It uses a non-distributive conditional (tuple wrapper) to prevent
|
|
538
|
+
* unexpected behavior when TFlag is a union type.
|
|
539
|
+
*
|
|
540
|
+
* @template TFlag - The singleResult flag value to check
|
|
541
|
+
* @returns { singleResult: true } if TFlag is true, otherwise {}
|
|
542
|
+
*/
|
|
543
|
+
type PreserveSingleResultFlag<TFlag> = [TFlag] extends [true]
|
|
544
|
+
? { singleResult: true }
|
|
545
|
+
: {}
|
|
546
|
+
|
|
533
547
|
/**
|
|
534
548
|
* MergeContextWithJoinType - Creates a new context after a join operation
|
|
535
549
|
*
|
|
@@ -551,6 +565,7 @@ type WithoutRefBrand<T> =
|
|
|
551
565
|
* - `hasJoins`: Set to true
|
|
552
566
|
* - `joinTypes`: Updated to track this join type
|
|
553
567
|
* - `result`: Preserved from previous operations
|
|
568
|
+
* - `singleResult`: Preserved only if already true (via PreserveSingleResultFlag)
|
|
554
569
|
*/
|
|
555
570
|
export type MergeContextWithJoinType<
|
|
556
571
|
TContext extends Context,
|
|
@@ -574,8 +589,7 @@ export type MergeContextWithJoinType<
|
|
|
574
589
|
[K in keyof TNewSchema & string]: TJoinType
|
|
575
590
|
}
|
|
576
591
|
result: TContext[`result`]
|
|
577
|
-
|
|
578
|
-
}
|
|
592
|
+
} & PreserveSingleResultFlag<TContext[`singleResult`]>
|
|
579
593
|
|
|
580
594
|
/**
|
|
581
595
|
* ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas
|
|
@@ -9,6 +9,8 @@ import { transactionScopedScheduler } from "../../scheduler.js"
|
|
|
9
9
|
import { getActiveTransaction } from "../../transactions.js"
|
|
10
10
|
import { CollectionSubscriber } from "./collection-subscriber.js"
|
|
11
11
|
import { getCollectionBuilder } from "./collection-registry.js"
|
|
12
|
+
import { LIVE_QUERY_INTERNAL } from "./internal.js"
|
|
13
|
+
import type { LiveQueryInternalUtils } from "./internal.js"
|
|
12
14
|
import type { WindowOptions } from "../compiler/index.js"
|
|
13
15
|
import type { SchedulerContextId } from "../../scheduler.js"
|
|
14
16
|
import type { CollectionSubscription } from "../../collection/subscription.js"
|
|
@@ -35,7 +37,6 @@ import type { AllCollectionEvents } from "../../collection/events.js"
|
|
|
35
37
|
|
|
36
38
|
export type LiveQueryCollectionUtils = UtilsRecord & {
|
|
37
39
|
getRunCount: () => number
|
|
38
|
-
getBuilder: () => CollectionConfigBuilder<any, any>
|
|
39
40
|
/**
|
|
40
41
|
* Sets the offset and limit of an ordered query.
|
|
41
42
|
* Is a no-op if the query is not ordered.
|
|
@@ -49,6 +50,7 @@ export type LiveQueryCollectionUtils = UtilsRecord & {
|
|
|
49
50
|
* @returns The current window settings, or `undefined` if the query is not windowed
|
|
50
51
|
*/
|
|
51
52
|
getWindow: () => { offset: number; limit: number } | undefined
|
|
53
|
+
[LIVE_QUERY_INTERNAL]: LiveQueryInternalUtils
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
type PendingGraphRun = {
|
|
@@ -173,6 +175,25 @@ export class CollectionConfigBuilder<
|
|
|
173
175
|
this.compileBasePipeline()
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Recursively checks if a query or any of its subqueries contains joins
|
|
180
|
+
*/
|
|
181
|
+
private hasJoins(query: QueryIR): boolean {
|
|
182
|
+
// Check if this query has joins
|
|
183
|
+
if (query.join && query.join.length > 0) {
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Recursively check subqueries in the from clause
|
|
188
|
+
if (query.from.type === `queryRef`) {
|
|
189
|
+
if (this.hasJoins(query.from.query)) {
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
176
197
|
getConfig(): CollectionConfigSingleRowOption<TResult> & {
|
|
177
198
|
utils: LiveQueryCollectionUtils
|
|
178
199
|
} {
|
|
@@ -192,9 +213,13 @@ export class CollectionConfigBuilder<
|
|
|
192
213
|
singleResult: this.query.singleResult,
|
|
193
214
|
utils: {
|
|
194
215
|
getRunCount: this.getRunCount.bind(this),
|
|
195
|
-
getBuilder: () => this,
|
|
196
216
|
setWindow: this.setWindow.bind(this),
|
|
197
217
|
getWindow: this.getWindow.bind(this),
|
|
218
|
+
[LIVE_QUERY_INTERNAL]: {
|
|
219
|
+
getBuilder: () => this,
|
|
220
|
+
hasCustomGetKey: !!this.config.getKey,
|
|
221
|
+
hasJoins: this.hasJoins(this.query),
|
|
222
|
+
},
|
|
198
223
|
},
|
|
199
224
|
}
|
|
200
225
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LIVE_QUERY_INTERNAL } from "./internal.js"
|
|
1
2
|
import type { Collection } from "../../collection/index.js"
|
|
2
3
|
import type { CollectionConfigBuilder } from "./collection-config-builder.js"
|
|
3
4
|
|
|
@@ -7,7 +8,7 @@ const collectionBuilderRegistry = new WeakMap<
|
|
|
7
8
|
>()
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Retrieves the builder attached to a config object via its utils.
|
|
11
|
+
* Retrieves the builder attached to a config object via its internal utils.
|
|
11
12
|
*
|
|
12
13
|
* @param config - The collection config object
|
|
13
14
|
* @returns The attached builder, or `undefined` if none exists
|
|
@@ -15,7 +16,7 @@ const collectionBuilderRegistry = new WeakMap<
|
|
|
15
16
|
export function getBuilderFromConfig(
|
|
16
17
|
config: object
|
|
17
18
|
): CollectionConfigBuilder<any, any> | undefined {
|
|
18
|
-
return (config as any).utils?.getBuilder?.()
|
|
19
|
+
return (config as any).utils?.[LIVE_QUERY_INTERNAL]?.getBuilder?.()
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CollectionConfigBuilder } from "./collection-config-builder.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Symbol for accessing internal utilities that should not be part of the public API
|
|
5
|
+
*/
|
|
6
|
+
export const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal utilities for live queries, accessible via Symbol
|
|
10
|
+
*/
|
|
11
|
+
export type LiveQueryInternalUtils = {
|
|
12
|
+
getBuilder: () => CollectionConfigBuilder<any, any>
|
|
13
|
+
hasCustomGetKey: boolean
|
|
14
|
+
hasJoins: boolean
|
|
15
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -35,9 +35,9 @@ export type TransactionState = `pending` | `persisting` | `completed` | `failed`
|
|
|
35
35
|
export type Fn = (...args: Array<any>) => any
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* A record of
|
|
38
|
+
* A record of utilities (functions or getters) that can be attached to a collection
|
|
39
39
|
*/
|
|
40
|
-
export type UtilsRecord = Record<string,
|
|
40
|
+
export type UtilsRecord = Record<string, any>
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
*
|