@tanstack/db 0.5.32 → 0.6.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/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +3 -2
- package/dist/cjs/collection/changes.cjs +13 -4
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +10 -1
- package/dist/cjs/collection/cleanup-queue.cjs +89 -0
- package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
- package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
- package/dist/cjs/collection/events.cjs +14 -0
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +39 -1
- package/dist/cjs/collection/index.cjs +66 -28
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +49 -36
- package/dist/cjs/collection/indexes.cjs +211 -62
- package/dist/cjs/collection/indexes.cjs.map +1 -1
- package/dist/cjs/collection/indexes.d.cts +27 -17
- package/dist/cjs/collection/lifecycle.cjs +5 -22
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +0 -1
- package/dist/cjs/collection/mutations.cjs +18 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/mutations.d.cts +1 -0
- package/dist/cjs/collection/state.cjs +381 -53
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +65 -1
- package/dist/cjs/collection/subscription.cjs +6 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +4 -0
- package/dist/cjs/collection/sync.cjs +108 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +2 -0
- package/dist/cjs/collection/transaction-metadata.cjs +5 -0
- package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
- package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +24 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +12 -3
- package/dist/cjs/indexes/auto-index.cjs +13 -6
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.cjs +0 -3
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -6
- package/dist/cjs/indexes/basic-index.cjs +361 -0
- package/dist/cjs/indexes/basic-index.cjs.map +1 -0
- package/dist/cjs/indexes/basic-index.d.cts +102 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/indexes/index-options.d.cts +8 -9
- package/dist/cjs/indexes/index-registry.cjs +89 -0
- package/dist/cjs/indexes/index-registry.cjs.map +1 -0
- package/dist/cjs/indexes/index-registry.d.cts +61 -0
- package/dist/cjs/local-only.cjs +5 -0
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +27 -11
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +25 -3
- package/dist/cjs/query/builder/index.cjs +200 -39
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +4 -3
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
- package/dist/cjs/query/builder/types.d.cts +84 -19
- package/dist/cjs/query/compiler/evaluators.cjs +51 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +100 -28
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +4 -2
- package/dist/cjs/query/compiler/index.cjs +283 -11
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +30 -2
- package/dist/cjs/query/compiler/order-by.cjs +29 -10
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/compiler/select.cjs +8 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/effect.cjs +602 -0
- package/dist/cjs/query/effect.cjs.map +1 -0
- package/dist/cjs/query/effect.d.cts +94 -0
- package/dist/cjs/query/index.d.cts +2 -1
- package/dist/cjs/query/ir.cjs +18 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +21 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +493 -66
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +33 -100
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +0 -1
- package/dist/cjs/query/live/types.d.cts +3 -3
- package/dist/cjs/query/live/utils.cjs +219 -0
- package/dist/cjs/query/live/utils.cjs.map +1 -0
- package/dist/cjs/query/live/utils.d.cts +110 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +9 -6
- package/dist/cjs/query/query-once.cjs.map +1 -1
- package/dist/cjs/query/query-once.d.cts +7 -5
- package/dist/cjs/query/subset-dedupe.cjs +9 -3
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/cjs/types.d.cts +42 -8
- package/dist/cjs/utils/array-utils.cjs +27 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +16 -0
- package/dist/cjs/utils/comparison.cjs +11 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.cjs +4 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils.cjs +7 -9
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +6 -1
- package/dist/cjs/virtual-props.cjs +33 -0
- package/dist/cjs/virtual-props.cjs.map +1 -0
- package/dist/cjs/virtual-props.d.cts +196 -0
- package/dist/esm/collection/change-events.d.ts +3 -2
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.d.ts +10 -1
- package/dist/esm/collection/changes.js +13 -4
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/cleanup-queue.d.ts +30 -0
- package/dist/esm/collection/cleanup-queue.js +89 -0
- package/dist/esm/collection/cleanup-queue.js.map +1 -0
- package/dist/esm/collection/events.d.ts +39 -1
- package/dist/esm/collection/events.js +14 -0
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +49 -36
- package/dist/esm/collection/index.js +67 -29
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/indexes.d.ts +27 -17
- package/dist/esm/collection/indexes.js +211 -62
- package/dist/esm/collection/indexes.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +0 -1
- package/dist/esm/collection/lifecycle.js +5 -22
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.d.ts +1 -0
- package/dist/esm/collection/mutations.js +18 -0
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/state.d.ts +65 -1
- package/dist/esm/collection/state.js +381 -53
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +4 -0
- package/dist/esm/collection/subscription.js +6 -0
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +2 -0
- package/dist/esm/collection/sync.js +108 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/collection/transaction-metadata.d.ts +1 -0
- package/dist/esm/collection/transaction-metadata.js +5 -0
- package/dist/esm/collection/transaction-metadata.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +12 -3
- package/dist/esm/index.js +27 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +13 -6
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -6
- package/dist/esm/indexes/base-index.js +1 -4
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/basic-index.d.ts +102 -0
- package/dist/esm/indexes/basic-index.js +361 -0
- package/dist/esm/indexes/basic-index.js.map +1 -0
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +8 -9
- package/dist/esm/indexes/index-registry.d.ts +61 -0
- package/dist/esm/indexes/index-registry.js +89 -0
- package/dist/esm/indexes/index-registry.js.map +1 -0
- package/dist/esm/local-only.js +5 -0
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +25 -3
- package/dist/esm/query/builder/functions.js +27 -11
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +4 -3
- package/dist/esm/query/builder/index.js +201 -40
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +84 -19
- package/dist/esm/query/compiler/evaluators.js +51 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +4 -2
- package/dist/esm/query/compiler/group-by.js +101 -29
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +30 -2
- package/dist/esm/query/compiler/index.js +285 -13
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js +30 -11
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js +8 -0
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/effect.d.ts +94 -0
- package/dist/esm/query/effect.js +602 -0
- package/dist/esm/query/effect.js.map +1 -0
- package/dist/esm/query/index.d.ts +2 -1
- package/dist/esm/query/ir.d.ts +21 -1
- package/dist/esm/query/ir.js +18 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
- package/dist/esm/query/live/collection-config-builder.js +492 -65
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +0 -1
- package/dist/esm/query/live/collection-subscriber.js +31 -98
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +3 -3
- package/dist/esm/query/live/utils.d.ts +110 -0
- package/dist/esm/query/live/utils.js +219 -0
- package/dist/esm/query/live/utils.js.map +1 -0
- package/dist/esm/query/live-query-collection.d.ts +9 -6
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/query-once.d.ts +7 -5
- package/dist/esm/query/query-once.js.map +1 -1
- package/dist/esm/query/subset-dedupe.js +9 -3
- package/dist/esm/query/subset-dedupe.js.map +1 -1
- package/dist/esm/types.d.ts +42 -8
- package/dist/esm/utils/array-utils.d.ts +16 -0
- package/dist/esm/utils/array-utils.js +27 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.js +11 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.js +4 -0
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/dist/esm/utils.d.ts +6 -1
- package/dist/esm/utils.js +7 -9
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/virtual-props.d.ts +196 -0
- package/dist/esm/virtual-props.js +33 -0
- package/dist/esm/virtual-props.js.map +1 -0
- package/package.json +2 -2
- package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
- package/src/collection/change-events.ts +13 -9
- package/src/collection/changes.ts +30 -7
- package/src/collection/cleanup-queue.ts +105 -0
- package/src/collection/events.ts +65 -0
- package/src/collection/index.ts +110 -45
- package/src/collection/indexes.ts +283 -76
- package/src/collection/lifecycle.ts +5 -26
- package/src/collection/mutations.ts +21 -0
- package/src/collection/state.ts +545 -71
- package/src/collection/subscription.ts +7 -0
- package/src/collection/sync.ts +137 -0
- package/src/collection/transaction-metadata.ts +1 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +57 -3
- package/src/indexes/auto-index.ts +18 -8
- package/src/indexes/base-index.ts +2 -10
- package/src/indexes/basic-index.ts +507 -0
- package/src/indexes/btree-index.ts +1 -1
- package/src/indexes/index-options.ts +17 -37
- package/src/indexes/index-registry.ts +174 -0
- package/src/local-only.ts +7 -0
- package/src/query/builder/functions.ts +84 -7
- package/src/query/builder/index.ts +329 -9
- package/src/query/builder/ref-proxy.ts +22 -4
- package/src/query/builder/types.ts +257 -62
- package/src/query/compiler/evaluators.ts +57 -0
- package/src/query/compiler/group-by.ts +156 -35
- package/src/query/compiler/index.ts +445 -15
- package/src/query/compiler/order-by.ts +51 -12
- package/src/query/compiler/select.ts +9 -0
- package/src/query/effect.ts +1119 -0
- package/src/query/index.ts +7 -0
- package/src/query/ir.ts +23 -2
- package/src/query/live/collection-config-builder.ts +778 -104
- package/src/query/live/collection-subscriber.ts +40 -156
- package/src/query/live/types.ts +10 -4
- package/src/query/live/utils.ts +417 -0
- package/src/query/live-query-collection.ts +43 -18
- package/src/query/query-once.ts +31 -12
- package/src/query/subset-dedupe.ts +11 -7
- package/src/types.ts +49 -9
- package/src/utils/array-utils.ts +49 -0
- package/src/utils/comparison.ts +14 -0
- package/src/utils/index-optimization.ts +4 -0
- package/src/utils.ts +12 -9
- package/src/virtual-props.ts +282 -0
- package/dist/cjs/indexes/lazy-index.cjs +0 -190
- package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
- package/dist/cjs/indexes/lazy-index.d.cts +0 -96
- package/dist/esm/indexes/lazy-index.d.ts +0 -96
- package/dist/esm/indexes/lazy-index.js +0 -190
- package/dist/esm/indexes/lazy-index.js.map +0 -1
- package/src/indexes/lazy-index.ts +0 -251
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { MultiSet, serializeValue } from '@tanstack/db-ivm'
|
|
2
1
|
import {
|
|
3
2
|
normalizeExpressionPaths,
|
|
4
3
|
normalizeOrderByPaths,
|
|
5
4
|
} from '../compiler/expressions.js'
|
|
6
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
computeOrderedLoadCursor,
|
|
7
|
+
computeSubscriptionOrderByHints,
|
|
8
|
+
filterDuplicateInserts,
|
|
9
|
+
sendChangesToInput,
|
|
10
|
+
splitUpdates,
|
|
11
|
+
trackBiggestSentValue,
|
|
12
|
+
} from './utils.js'
|
|
7
13
|
import type { Collection } from '../../collection/index.js'
|
|
8
14
|
import type {
|
|
9
15
|
ChangeMessage,
|
|
@@ -147,25 +153,11 @@ export class CollectionSubscriber<
|
|
|
147
153
|
changes: Iterable<ChangeMessage<any, string | number>>,
|
|
148
154
|
callback?: () => boolean,
|
|
149
155
|
) {
|
|
150
|
-
// Filter changes to prevent duplicate inserts to D2 pipeline.
|
|
151
|
-
// This ensures D2 multiplicity stays at 1 for visible items, so deletes
|
|
152
|
-
// properly reduce multiplicity to 0 (triggering DELETE output).
|
|
153
156
|
const changesArray = Array.isArray(changes) ? changes : [...changes]
|
|
154
|
-
const filteredChanges
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Skip duplicate insert - already sent to D2
|
|
159
|
-
continue
|
|
160
|
-
}
|
|
161
|
-
this.sentToD2Keys.add(change.key)
|
|
162
|
-
} else if (change.type === `delete`) {
|
|
163
|
-
// Remove from tracking so future re-inserts are allowed
|
|
164
|
-
this.sentToD2Keys.delete(change.key)
|
|
165
|
-
}
|
|
166
|
-
// Updates are handled as delete+insert by splitUpdates, so no special handling needed
|
|
167
|
-
filteredChanges.push(change)
|
|
168
|
-
}
|
|
157
|
+
const filteredChanges = filterDuplicateInserts(
|
|
158
|
+
changesArray,
|
|
159
|
+
this.sentToD2Keys,
|
|
160
|
+
)
|
|
169
161
|
|
|
170
162
|
// currentSyncState and input are always defined when this method is called
|
|
171
163
|
// (only called from active subscriptions during a sync session)
|
|
@@ -202,27 +194,10 @@ export class CollectionSubscriber<
|
|
|
202
194
|
}
|
|
203
195
|
|
|
204
196
|
// Get the query's orderBy and limit to pass to loadSubset.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
limit !== undefined && offset !== undefined ? limit + offset : limit
|
|
210
|
-
const normalizedOrderBy = orderBy
|
|
211
|
-
? normalizeOrderByPaths(orderBy, this.alias)
|
|
212
|
-
: undefined
|
|
213
|
-
const canPassOrderBy =
|
|
214
|
-
normalizedOrderBy?.every((clause) => {
|
|
215
|
-
const exp = clause.expression
|
|
216
|
-
if (exp.type !== `ref`) {
|
|
217
|
-
return false
|
|
218
|
-
}
|
|
219
|
-
const path = exp.path
|
|
220
|
-
return Array.isArray(path) && path.length === 1
|
|
221
|
-
}) ?? false
|
|
222
|
-
const orderByForSubscription = canPassOrderBy
|
|
223
|
-
? normalizedOrderBy
|
|
224
|
-
: undefined
|
|
225
|
-
const limitForSubscription = canPassOrderBy ? effectiveLimit : undefined
|
|
197
|
+
const hints = computeSubscriptionOrderByHints(
|
|
198
|
+
this.collectionConfigBuilder.query,
|
|
199
|
+
this.alias,
|
|
200
|
+
)
|
|
226
201
|
|
|
227
202
|
// Track loading via the loadSubset promise directly.
|
|
228
203
|
// requestSnapshot uses trackLoadSubsetPromise: false (needed for truncate handling),
|
|
@@ -241,8 +216,8 @@ export class CollectionSubscriber<
|
|
|
241
216
|
...(includeInitialState && { includeInitialState }),
|
|
242
217
|
whereExpression,
|
|
243
218
|
onStatusChange,
|
|
244
|
-
orderBy:
|
|
245
|
-
limit:
|
|
219
|
+
orderBy: hints.orderBy,
|
|
220
|
+
limit: hints.limit,
|
|
246
221
|
onLoadSubsetResult,
|
|
247
222
|
})
|
|
248
223
|
|
|
@@ -415,52 +390,28 @@ export class CollectionSubscriber<
|
|
|
415
390
|
if (!orderByInfo) {
|
|
416
391
|
return
|
|
417
392
|
}
|
|
418
|
-
const { orderBy, valueExtractorForRawRow, offset } = orderByInfo
|
|
419
|
-
const biggestSentRow = this.biggest
|
|
420
|
-
|
|
421
|
-
// Extract all orderBy column values from the biggest sent row
|
|
422
|
-
// For single-column: returns single value, for multi-column: returns array
|
|
423
|
-
const extractedValues = biggestSentRow
|
|
424
|
-
? valueExtractorForRawRow(biggestSentRow)
|
|
425
|
-
: undefined
|
|
426
|
-
|
|
427
|
-
// Normalize to array format for minValues
|
|
428
|
-
let minValues: Array<unknown> | undefined
|
|
429
|
-
if (extractedValues !== undefined) {
|
|
430
|
-
minValues = Array.isArray(extractedValues)
|
|
431
|
-
? extractedValues
|
|
432
|
-
: [extractedValues]
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const loadRequestKey = this.getLoadRequestKey({
|
|
436
|
-
minValues,
|
|
437
|
-
offset,
|
|
438
|
-
limit: n,
|
|
439
|
-
})
|
|
440
393
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
394
|
+
const cursor = computeOrderedLoadCursor(
|
|
395
|
+
orderByInfo,
|
|
396
|
+
this.biggest,
|
|
397
|
+
this.lastLoadRequestKey,
|
|
398
|
+
this.alias,
|
|
399
|
+
n,
|
|
400
|
+
)
|
|
401
|
+
if (!cursor) return // Duplicate request — skip
|
|
447
402
|
|
|
448
|
-
|
|
449
|
-
const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
|
|
403
|
+
this.lastLoadRequestKey = cursor.loadRequestKey
|
|
450
404
|
|
|
451
405
|
// Take the `n` items after the biggest sent value
|
|
452
|
-
//
|
|
406
|
+
// Omit offset so requestLimitedSnapshot can advance based on
|
|
407
|
+
// the number of rows already loaded (supports offset-based backends).
|
|
453
408
|
subscription.requestLimitedSnapshot({
|
|
454
|
-
orderBy: normalizedOrderBy,
|
|
409
|
+
orderBy: cursor.normalizedOrderBy,
|
|
455
410
|
limit: n,
|
|
456
|
-
minValues,
|
|
457
|
-
// Omit offset so requestLimitedSnapshot can advance the offset based on
|
|
458
|
-
// the number of rows already loaded (supports offset-based backends).
|
|
411
|
+
minValues: cursor.minValues,
|
|
459
412
|
trackLoadSubsetPromise: false,
|
|
460
413
|
onLoadSubsetResult: this.orderedLoadSubsetResult,
|
|
461
414
|
})
|
|
462
|
-
|
|
463
|
-
this.lastLoadRequestKey = loadRequestKey
|
|
464
415
|
}
|
|
465
416
|
|
|
466
417
|
private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {
|
|
@@ -487,24 +438,15 @@ export class CollectionSubscriber<
|
|
|
487
438
|
changes: Array<ChangeMessage<any, string | number>>,
|
|
488
439
|
comparator: (a: any, b: any) => number,
|
|
489
440
|
): void {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
this.biggest = change.value
|
|
500
|
-
this.lastLoadRequestKey = undefined
|
|
501
|
-
} else if (comparator(this.biggest, change.value) < 0) {
|
|
502
|
-
this.biggest = change.value
|
|
503
|
-
this.lastLoadRequestKey = undefined
|
|
504
|
-
} else if (isNewKey) {
|
|
505
|
-
// New key with same orderBy value - allow another load if needed
|
|
506
|
-
this.lastLoadRequestKey = undefined
|
|
507
|
-
}
|
|
441
|
+
const result = trackBiggestSentValue(
|
|
442
|
+
changes,
|
|
443
|
+
this.biggest,
|
|
444
|
+
this.sentToD2Keys,
|
|
445
|
+
comparator,
|
|
446
|
+
)
|
|
447
|
+
this.biggest = result.biggest
|
|
448
|
+
if (result.shouldResetLoadKey) {
|
|
449
|
+
this.lastLoadRequestKey = undefined
|
|
508
450
|
}
|
|
509
451
|
}
|
|
510
452
|
|
|
@@ -525,62 +467,4 @@ export class CollectionSubscriber<
|
|
|
525
467
|
promise,
|
|
526
468
|
)
|
|
527
469
|
}
|
|
528
|
-
|
|
529
|
-
private getLoadRequestKey(options: {
|
|
530
|
-
minValues: Array<unknown> | undefined
|
|
531
|
-
offset: number
|
|
532
|
-
limit: number
|
|
533
|
-
}): string {
|
|
534
|
-
return serializeValue({
|
|
535
|
-
minValues: options.minValues ?? null,
|
|
536
|
-
offset: options.offset,
|
|
537
|
-
limit: options.limit,
|
|
538
|
-
})
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Helper function to send changes to a D2 input stream
|
|
544
|
-
*/
|
|
545
|
-
function sendChangesToInput(
|
|
546
|
-
input: RootStreamBuilder<unknown>,
|
|
547
|
-
changes: Iterable<ChangeMessage>,
|
|
548
|
-
getKey: (item: ChangeMessage[`value`]) => any,
|
|
549
|
-
): number {
|
|
550
|
-
const multiSetArray: MultiSetArray<unknown> = []
|
|
551
|
-
for (const change of changes) {
|
|
552
|
-
const key = getKey(change.value)
|
|
553
|
-
if (change.type === `insert`) {
|
|
554
|
-
multiSetArray.push([[key, change.value], 1])
|
|
555
|
-
} else if (change.type === `update`) {
|
|
556
|
-
multiSetArray.push([[key, change.previousValue], -1])
|
|
557
|
-
multiSetArray.push([[key, change.value], 1])
|
|
558
|
-
} else {
|
|
559
|
-
// change.type === `delete`
|
|
560
|
-
multiSetArray.push([[key, change.value], -1])
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
if (multiSetArray.length !== 0) {
|
|
565
|
-
input.sendData(new MultiSet(multiSetArray))
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return multiSetArray.length
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/** Splits updates into a delete of the old value and an insert of the new value */
|
|
572
|
-
function* splitUpdates<
|
|
573
|
-
T extends object = Record<string, unknown>,
|
|
574
|
-
TKey extends string | number = string | number,
|
|
575
|
-
>(
|
|
576
|
-
changes: Iterable<ChangeMessage<T, TKey>>,
|
|
577
|
-
): Generator<ChangeMessage<T, TKey>> {
|
|
578
|
-
for (const change of changes) {
|
|
579
|
-
if (change.type === `update`) {
|
|
580
|
-
yield { type: `delete`, key: change.key, value: change.previousValue! }
|
|
581
|
-
yield { type: `insert`, key: change.key, value: change.value }
|
|
582
|
-
} else {
|
|
583
|
-
yield change
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
470
|
}
|
package/src/query/live/types.ts
CHANGED
|
@@ -5,7 +5,11 @@ import type {
|
|
|
5
5
|
StringCollationConfig,
|
|
6
6
|
} from '../../types.js'
|
|
7
7
|
import type { InitialQueryBuilder, QueryBuilder } from '../builder/index.js'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
Context,
|
|
10
|
+
RootObjectResultConstraint,
|
|
11
|
+
RootQueryResult,
|
|
12
|
+
} from '../builder/types.js'
|
|
9
13
|
|
|
10
14
|
export type Changes<T> = {
|
|
11
15
|
deletes: number
|
|
@@ -54,7 +58,7 @@ export type FullSyncState = Required<Omit<SyncState, `flushPendingChanges`>> &
|
|
|
54
58
|
*/
|
|
55
59
|
export interface LiveQueryCollectionConfig<
|
|
56
60
|
TContext extends Context,
|
|
57
|
-
TResult extends object =
|
|
61
|
+
TResult extends object = RootQueryResult<TContext>,
|
|
58
62
|
> {
|
|
59
63
|
/**
|
|
60
64
|
* Unique identifier for the collection
|
|
@@ -66,8 +70,10 @@ export interface LiveQueryCollectionConfig<
|
|
|
66
70
|
* Query builder function that defines the live query
|
|
67
71
|
*/
|
|
68
72
|
query:
|
|
69
|
-
| ((
|
|
70
|
-
|
|
73
|
+
| ((
|
|
74
|
+
q: InitialQueryBuilder,
|
|
75
|
+
) => QueryBuilder<TContext> & RootObjectResultConstraint<TContext>)
|
|
76
|
+
| (QueryBuilder<TContext> & RootObjectResultConstraint<TContext>)
|
|
71
77
|
|
|
72
78
|
/**
|
|
73
79
|
* Function to extract the key from result items
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { MultiSet, serializeValue } from '@tanstack/db-ivm'
|
|
2
|
+
import { UnsupportedRootScalarSelectError } from '../../errors.js'
|
|
3
|
+
import { normalizeOrderByPaths } from '../compiler/expressions.js'
|
|
4
|
+
import { buildQuery, getQueryIR } from '../builder/index.js'
|
|
5
|
+
import { IncludesSubquery } from '../ir.js'
|
|
6
|
+
import type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'
|
|
7
|
+
import type { Collection } from '../../collection/index.js'
|
|
8
|
+
import type { ChangeMessage } from '../../types.js'
|
|
9
|
+
import type { InitialQueryBuilder, QueryBuilder } from '../builder/index.js'
|
|
10
|
+
import type { Context } from '../builder/types.js'
|
|
11
|
+
import type { OrderBy, QueryIR } from '../ir.js'
|
|
12
|
+
import type { OrderByOptimizationInfo } from '../compiler/order-by.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper function to extract collections from a compiled query.
|
|
16
|
+
* Traverses the query IR to find all collection references.
|
|
17
|
+
* Maps collections by their ID (not alias) as expected by the compiler.
|
|
18
|
+
*/
|
|
19
|
+
export function extractCollectionsFromQuery(
|
|
20
|
+
query: any,
|
|
21
|
+
): Record<string, Collection<any, any, any>> {
|
|
22
|
+
const collections: Record<string, any> = {}
|
|
23
|
+
|
|
24
|
+
// Helper function to recursively extract collections from a query or source
|
|
25
|
+
function extractFromSource(source: any) {
|
|
26
|
+
if (source.type === `collectionRef`) {
|
|
27
|
+
collections[source.collection.id] = source.collection
|
|
28
|
+
} else if (source.type === `queryRef`) {
|
|
29
|
+
// Recursively extract from subquery
|
|
30
|
+
extractFromQuery(source.query)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Helper function to recursively extract collections from a query
|
|
35
|
+
function extractFromQuery(q: any) {
|
|
36
|
+
// Extract from FROM clause
|
|
37
|
+
if (q.from) {
|
|
38
|
+
extractFromSource(q.from)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract from JOIN clauses
|
|
42
|
+
if (q.join && Array.isArray(q.join)) {
|
|
43
|
+
for (const joinClause of q.join) {
|
|
44
|
+
if (joinClause.from) {
|
|
45
|
+
extractFromSource(joinClause.from)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract from SELECT (for IncludesSubquery)
|
|
51
|
+
if (q.select) {
|
|
52
|
+
extractFromSelect(q.select)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractFromSelect(select: any) {
|
|
57
|
+
for (const [key, value] of Object.entries(select)) {
|
|
58
|
+
if (typeof key === `string` && key.startsWith(`__SPREAD_SENTINEL__`)) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (value instanceof IncludesSubquery) {
|
|
62
|
+
extractFromQuery(value.query)
|
|
63
|
+
} else if (isNestedSelectObject(value)) {
|
|
64
|
+
extractFromSelect(value)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Start extraction from the root query
|
|
70
|
+
extractFromQuery(query)
|
|
71
|
+
|
|
72
|
+
return collections
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Helper function to extract the collection that is referenced in the query's FROM clause.
|
|
77
|
+
* The FROM clause may refer directly to a collection or indirectly to a subquery.
|
|
78
|
+
*/
|
|
79
|
+
export function extractCollectionFromSource(
|
|
80
|
+
query: any,
|
|
81
|
+
): Collection<any, any, any> {
|
|
82
|
+
const from = query.from
|
|
83
|
+
|
|
84
|
+
if (from.type === `collectionRef`) {
|
|
85
|
+
return from.collection
|
|
86
|
+
} else if (from.type === `queryRef`) {
|
|
87
|
+
// Recursively extract from subquery
|
|
88
|
+
return extractCollectionFromSource(from.query)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extracts all aliases used for each collection across the entire query tree.
|
|
98
|
+
*
|
|
99
|
+
* Traverses the QueryIR recursively to build a map from collection ID to all aliases
|
|
100
|
+
* that reference that collection. This is essential for self-join support, where the
|
|
101
|
+
* same collection may be referenced multiple times with different aliases.
|
|
102
|
+
*
|
|
103
|
+
* For example, given a query like:
|
|
104
|
+
* ```ts
|
|
105
|
+
* q.from({ employee: employeesCollection })
|
|
106
|
+
* .join({ manager: employeesCollection }, ({ employee, manager }) =>
|
|
107
|
+
* eq(employee.managerId, manager.id)
|
|
108
|
+
* )
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* This function would return:
|
|
112
|
+
* ```
|
|
113
|
+
* Map { "employees" => Set { "employee", "manager" } }
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @param query - The query IR to extract aliases from
|
|
117
|
+
* @returns A map from collection ID to the set of all aliases referencing that collection
|
|
118
|
+
*/
|
|
119
|
+
export function extractCollectionAliases(
|
|
120
|
+
query: QueryIR,
|
|
121
|
+
): Map<string, Set<string>> {
|
|
122
|
+
const aliasesById = new Map<string, Set<string>>()
|
|
123
|
+
|
|
124
|
+
function recordAlias(source: any) {
|
|
125
|
+
if (!source) return
|
|
126
|
+
|
|
127
|
+
if (source.type === `collectionRef`) {
|
|
128
|
+
const { id } = source.collection
|
|
129
|
+
const existing = aliasesById.get(id)
|
|
130
|
+
if (existing) {
|
|
131
|
+
existing.add(source.alias)
|
|
132
|
+
} else {
|
|
133
|
+
aliasesById.set(id, new Set([source.alias]))
|
|
134
|
+
}
|
|
135
|
+
} else if (source.type === `queryRef`) {
|
|
136
|
+
traverse(source.query)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function traverseSelect(select: any) {
|
|
141
|
+
for (const [key, value] of Object.entries(select)) {
|
|
142
|
+
if (typeof key === `string` && key.startsWith(`__SPREAD_SENTINEL__`)) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
if (value instanceof IncludesSubquery) {
|
|
146
|
+
traverse(value.query)
|
|
147
|
+
} else if (isNestedSelectObject(value)) {
|
|
148
|
+
traverseSelect(value)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function traverse(q?: QueryIR) {
|
|
154
|
+
if (!q) return
|
|
155
|
+
|
|
156
|
+
recordAlias(q.from)
|
|
157
|
+
|
|
158
|
+
if (q.join) {
|
|
159
|
+
for (const joinClause of q.join) {
|
|
160
|
+
recordAlias(joinClause.from)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (q.select) {
|
|
165
|
+
traverseSelect(q.select)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
traverse(query)
|
|
170
|
+
|
|
171
|
+
return aliasesById
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a value is a nested select object (plain object, not an expression)
|
|
176
|
+
*/
|
|
177
|
+
function isNestedSelectObject(obj: any): boolean {
|
|
178
|
+
if (obj === null || typeof obj !== `object`) return false
|
|
179
|
+
if (obj instanceof IncludesSubquery) return false
|
|
180
|
+
// Expression-like objects have a .type property
|
|
181
|
+
if (`type` in obj && typeof obj.type === `string`) return false
|
|
182
|
+
// Ref proxies from spread operations
|
|
183
|
+
if (obj.__refProxy) return false
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Builds a query IR from a config object that contains either a query builder
|
|
189
|
+
* function or a QueryBuilder instance.
|
|
190
|
+
*/
|
|
191
|
+
export function buildQueryFromConfig<TContext extends Context>(config: {
|
|
192
|
+
query:
|
|
193
|
+
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
|
|
194
|
+
| QueryBuilder<TContext>
|
|
195
|
+
requireObjectResult?: boolean
|
|
196
|
+
}): QueryIR {
|
|
197
|
+
// Build the query using the provided query builder function or instance
|
|
198
|
+
const query =
|
|
199
|
+
typeof config.query === `function`
|
|
200
|
+
? buildQuery<TContext>(config.query)
|
|
201
|
+
: getQueryIR(config.query)
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
config.requireObjectResult &&
|
|
205
|
+
query.select &&
|
|
206
|
+
!isNestedSelectObject(query.select)
|
|
207
|
+
) {
|
|
208
|
+
throw new UnsupportedRootScalarSelectError()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return query
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Helper function to send changes to a D2 input stream.
|
|
216
|
+
* Converts ChangeMessages to D2 MultiSet data and sends to the input.
|
|
217
|
+
*
|
|
218
|
+
* @returns The number of multiset entries sent
|
|
219
|
+
*/
|
|
220
|
+
export function sendChangesToInput(
|
|
221
|
+
input: RootStreamBuilder<unknown>,
|
|
222
|
+
changes: Iterable<ChangeMessage>,
|
|
223
|
+
getKey: (item: ChangeMessage[`value`]) => any,
|
|
224
|
+
): number {
|
|
225
|
+
const multiSetArray: MultiSetArray<unknown> = []
|
|
226
|
+
for (const change of changes) {
|
|
227
|
+
const key = getKey(change.value)
|
|
228
|
+
if (change.type === `insert`) {
|
|
229
|
+
multiSetArray.push([[key, change.value], 1])
|
|
230
|
+
} else if (change.type === `update`) {
|
|
231
|
+
multiSetArray.push([[key, change.previousValue], -1])
|
|
232
|
+
multiSetArray.push([[key, change.value], 1])
|
|
233
|
+
} else {
|
|
234
|
+
// change.type === `delete`
|
|
235
|
+
multiSetArray.push([[key, change.value], -1])
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (multiSetArray.length !== 0) {
|
|
240
|
+
input.sendData(new MultiSet(multiSetArray))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return multiSetArray.length
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Splits updates into a delete of the old value and an insert of the new value */
|
|
247
|
+
export function* splitUpdates<
|
|
248
|
+
T extends object = Record<string, unknown>,
|
|
249
|
+
TKey extends string | number = string | number,
|
|
250
|
+
>(
|
|
251
|
+
changes: Iterable<ChangeMessage<T, TKey>>,
|
|
252
|
+
): Generator<ChangeMessage<T, TKey>> {
|
|
253
|
+
for (const change of changes) {
|
|
254
|
+
if (change.type === `update`) {
|
|
255
|
+
yield { type: `delete`, key: change.key, value: change.previousValue! }
|
|
256
|
+
yield { type: `insert`, key: change.key, value: change.value }
|
|
257
|
+
} else {
|
|
258
|
+
yield change
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Filter changes to prevent duplicate inserts to a D2 pipeline.
|
|
265
|
+
* Maintains D2 multiplicity at 1 for visible items so that deletes
|
|
266
|
+
* properly reduce multiplicity to 0.
|
|
267
|
+
*
|
|
268
|
+
* Mutates `sentKeys` in place: adds keys on insert, removes on delete.
|
|
269
|
+
*/
|
|
270
|
+
export function filterDuplicateInserts(
|
|
271
|
+
changes: Array<ChangeMessage<any, string | number>>,
|
|
272
|
+
sentKeys: Set<string | number>,
|
|
273
|
+
): Array<ChangeMessage<any, string | number>> {
|
|
274
|
+
const filtered: Array<ChangeMessage<any, string | number>> = []
|
|
275
|
+
for (const change of changes) {
|
|
276
|
+
if (change.type === `insert`) {
|
|
277
|
+
if (sentKeys.has(change.key)) {
|
|
278
|
+
continue // Skip duplicate
|
|
279
|
+
}
|
|
280
|
+
sentKeys.add(change.key)
|
|
281
|
+
} else if (change.type === `delete`) {
|
|
282
|
+
sentKeys.delete(change.key)
|
|
283
|
+
}
|
|
284
|
+
filtered.push(change)
|
|
285
|
+
}
|
|
286
|
+
return filtered
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Track the biggest value seen in a stream of changes, used for cursor-based
|
|
291
|
+
* pagination in ordered subscriptions. Returns whether the load request key
|
|
292
|
+
* should be reset (allowing another load).
|
|
293
|
+
*
|
|
294
|
+
* @param changes - changes to process (deletes are skipped)
|
|
295
|
+
* @param current - the current biggest value (or undefined if none)
|
|
296
|
+
* @param sentKeys - set of keys already sent to D2 (for new-key detection)
|
|
297
|
+
* @param comparator - orderBy comparator
|
|
298
|
+
* @returns `{ biggest, shouldResetLoadKey }` — the new biggest value and
|
|
299
|
+
* whether the caller should clear its last-load-request-key
|
|
300
|
+
*/
|
|
301
|
+
export function trackBiggestSentValue(
|
|
302
|
+
changes: Array<ChangeMessage<any, string | number>>,
|
|
303
|
+
current: unknown | undefined,
|
|
304
|
+
sentKeys: Set<string | number>,
|
|
305
|
+
comparator: (a: any, b: any) => number,
|
|
306
|
+
): { biggest: unknown; shouldResetLoadKey: boolean } {
|
|
307
|
+
let biggest = current
|
|
308
|
+
let shouldResetLoadKey = false
|
|
309
|
+
|
|
310
|
+
for (const change of changes) {
|
|
311
|
+
if (change.type === `delete`) continue
|
|
312
|
+
|
|
313
|
+
const isNewKey = !sentKeys.has(change.key)
|
|
314
|
+
|
|
315
|
+
if (biggest === undefined) {
|
|
316
|
+
biggest = change.value
|
|
317
|
+
shouldResetLoadKey = true
|
|
318
|
+
} else if (comparator(biggest, change.value) < 0) {
|
|
319
|
+
biggest = change.value
|
|
320
|
+
shouldResetLoadKey = true
|
|
321
|
+
} else if (isNewKey) {
|
|
322
|
+
// New key at same sort position — allow another load if needed
|
|
323
|
+
shouldResetLoadKey = true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { biggest, shouldResetLoadKey }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Compute orderBy/limit subscription hints for an alias.
|
|
332
|
+
* Returns normalised orderBy and effective limit suitable for passing to
|
|
333
|
+
* `subscribeChanges`, or `undefined` values when the query's orderBy cannot
|
|
334
|
+
* be scoped to the given alias (e.g. cross-collection refs or aggregates).
|
|
335
|
+
*/
|
|
336
|
+
export function computeSubscriptionOrderByHints(
|
|
337
|
+
query: { orderBy?: OrderBy; limit?: number; offset?: number },
|
|
338
|
+
alias: string,
|
|
339
|
+
): { orderBy: OrderBy | undefined; limit: number | undefined } {
|
|
340
|
+
const { orderBy, limit, offset } = query
|
|
341
|
+
const effectiveLimit =
|
|
342
|
+
limit !== undefined && offset !== undefined ? limit + offset : limit
|
|
343
|
+
|
|
344
|
+
const normalizedOrderBy = orderBy
|
|
345
|
+
? normalizeOrderByPaths(orderBy, alias)
|
|
346
|
+
: undefined
|
|
347
|
+
|
|
348
|
+
// Only pass orderBy when it is scoped to this alias and uses simple refs,
|
|
349
|
+
// to avoid leaking cross-collection paths into backend-specific compilers.
|
|
350
|
+
const canPassOrderBy =
|
|
351
|
+
normalizedOrderBy?.every((clause) => {
|
|
352
|
+
const exp = clause.expression
|
|
353
|
+
if (exp.type !== `ref`) return false
|
|
354
|
+
const path = exp.path
|
|
355
|
+
return Array.isArray(path) && path.length === 1
|
|
356
|
+
}) ?? false
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
orderBy: canPassOrderBy ? normalizedOrderBy : undefined,
|
|
360
|
+
limit: canPassOrderBy ? effectiveLimit : undefined,
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Compute the cursor for loading the next batch of ordered data.
|
|
366
|
+
* Extracts values from the biggest sent row and builds the `minValues`
|
|
367
|
+
* array and a deduplication key.
|
|
368
|
+
*
|
|
369
|
+
* @returns `undefined` if the load should be skipped (duplicate request),
|
|
370
|
+
* otherwise `{ minValues, normalizedOrderBy, loadRequestKey }`.
|
|
371
|
+
*/
|
|
372
|
+
export function computeOrderedLoadCursor(
|
|
373
|
+
orderByInfo: Pick<
|
|
374
|
+
OrderByOptimizationInfo,
|
|
375
|
+
'orderBy' | 'valueExtractorForRawRow' | 'offset'
|
|
376
|
+
>,
|
|
377
|
+
biggestSentRow: unknown | undefined,
|
|
378
|
+
lastLoadRequestKey: string | undefined,
|
|
379
|
+
alias: string,
|
|
380
|
+
limit: number,
|
|
381
|
+
):
|
|
382
|
+
| {
|
|
383
|
+
minValues: Array<unknown> | undefined
|
|
384
|
+
normalizedOrderBy: OrderBy
|
|
385
|
+
loadRequestKey: string
|
|
386
|
+
}
|
|
387
|
+
| undefined {
|
|
388
|
+
const { orderBy, valueExtractorForRawRow, offset } = orderByInfo
|
|
389
|
+
|
|
390
|
+
// Extract all orderBy column values from the biggest sent row
|
|
391
|
+
// For single-column: returns single value, for multi-column: returns array
|
|
392
|
+
const extractedValues = biggestSentRow
|
|
393
|
+
? valueExtractorForRawRow(biggestSentRow as Record<string, unknown>)
|
|
394
|
+
: undefined
|
|
395
|
+
|
|
396
|
+
// Normalize to array format for minValues
|
|
397
|
+
let minValues: Array<unknown> | undefined
|
|
398
|
+
if (extractedValues !== undefined) {
|
|
399
|
+
minValues = Array.isArray(extractedValues)
|
|
400
|
+
? extractedValues
|
|
401
|
+
: [extractedValues]
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Deduplicate: skip if we already issued an identical load request
|
|
405
|
+
const loadRequestKey = serializeValue({
|
|
406
|
+
minValues: minValues ?? null,
|
|
407
|
+
offset,
|
|
408
|
+
limit,
|
|
409
|
+
})
|
|
410
|
+
if (lastLoadRequestKey === loadRequestKey) {
|
|
411
|
+
return undefined
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const normalizedOrderBy = normalizeOrderByPaths(orderBy, alias)
|
|
415
|
+
|
|
416
|
+
return { minValues, normalizedOrderBy, loadRequestKey }
|
|
417
|
+
}
|