@tanstack/db 0.4.19 → 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/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/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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Collection } from '../../collection/index.js';
|
|
2
2
|
import { CollectionConfigBuilder } from './collection-config-builder.js';
|
|
3
3
|
/**
|
|
4
|
-
* Retrieves the builder attached to a config object via its utils.
|
|
4
|
+
* Retrieves the builder attached to a config object via its internal utils.
|
|
5
5
|
*
|
|
6
6
|
* @param config - The collection config object
|
|
7
7
|
* @returns The attached builder, or `undefined` if none exists
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { LIVE_QUERY_INTERNAL } from "./internal.js";
|
|
1
2
|
const collectionBuilderRegistry = /* @__PURE__ */ new WeakMap();
|
|
2
3
|
function getBuilderFromConfig(config) {
|
|
3
|
-
return config.utils?.getBuilder?.();
|
|
4
|
+
return config.utils?.[LIVE_QUERY_INTERNAL]?.getBuilder?.();
|
|
4
5
|
}
|
|
5
6
|
function registerCollectionBuilder(collection, builder) {
|
|
6
7
|
collectionBuilderRegistry.set(collection, builder);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-registry.js","sources":["../../../../src/query/live/collection-registry.ts"],"sourcesContent":["import type { Collection } from \"../../collection/index.js\"\nimport type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\n\nconst collectionBuilderRegistry = new WeakMap<\n Collection<any, any, any>,\n CollectionConfigBuilder<any, any>\n>()\n\n/**\n * Retrieves the builder attached to a config object via its utils
|
|
1
|
+
{"version":3,"file":"collection-registry.js","sources":["../../../../src/query/live/collection-registry.ts"],"sourcesContent":["import { LIVE_QUERY_INTERNAL } from \"./internal.js\"\nimport type { Collection } from \"../../collection/index.js\"\nimport type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\n\nconst collectionBuilderRegistry = new WeakMap<\n Collection<any, any, any>,\n CollectionConfigBuilder<any, any>\n>()\n\n/**\n * Retrieves the builder attached to a config object via its internal utils.\n *\n * @param config - The collection config object\n * @returns The attached builder, or `undefined` if none exists\n */\nexport function getBuilderFromConfig(\n config: object\n): CollectionConfigBuilder<any, any> | undefined {\n return (config as any).utils?.[LIVE_QUERY_INTERNAL]?.getBuilder?.()\n}\n\n/**\n * Registers a builder for a collection in the global registry.\n * Used to detect when a live query depends on another live query,\n * enabling the scheduler to ensure parent queries run first.\n *\n * @param collection - The collection to register the builder for\n * @param builder - The builder that produces this collection\n */\nexport function registerCollectionBuilder(\n collection: Collection<any, any, any>,\n builder: CollectionConfigBuilder<any, any>\n): void {\n collectionBuilderRegistry.set(collection, builder)\n}\n\n/**\n * Retrieves the builder registered for a collection.\n * Used to discover dependencies when a live query subscribes to another live query.\n *\n * @param collection - The collection to look up\n * @returns The registered builder, or `undefined` if none exists\n */\nexport function getCollectionBuilder(\n collection: Collection<any, any, any>\n): CollectionConfigBuilder<any, any> | undefined {\n return collectionBuilderRegistry.get(collection)\n}\n"],"names":[],"mappings":";AAIA,MAAM,gDAAgC,QAAA;AAW/B,SAAS,qBACd,QAC+C;AAC/C,SAAQ,OAAe,QAAQ,mBAAmB,GAAG,aAAA;AACvD;AAUO,SAAS,0BACd,YACA,SACM;AACN,4BAA0B,IAAI,YAAY,OAAO;AACnD;AASO,SAAS,qBACd,YAC+C;AAC/C,SAAO,0BAA0B,IAAI,UAAU;AACjD;"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CollectionConfigBuilder } from './collection-config-builder.js';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol for accessing internal utilities that should not be part of the public API
|
|
4
|
+
*/
|
|
5
|
+
export declare const LIVE_QUERY_INTERNAL: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* Internal utilities for live queries, accessible via Symbol
|
|
8
|
+
*/
|
|
9
|
+
export type LiveQueryInternalUtils = {
|
|
10
|
+
getBuilder: () => CollectionConfigBuilder<any, any>;
|
|
11
|
+
hasCustomGetKey: boolean;
|
|
12
|
+
hasJoins: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.js","sources":["../../../../src/query/live/internal.ts"],"sourcesContent":["import type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\n\n/**\n * Symbol for accessing internal utilities that should not be part of the public API\n */\nexport const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`)\n\n/**\n * Internal utilities for live queries, accessible via Symbol\n */\nexport type LiveQueryInternalUtils = {\n getBuilder: () => CollectionConfigBuilder<any, any>\n hasCustomGetKey: boolean\n hasJoins: boolean\n}\n"],"names":[],"mappings":"AAKO,MAAM,sBAAsB,OAAO,mBAAmB;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ export type TransactionState = `pending` | `persisting` | `completed` | `failed`
|
|
|
22
22
|
*/
|
|
23
23
|
export type Fn = (...args: Array<any>) => any;
|
|
24
24
|
/**
|
|
25
|
-
* A record of
|
|
25
|
+
* A record of utilities (functions or getters) that can be attached to a collection
|
|
26
26
|
*/
|
|
27
|
-
export type UtilsRecord = Record<string,
|
|
27
|
+
export type UtilsRecord = Record<string, any>;
|
|
28
28
|
/**
|
|
29
29
|
*
|
|
30
30
|
* @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity.
|
package/package.json
CHANGED
package/src/collection/index.ts
CHANGED
|
@@ -189,9 +189,9 @@ export function createCollection(
|
|
|
189
189
|
options
|
|
190
190
|
)
|
|
191
191
|
|
|
192
|
-
//
|
|
192
|
+
// Attach utils to collection
|
|
193
193
|
if (options.utils) {
|
|
194
|
-
collection.utils =
|
|
194
|
+
collection.utils = options.utils
|
|
195
195
|
} else {
|
|
196
196
|
collection.utils = {}
|
|
197
197
|
}
|
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
|
|
|
@@ -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
|
*
|