@tanstack/db 0.4.8 → 0.4.10
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/events.cjs +9 -51
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +18 -7
- package/dist/cjs/collection/index.cjs +9 -12
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +13 -14
- package/dist/cjs/collection/subscription.cjs +62 -6
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +16 -3
- package/dist/cjs/collection/sync.cjs +58 -6
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +18 -4
- package/dist/cjs/errors.cjs +59 -17
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +44 -8
- package/dist/cjs/event-emitter.cjs +94 -0
- package/dist/cjs/event-emitter.cjs.map +1 -0
- package/dist/cjs/event-emitter.d.cts +45 -0
- package/dist/cjs/index.cjs +9 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/local-only.d.cts +2 -5
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +46 -19
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +35 -9
- package/dist/cjs/query/compiler/joins.cjs +91 -66
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +6 -3
- package/dist/cjs/query/compiler/order-by.cjs +20 -4
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +3 -1
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/compiler/types.d.cts +4 -0
- package/dist/cjs/query/index.d.cts +1 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +306 -46
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +97 -9
- package/dist/cjs/query/live/collection-registry.cjs +16 -0
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
- package/dist/cjs/query/live/collection-registry.d.cts +26 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +86 -58
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +5 -7
- package/dist/cjs/query/live-query-collection.cjs +11 -5
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +12 -5
- package/dist/cjs/query/optimizer.cjs +44 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +4 -4
- package/dist/cjs/scheduler.cjs +137 -0
- package/dist/cjs/scheduler.cjs.map +1 -0
- package/dist/cjs/scheduler.d.cts +56 -0
- package/dist/cjs/transactions.cjs +7 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +82 -11
- package/dist/esm/collection/events.d.ts +18 -7
- package/dist/esm/collection/events.js +9 -51
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +13 -14
- package/dist/esm/collection/index.js +9 -12
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +16 -3
- package/dist/esm/collection/subscription.js +62 -6
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +18 -4
- package/dist/esm/collection/sync.js +59 -7
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +44 -8
- package/dist/esm/errors.js +60 -18
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/event-emitter.d.ts +45 -0
- package/dist/esm/event-emitter.js +94 -0
- package/dist/esm/event-emitter.js.map +1 -0
- package/dist/esm/index.js +10 -5
- package/dist/esm/local-only.d.ts +2 -5
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +35 -9
- package/dist/esm/query/compiler/index.js +46 -19
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +6 -3
- package/dist/esm/query/compiler/joins.js +93 -68
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +3 -1
- package/dist/esm/query/compiler/order-by.js +20 -4
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/compiler/types.d.ts +4 -0
- package/dist/esm/query/index.d.ts +1 -0
- package/dist/esm/query/live/collection-config-builder.d.ts +97 -9
- package/dist/esm/query/live/collection-config-builder.js +306 -46
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +26 -0
- package/dist/esm/query/live/collection-registry.js +16 -0
- package/dist/esm/query/live/collection-registry.js.map +1 -0
- package/dist/esm/query/live/collection-subscriber.d.ts +5 -7
- package/dist/esm/query/live/collection-subscriber.js +86 -58
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +12 -5
- package/dist/esm/query/live-query-collection.js +11 -5
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +4 -4
- package/dist/esm/query/optimizer.js +44 -7
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/scheduler.d.ts +56 -0
- package/dist/esm/scheduler.js +137 -0
- package/dist/esm/scheduler.js.map +1 -0
- package/dist/esm/transactions.js +7 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +82 -11
- package/package.json +2 -2
- package/src/collection/events.ts +25 -74
- package/src/collection/index.ts +15 -19
- package/src/collection/subscription.ts +88 -6
- package/src/collection/sync.ts +81 -9
- package/src/errors.ts +91 -13
- package/src/event-emitter.ts +118 -0
- package/src/local-only.ts +5 -12
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +124 -33
- package/src/query/compiler/joins.ts +187 -128
- package/src/query/compiler/order-by.ts +30 -2
- package/src/query/compiler/select.ts +2 -3
- package/src/query/compiler/types.ts +5 -0
- package/src/query/index.ts +1 -0
- package/src/query/live/collection-config-builder.ts +501 -60
- package/src/query/live/collection-registry.ts +47 -0
- package/src/query/live/collection-subscriber.ts +137 -105
- package/src/query/live-query-collection.ts +47 -18
- package/src/query/optimizer.ts +85 -15
- package/src/scheduler.ts +198 -0
- package/src/transactions.ts +12 -1
- package/src/types.ts +93 -11
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Collection } from "../../collection/index.js"
|
|
2
|
+
import type { CollectionConfigBuilder } from "./collection-config-builder.js"
|
|
3
|
+
|
|
4
|
+
const collectionBuilderRegistry = new WeakMap<
|
|
5
|
+
Collection<any, any, any>,
|
|
6
|
+
CollectionConfigBuilder<any, any>
|
|
7
|
+
>()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves the builder attached to a config object via its utils.getBuilder() method.
|
|
11
|
+
*
|
|
12
|
+
* @param config - The collection config object
|
|
13
|
+
* @returns The attached builder, or `undefined` if none exists
|
|
14
|
+
*/
|
|
15
|
+
export function getBuilderFromConfig(
|
|
16
|
+
config: object
|
|
17
|
+
): CollectionConfigBuilder<any, any> | undefined {
|
|
18
|
+
return (config as any).utils?.getBuilder?.()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers a builder for a collection in the global registry.
|
|
23
|
+
* Used to detect when a live query depends on another live query,
|
|
24
|
+
* enabling the scheduler to ensure parent queries run first.
|
|
25
|
+
*
|
|
26
|
+
* @param collection - The collection to register the builder for
|
|
27
|
+
* @param builder - The builder that produces this collection
|
|
28
|
+
*/
|
|
29
|
+
export function registerCollectionBuilder(
|
|
30
|
+
collection: Collection<any, any, any>,
|
|
31
|
+
builder: CollectionConfigBuilder<any, any>
|
|
32
|
+
): void {
|
|
33
|
+
collectionBuilderRegistry.set(collection, builder)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Retrieves the builder registered for a collection.
|
|
38
|
+
* Used to discover dependencies when a live query subscribes to another live query.
|
|
39
|
+
*
|
|
40
|
+
* @param collection - The collection to look up
|
|
41
|
+
* @returns The registered builder, or `undefined` if none exists
|
|
42
|
+
*/
|
|
43
|
+
export function getCollectionBuilder(
|
|
44
|
+
collection: Collection<any, any, any>
|
|
45
|
+
): CollectionConfigBuilder<any, any> | undefined {
|
|
46
|
+
return collectionBuilderRegistry.get(collection)
|
|
47
|
+
}
|
|
@@ -3,15 +3,20 @@ import {
|
|
|
3
3
|
convertOrderByToBasicExpression,
|
|
4
4
|
convertToBasicExpression,
|
|
5
5
|
} from "../compiler/expressions.js"
|
|
6
|
-
import
|
|
6
|
+
import { WhereClauseConversionError } from "../../errors.js"
|
|
7
7
|
import type { MultiSetArray, RootStreamBuilder } from "@tanstack/db-ivm"
|
|
8
8
|
import type { Collection } from "../../collection/index.js"
|
|
9
|
-
import type { ChangeMessage
|
|
9
|
+
import type { ChangeMessage } from "../../types.js"
|
|
10
10
|
import type { Context, GetResult } from "../builder/types.js"
|
|
11
11
|
import type { BasicExpression } from "../ir.js"
|
|
12
|
+
import type { OrderByOptimizationInfo } from "../compiler/order-by.js"
|
|
12
13
|
import type { CollectionConfigBuilder } from "./collection-config-builder.js"
|
|
13
14
|
import type { CollectionSubscription } from "../../collection/subscription.js"
|
|
14
15
|
|
|
16
|
+
const loadMoreCallbackSymbol = Symbol.for(
|
|
17
|
+
`@tanstack/db.collection-config-builder`
|
|
18
|
+
)
|
|
19
|
+
|
|
15
20
|
export class CollectionSubscriber<
|
|
16
21
|
TContext extends Context,
|
|
17
22
|
TResult extends object = GetResult<TContext>,
|
|
@@ -19,71 +24,106 @@ export class CollectionSubscriber<
|
|
|
19
24
|
// Keep track of the biggest value we've sent so far (needed for orderBy optimization)
|
|
20
25
|
private biggest: any = undefined
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
// Track deferred promises for subscription loading states
|
|
28
|
+
private subscriptionLoadingPromises = new Map<
|
|
29
|
+
CollectionSubscription,
|
|
30
|
+
{ resolve: () => void }
|
|
31
|
+
>()
|
|
23
32
|
|
|
24
33
|
constructor(
|
|
34
|
+
private alias: string,
|
|
25
35
|
private collectionId: string,
|
|
26
36
|
private collection: Collection,
|
|
27
|
-
private config: Parameters<SyncConfig<TResult>[`sync`]>[0],
|
|
28
|
-
private syncState: FullSyncState,
|
|
29
37
|
private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>
|
|
30
|
-
) {
|
|
31
|
-
this.collectionAlias = findCollectionAlias(
|
|
32
|
-
this.collectionId,
|
|
33
|
-
this.collectionConfigBuilder.query
|
|
34
|
-
)!
|
|
35
|
-
}
|
|
38
|
+
) {}
|
|
36
39
|
|
|
37
40
|
subscribe(): CollectionSubscription {
|
|
38
|
-
const whereClause = this.
|
|
41
|
+
const whereClause = this.getWhereClauseForAlias()
|
|
39
42
|
|
|
40
43
|
if (whereClause) {
|
|
41
|
-
|
|
42
|
-
const whereExpression = convertToBasicExpression(
|
|
43
|
-
whereClause,
|
|
44
|
-
this.collectionAlias
|
|
45
|
-
)
|
|
44
|
+
const whereExpression = convertToBasicExpression(whereClause, this.alias)
|
|
46
45
|
|
|
47
46
|
if (whereExpression) {
|
|
48
|
-
// Use index optimization for this collection
|
|
49
47
|
return this.subscribeToChanges(whereExpression)
|
|
50
|
-
} else {
|
|
51
|
-
// This should not happen - if we have a whereClause but can't create whereExpression,
|
|
52
|
-
// it indicates a bug in our optimization logic
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. ` +
|
|
55
|
-
`This indicates a bug in the query optimization logic.`
|
|
56
|
-
)
|
|
57
48
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return this.subscribeToChanges()
|
|
49
|
+
|
|
50
|
+
throw new WhereClauseConversionError(this.collectionId, this.alias)
|
|
61
51
|
}
|
|
52
|
+
|
|
53
|
+
return this.subscribeToChanges()
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {
|
|
65
57
|
let subscription: CollectionSubscription
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
const orderByInfo = this.getOrderByInfo()
|
|
59
|
+
if (orderByInfo) {
|
|
60
|
+
subscription = this.subscribeToOrderedChanges(
|
|
61
|
+
whereExpression,
|
|
62
|
+
orderByInfo
|
|
70
63
|
)
|
|
71
|
-
) {
|
|
72
|
-
subscription = this.subscribeToOrderedChanges(whereExpression)
|
|
73
64
|
} else {
|
|
74
|
-
// If the
|
|
75
|
-
const includeInitialState =
|
|
76
|
-
|
|
65
|
+
// If the source alias is lazy then we should not include the initial state
|
|
66
|
+
const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
|
|
67
|
+
this.alias
|
|
68
|
+
)
|
|
77
69
|
|
|
78
70
|
subscription = this.subscribeToMatchingChanges(
|
|
79
71
|
whereExpression,
|
|
80
72
|
includeInitialState
|
|
81
73
|
)
|
|
82
74
|
}
|
|
75
|
+
|
|
76
|
+
// Subscribe to subscription status changes to propagate loading state
|
|
77
|
+
const statusUnsubscribe = subscription.on(`status:change`, (event) => {
|
|
78
|
+
// TODO: For now we are setting this loading state whenever the subscription
|
|
79
|
+
// status changes to 'loadingSubset'. But we have discussed it only happening
|
|
80
|
+
// when the the live query has it's offset/limit changed, and that triggers the
|
|
81
|
+
// subscription to request a snapshot. This will require more work to implement,
|
|
82
|
+
// and builds on https://github.com/TanStack/db/pull/663 which this PR
|
|
83
|
+
// does not yet depend on.
|
|
84
|
+
if (event.status === `loadingSubset`) {
|
|
85
|
+
// Guard against duplicate transitions
|
|
86
|
+
if (!this.subscriptionLoadingPromises.has(subscription)) {
|
|
87
|
+
let resolve: () => void
|
|
88
|
+
const promise = new Promise<void>((res) => {
|
|
89
|
+
resolve = res
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
this.subscriptionLoadingPromises.set(subscription, {
|
|
93
|
+
resolve: resolve!,
|
|
94
|
+
})
|
|
95
|
+
this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(
|
|
96
|
+
promise
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// status is 'ready'
|
|
101
|
+
const deferred = this.subscriptionLoadingPromises.get(subscription)
|
|
102
|
+
if (deferred) {
|
|
103
|
+
// Clear the map entry FIRST (before resolving)
|
|
104
|
+
this.subscriptionLoadingPromises.delete(subscription)
|
|
105
|
+
deferred.resolve()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
83
110
|
const unsubscribe = () => {
|
|
111
|
+
// If subscription has a pending promise, resolve it before unsubscribing
|
|
112
|
+
const deferred = this.subscriptionLoadingPromises.get(subscription)
|
|
113
|
+
if (deferred) {
|
|
114
|
+
// Clear the map entry FIRST (before resolving)
|
|
115
|
+
this.subscriptionLoadingPromises.delete(subscription)
|
|
116
|
+
deferred.resolve()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
statusUnsubscribe()
|
|
84
120
|
subscription.unsubscribe()
|
|
85
121
|
}
|
|
86
|
-
|
|
122
|
+
// currentSyncState is always defined when subscribe() is called
|
|
123
|
+
// (called during sync session setup)
|
|
124
|
+
this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(
|
|
125
|
+
unsubscribe
|
|
126
|
+
)
|
|
87
127
|
return subscription
|
|
88
128
|
}
|
|
89
129
|
|
|
@@ -91,7 +131,10 @@ export class CollectionSubscriber<
|
|
|
91
131
|
changes: Iterable<ChangeMessage<any, string | number>>,
|
|
92
132
|
callback?: () => boolean
|
|
93
133
|
) {
|
|
94
|
-
|
|
134
|
+
// currentSyncState and input are always defined when this method is called
|
|
135
|
+
// (only called from active subscriptions during a sync session)
|
|
136
|
+
const input =
|
|
137
|
+
this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!
|
|
95
138
|
const sentChanges = sendChangesToInput(
|
|
96
139
|
input,
|
|
97
140
|
changes,
|
|
@@ -103,14 +146,12 @@ export class CollectionSubscriber<
|
|
|
103
146
|
// otherwise we end up in an infinite loop trying to load more data
|
|
104
147
|
const dataLoader = sentChanges > 0 ? callback : undefined
|
|
105
148
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
this.collectionConfigBuilder.
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
dataLoader
|
|
113
|
-
)
|
|
149
|
+
// We need to schedule a graph run even if there's no data to load
|
|
150
|
+
// because we need to mark the collection as ready if it's not already
|
|
151
|
+
// and that's only done in `scheduleGraphRun`
|
|
152
|
+
this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {
|
|
153
|
+
alias: this.alias,
|
|
154
|
+
})
|
|
114
155
|
}
|
|
115
156
|
|
|
116
157
|
private subscribeToMatchingChanges(
|
|
@@ -132,12 +173,11 @@ export class CollectionSubscriber<
|
|
|
132
173
|
}
|
|
133
174
|
|
|
134
175
|
private subscribeToOrderedChanges(
|
|
135
|
-
whereExpression: BasicExpression<boolean> | undefined
|
|
176
|
+
whereExpression: BasicExpression<boolean> | undefined,
|
|
177
|
+
orderByInfo: OrderByOptimizationInfo
|
|
136
178
|
) {
|
|
137
179
|
const { orderBy, offset, limit, comparator, dataNeeded, index } =
|
|
138
|
-
|
|
139
|
-
this.collectionId
|
|
140
|
-
]!
|
|
180
|
+
orderByInfo
|
|
141
181
|
|
|
142
182
|
const sendChangesInRange = (
|
|
143
183
|
changes: Iterable<ChangeMessage<any, string | number>>
|
|
@@ -147,7 +187,7 @@ export class CollectionSubscriber<
|
|
|
147
187
|
// because they can't affect the topK (and if later we need more data, we will dynamically load more data)
|
|
148
188
|
const splittedChanges = splitUpdates(changes)
|
|
149
189
|
let filteredChanges = splittedChanges
|
|
150
|
-
if (dataNeeded
|
|
190
|
+
if (dataNeeded && dataNeeded() === 0) {
|
|
151
191
|
// If the topK is full [..., maxSentValue] then we do not need to send changes > maxSentValue
|
|
152
192
|
// because they can never make it into the topK.
|
|
153
193
|
// However, if the topK isn't full yet, we need to also send changes > maxSentValue
|
|
@@ -173,7 +213,7 @@ export class CollectionSubscriber<
|
|
|
173
213
|
// Normalize the orderBy clauses such that the references are relative to the collection
|
|
174
214
|
const normalizedOrderBy = convertOrderByToBasicExpression(
|
|
175
215
|
orderBy,
|
|
176
|
-
this.
|
|
216
|
+
this.alias
|
|
177
217
|
)
|
|
178
218
|
|
|
179
219
|
// Load the first `offset + limit` values from the index
|
|
@@ -190,10 +230,7 @@ export class CollectionSubscriber<
|
|
|
190
230
|
// after each iteration of the query pipeline
|
|
191
231
|
// to ensure that the orderBy operator has enough data to work with
|
|
192
232
|
loadMoreIfNeeded(subscription: CollectionSubscription) {
|
|
193
|
-
const orderByInfo =
|
|
194
|
-
this.collectionConfigBuilder.optimizableOrderByCollections[
|
|
195
|
-
this.collectionId
|
|
196
|
-
]
|
|
233
|
+
const orderByInfo = this.getOrderByInfo()
|
|
197
234
|
|
|
198
235
|
if (!orderByInfo) {
|
|
199
236
|
// This query has no orderBy operator
|
|
@@ -224,24 +261,40 @@ export class CollectionSubscriber<
|
|
|
224
261
|
changes: Iterable<ChangeMessage<any, string | number>>,
|
|
225
262
|
subscription: CollectionSubscription
|
|
226
263
|
) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
264
|
+
const orderByInfo = this.getOrderByInfo()
|
|
265
|
+
if (!orderByInfo) {
|
|
266
|
+
this.sendChangesToPipeline(changes)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)
|
|
271
|
+
|
|
272
|
+
// Cache the loadMoreIfNeeded callback on the subscription using a symbol property.
|
|
273
|
+
// This ensures we pass the same function instance to the scheduler each time,
|
|
274
|
+
// allowing it to deduplicate callbacks when multiple changes arrive during a transaction.
|
|
275
|
+
type SubscriptionWithLoader = CollectionSubscription & {
|
|
276
|
+
[loadMoreCallbackSymbol]?: () => boolean
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const subscriptionWithLoader = subscription as SubscriptionWithLoader
|
|
280
|
+
|
|
281
|
+
subscriptionWithLoader[loadMoreCallbackSymbol] ??=
|
|
282
|
+
this.loadMoreIfNeeded.bind(this, subscription)
|
|
283
|
+
|
|
232
284
|
this.sendChangesToPipeline(
|
|
233
285
|
trackedChanges,
|
|
234
|
-
|
|
286
|
+
subscriptionWithLoader[loadMoreCallbackSymbol]
|
|
235
287
|
)
|
|
236
288
|
}
|
|
237
289
|
|
|
238
290
|
// Loads the next `n` items from the collection
|
|
239
291
|
// starting from the biggest item it has sent
|
|
240
292
|
private loadNextItems(n: number, subscription: CollectionSubscription) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
293
|
+
const orderByInfo = this.getOrderByInfo()
|
|
294
|
+
if (!orderByInfo) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
const { orderBy, valueExtractorForRawRow } = orderByInfo
|
|
245
298
|
const biggestSentRow = this.biggest
|
|
246
299
|
const biggestSentValue = biggestSentRow
|
|
247
300
|
? valueExtractorForRawRow(biggestSentRow)
|
|
@@ -250,7 +303,7 @@ export class CollectionSubscriber<
|
|
|
250
303
|
// Normalize the orderBy clauses such that the references are relative to the collection
|
|
251
304
|
const normalizedOrderBy = convertOrderByToBasicExpression(
|
|
252
305
|
orderBy,
|
|
253
|
-
this.
|
|
306
|
+
this.alias
|
|
254
307
|
)
|
|
255
308
|
|
|
256
309
|
// Take the `n` items after the biggest sent value
|
|
@@ -261,13 +314,22 @@ export class CollectionSubscriber<
|
|
|
261
314
|
})
|
|
262
315
|
}
|
|
263
316
|
|
|
264
|
-
private
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
317
|
+
private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {
|
|
318
|
+
const sourceWhereClausesCache =
|
|
319
|
+
this.collectionConfigBuilder.sourceWhereClausesCache
|
|
320
|
+
if (!sourceWhereClausesCache) {
|
|
321
|
+
return undefined
|
|
322
|
+
}
|
|
323
|
+
return sourceWhereClausesCache.get(this.alias)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private getOrderByInfo(): OrderByOptimizationInfo | undefined {
|
|
327
|
+
const info =
|
|
328
|
+
this.collectionConfigBuilder.optimizableOrderByCollections[
|
|
329
|
+
this.collectionId
|
|
330
|
+
]
|
|
331
|
+
if (info && info.alias === this.alias) {
|
|
332
|
+
return info
|
|
271
333
|
}
|
|
272
334
|
return undefined
|
|
273
335
|
}
|
|
@@ -288,36 +350,6 @@ export class CollectionSubscriber<
|
|
|
288
350
|
}
|
|
289
351
|
}
|
|
290
352
|
|
|
291
|
-
/**
|
|
292
|
-
* Finds the alias for a collection ID in the query
|
|
293
|
-
*/
|
|
294
|
-
function findCollectionAlias(
|
|
295
|
-
collectionId: string,
|
|
296
|
-
query: any
|
|
297
|
-
): string | undefined {
|
|
298
|
-
// Check FROM clause
|
|
299
|
-
if (
|
|
300
|
-
query.from?.type === `collectionRef` &&
|
|
301
|
-
query.from.collection?.id === collectionId
|
|
302
|
-
) {
|
|
303
|
-
return query.from.alias
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Check JOIN clauses
|
|
307
|
-
if (query.join) {
|
|
308
|
-
for (const joinClause of query.join) {
|
|
309
|
-
if (
|
|
310
|
-
joinClause.from?.type === `collectionRef` &&
|
|
311
|
-
joinClause.from.collection?.id === collectionId
|
|
312
|
-
) {
|
|
313
|
-
return joinClause.from.alias
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return undefined
|
|
319
|
-
}
|
|
320
|
-
|
|
321
353
|
/**
|
|
322
354
|
* Helper function to send changes to a D2 input stream
|
|
323
355
|
*/
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createCollection } from "../collection/index.js"
|
|
2
2
|
import { CollectionConfigBuilder } from "./live/collection-config-builder.js"
|
|
3
|
+
import {
|
|
4
|
+
getBuilderFromConfig,
|
|
5
|
+
registerCollectionBuilder,
|
|
6
|
+
} from "./live/collection-registry.js"
|
|
7
|
+
import type { LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
|
|
3
8
|
import type { LiveQueryCollectionConfig } from "./live/types.js"
|
|
4
9
|
import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
|
|
5
10
|
import type { Collection } from "../collection/index.js"
|
|
@@ -15,16 +20,20 @@ import type { Context, GetResult } from "./builder/types.js"
|
|
|
15
20
|
type CollectionConfigForContext<
|
|
16
21
|
TContext extends Context,
|
|
17
22
|
TResult extends object,
|
|
23
|
+
TUtils extends UtilsRecord = {},
|
|
18
24
|
> = TContext extends SingleResult
|
|
19
|
-
? CollectionConfigSingleRowOption<TResult> &
|
|
20
|
-
|
|
25
|
+
? CollectionConfigSingleRowOption<TResult, string | number, never, TUtils> &
|
|
26
|
+
SingleResult
|
|
27
|
+
: CollectionConfigSingleRowOption<TResult, string | number, never, TUtils> &
|
|
28
|
+
NonSingleResult
|
|
21
29
|
|
|
22
30
|
type CollectionForContext<
|
|
23
31
|
TContext extends Context,
|
|
24
32
|
TResult extends object,
|
|
33
|
+
TUtils extends UtilsRecord = {},
|
|
25
34
|
> = TContext extends SingleResult
|
|
26
|
-
? Collection<TResult> & SingleResult
|
|
27
|
-
: Collection<TResult> & NonSingleResult
|
|
35
|
+
? Collection<TResult, string | number, TUtils> & SingleResult
|
|
36
|
+
: Collection<TResult, string | number, TUtils> & NonSingleResult
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
39
|
* Creates live query collection options for use with createCollection
|
|
@@ -55,7 +64,9 @@ export function liveQueryCollectionOptions<
|
|
|
55
64
|
TResult extends object = GetResult<TContext>,
|
|
56
65
|
>(
|
|
57
66
|
config: LiveQueryCollectionConfig<TContext, TResult>
|
|
58
|
-
): CollectionConfigForContext<TContext, TResult> {
|
|
67
|
+
): CollectionConfigForContext<TContext, TResult> & {
|
|
68
|
+
utils: LiveQueryCollectionUtils
|
|
69
|
+
} {
|
|
59
70
|
const collectionConfigBuilder = new CollectionConfigBuilder<
|
|
60
71
|
TContext,
|
|
61
72
|
TResult
|
|
@@ -63,7 +74,7 @@ export function liveQueryCollectionOptions<
|
|
|
63
74
|
return collectionConfigBuilder.getConfig() as CollectionConfigForContext<
|
|
64
75
|
TContext,
|
|
65
76
|
TResult
|
|
66
|
-
>
|
|
77
|
+
> & { utils: LiveQueryCollectionUtils }
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
/**
|
|
@@ -106,7 +117,9 @@ export function createLiveQueryCollection<
|
|
|
106
117
|
TResult extends object = GetResult<TContext>,
|
|
107
118
|
>(
|
|
108
119
|
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
|
|
109
|
-
): CollectionForContext<TContext, TResult>
|
|
120
|
+
): CollectionForContext<TContext, TResult> & {
|
|
121
|
+
utils: LiveQueryCollectionUtils
|
|
122
|
+
}
|
|
110
123
|
|
|
111
124
|
// Overload 2: Accept full config object with optional utilities
|
|
112
125
|
export function createLiveQueryCollection<
|
|
@@ -115,7 +128,9 @@ export function createLiveQueryCollection<
|
|
|
115
128
|
TUtils extends UtilsRecord = {},
|
|
116
129
|
>(
|
|
117
130
|
config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
|
|
118
|
-
): CollectionForContext<TContext, TResult>
|
|
131
|
+
): CollectionForContext<TContext, TResult> & {
|
|
132
|
+
utils: LiveQueryCollectionUtils & TUtils
|
|
133
|
+
}
|
|
119
134
|
|
|
120
135
|
// Implementation
|
|
121
136
|
export function createLiveQueryCollection<
|
|
@@ -126,7 +141,9 @@ export function createLiveQueryCollection<
|
|
|
126
141
|
configOrQuery:
|
|
127
142
|
| (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
|
|
128
143
|
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
|
|
129
|
-
): CollectionForContext<TContext, TResult> {
|
|
144
|
+
): CollectionForContext<TContext, TResult> & {
|
|
145
|
+
utils: LiveQueryCollectionUtils & TUtils
|
|
146
|
+
} {
|
|
130
147
|
// Determine if the argument is a function (query) or a config object
|
|
131
148
|
if (typeof configOrQuery === `function`) {
|
|
132
149
|
// Simple query function case
|
|
@@ -139,7 +156,7 @@ export function createLiveQueryCollection<
|
|
|
139
156
|
return bridgeToCreateCollection(options) as CollectionForContext<
|
|
140
157
|
TContext,
|
|
141
158
|
TResult
|
|
142
|
-
>
|
|
159
|
+
> & { utils: LiveQueryCollectionUtils & TUtils }
|
|
143
160
|
} else {
|
|
144
161
|
// Config object case
|
|
145
162
|
const config = configOrQuery as LiveQueryCollectionConfig<
|
|
@@ -147,10 +164,16 @@ export function createLiveQueryCollection<
|
|
|
147
164
|
TResult
|
|
148
165
|
> & { utils?: TUtils }
|
|
149
166
|
const options = liveQueryCollectionOptions<TContext, TResult>(config)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
|
|
168
|
+
// Merge custom utils if provided, preserving the getBuilder() method for dependency tracking
|
|
169
|
+
if (config.utils) {
|
|
170
|
+
options.utils = { ...options.utils, ...config.utils }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return bridgeToCreateCollection(options) as CollectionForContext<
|
|
174
|
+
TContext,
|
|
175
|
+
TResult
|
|
176
|
+
> & { utils: LiveQueryCollectionUtils & TUtils }
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
|
|
@@ -162,12 +185,18 @@ function bridgeToCreateCollection<
|
|
|
162
185
|
TResult extends object,
|
|
163
186
|
TUtils extends UtilsRecord = {},
|
|
164
187
|
>(
|
|
165
|
-
options: CollectionConfig<TResult> & { utils
|
|
188
|
+
options: CollectionConfig<TResult> & { utils: TUtils }
|
|
166
189
|
): Collection<TResult, string | number, TUtils> {
|
|
167
|
-
|
|
168
|
-
return createCollection(options as any) as unknown as Collection<
|
|
190
|
+
const collection = createCollection(options as any) as unknown as Collection<
|
|
169
191
|
TResult,
|
|
170
192
|
string | number,
|
|
171
|
-
|
|
193
|
+
LiveQueryCollectionUtils
|
|
172
194
|
>
|
|
195
|
+
|
|
196
|
+
const builder = getBuilderFromConfig(options)
|
|
197
|
+
if (builder) {
|
|
198
|
+
registerCollectionBuilder(collection, builder)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return collection as unknown as Collection<TResult, string | number, TUtils>
|
|
173
202
|
}
|