@tanstack/db 0.6.1 → 0.6.3
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/index.cjs +14 -14
- package/dist/cjs/query/builder/functions.cjs +2 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +2 -0
- package/dist/cjs/query/builder/ref-proxy.cjs +6 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +4 -2
- package/dist/cjs/query/compiler/joins.cjs +13 -1
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +14 -5
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/effect.cjs +1 -1
- package/dist/cjs/query/effect.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +25 -3
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +2 -2
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/esm/index.js +4 -4
- package/dist/esm/query/builder/functions.d.ts +2 -0
- package/dist/esm/query/builder/functions.js +2 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +4 -2
- package/dist/esm/query/builder/ref-proxy.js +6 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +13 -1
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js +14 -5
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/effect.js +1 -1
- package/dist/esm/query/effect.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.js +25 -3
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.js +2 -2
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/query/builder/functions.ts +2 -0
- package/src/query/builder/ref-proxy.ts +18 -2
- package/src/query/compiler/joins.ts +22 -2
- package/src/query/compiler/order-by.ts +21 -6
- package/src/query/effect.ts +1 -1
- package/src/query/live/collection-config-builder.ts +32 -2
- package/src/query/live/collection-subscriber.ts +5 -5
|
@@ -284,8 +284,10 @@ export function createRefProxyWithSelected<T extends Record<string, any>>(
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
|
-
* Converts a value to an Expression
|
|
288
|
-
* If it's a RefProxy, creates a
|
|
287
|
+
* Converts a value to an Expression.
|
|
288
|
+
* If it's a RefProxy, creates a PropRef. Throws if the value is a
|
|
289
|
+
* ToArrayWrapper or ConcatToArrayWrapper (these must be used as direct
|
|
290
|
+
* select fields). Otherwise wraps it as a Value.
|
|
289
291
|
*/
|
|
290
292
|
export function toExpression<T = any>(value: T): BasicExpression<T>
|
|
291
293
|
export function toExpression(value: RefProxy<any>): BasicExpression<any>
|
|
@@ -293,6 +295,20 @@ export function toExpression(value: any): BasicExpression<any> {
|
|
|
293
295
|
if (isRefProxy(value)) {
|
|
294
296
|
return new PropRef(value.__path)
|
|
295
297
|
}
|
|
298
|
+
// toArray() and concat(toArray()) must be used as direct select fields, not inside expressions
|
|
299
|
+
if (
|
|
300
|
+
value &&
|
|
301
|
+
typeof value === `object` &&
|
|
302
|
+
(value.__brand === `ToArrayWrapper` ||
|
|
303
|
+
value.__brand === `ConcatToArrayWrapper`)
|
|
304
|
+
) {
|
|
305
|
+
const name =
|
|
306
|
+
value.__brand === `ToArrayWrapper` ? `toArray()` : `concat(toArray())`
|
|
307
|
+
throw new Error(
|
|
308
|
+
`${name} cannot be used inside expressions (e.g., coalesce(), eq(), not()). ` +
|
|
309
|
+
`Use ${name} directly as a select field value instead.`,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
296
312
|
// If it's already an Expression (Func, Ref, Value) or Agg, return it directly
|
|
297
313
|
if (
|
|
298
314
|
value &&
|
|
@@ -302,8 +302,20 @@ function processJoin(
|
|
|
302
302
|
return
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
//
|
|
306
|
-
const joinKeys =
|
|
305
|
+
// Deduplicate and filter null keys before requesting snapshot
|
|
306
|
+
const joinKeys = [
|
|
307
|
+
...new Set(
|
|
308
|
+
data
|
|
309
|
+
.getInner()
|
|
310
|
+
.map(([[joinKey]]) => joinKey)
|
|
311
|
+
.filter((key) => key != null),
|
|
312
|
+
),
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
if (joinKeys.length === 0) {
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
const lazyJoinRef = new PropRef(followRefResult.path)
|
|
308
320
|
const loaded = lazySourceSubscription.requestSnapshot({
|
|
309
321
|
where: inArray(lazyJoinRef, joinKeys),
|
|
@@ -312,6 +324,14 @@ function processJoin(
|
|
|
312
324
|
|
|
313
325
|
if (!loaded) {
|
|
314
326
|
// Snapshot wasn't sent because it could not be loaded from the indexes
|
|
327
|
+
const collectionId = followRefCollection.id
|
|
328
|
+
const fieldPath = followRefResult.path.join(`.`)
|
|
329
|
+
console.warn(
|
|
330
|
+
`[TanStack DB]${collectionId ? ` [${collectionId}]` : ``} Join requires an index on "${fieldPath}" for efficient loading. ` +
|
|
331
|
+
`Falling back to loading all data. ` +
|
|
332
|
+
`Consider creating an index on the collection with collection.createIndex((row) => row.${fieldPath}) ` +
|
|
333
|
+
`or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.`,
|
|
334
|
+
)
|
|
315
335
|
lazySourceSubscription.requestSnapshot()
|
|
316
336
|
}
|
|
317
337
|
}),
|
|
@@ -191,6 +191,17 @@ export function processOrderBy(
|
|
|
191
191
|
index = undefined
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
if (!index) {
|
|
195
|
+
const collectionId = followRefCollection.id
|
|
196
|
+
const fieldPath = followRefResult.path.join(`.`)
|
|
197
|
+
console.warn(
|
|
198
|
+
`[TanStack DB]${collectionId ? ` [${collectionId}]` : ``} orderBy with limit requires an index on "${fieldPath}" for efficient lazy loading. ` +
|
|
199
|
+
`Falling back to loading all data. ` +
|
|
200
|
+
`Consider creating an index on the collection with collection.createIndex((row) => row.${fieldPath}) ` +
|
|
201
|
+
`or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.`,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
orderByAlias =
|
|
195
206
|
firstOrderByExpression.path.length > 1
|
|
196
207
|
? String(firstOrderByExpression.path[0])
|
|
@@ -292,12 +303,16 @@ export function processOrderBy(
|
|
|
292
303
|
|
|
293
304
|
// Set up lazy loading callback to track how much more data is needed
|
|
294
305
|
// This is used by loadMoreIfNeeded to determine if more data should be loaded
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
// Only enable when an index exists — without an index, lazy loading can't work
|
|
307
|
+
// and all data is loaded eagerly via requestSnapshot instead.
|
|
308
|
+
if (index) {
|
|
309
|
+
setSizeCallback = (getSize: () => number) => {
|
|
310
|
+
optimizableOrderByCollections[targetCollectionId]![`dataNeeded`] =
|
|
311
|
+
() => {
|
|
312
|
+
const size = getSize()
|
|
313
|
+
return Math.max(0, orderByOptimizationInfo!.limit - size)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
301
316
|
}
|
|
302
317
|
}
|
|
303
318
|
}
|
package/src/query/effect.ts
CHANGED
|
@@ -883,7 +883,7 @@ class EffectPipelineRunner<TRow extends object, TKey extends string | number> {
|
|
|
883
883
|
for (const [, orderByInfo] of Object.entries(
|
|
884
884
|
this.optimizableOrderByCollections,
|
|
885
885
|
)) {
|
|
886
|
-
if (!orderByInfo.dataNeeded) continue
|
|
886
|
+
if (!orderByInfo.dataNeeded || !orderByInfo.index) continue
|
|
887
887
|
|
|
888
888
|
if (this.pendingOrderedLoadPromise) {
|
|
889
889
|
// Wait for in-flight loads to complete before requesting more
|
|
@@ -227,7 +227,8 @@ export class CollectionConfigBuilder<
|
|
|
227
227
|
id: this.id,
|
|
228
228
|
getKey:
|
|
229
229
|
this.config.getKey ||
|
|
230
|
-
((item) =>
|
|
230
|
+
((item: any) =>
|
|
231
|
+
(this.resultKeys.get(item) ?? item.$key) as string | number),
|
|
231
232
|
sync: this.getSyncConfig(),
|
|
232
233
|
compare: this.compare,
|
|
233
234
|
defaultStringCollation: this.compareOptions,
|
|
@@ -1515,6 +1516,7 @@ function createChildCollectionEntry(
|
|
|
1515
1516
|
},
|
|
1516
1517
|
},
|
|
1517
1518
|
startSync: true,
|
|
1519
|
+
gcTime: 0,
|
|
1518
1520
|
})
|
|
1519
1521
|
|
|
1520
1522
|
const entry: ChildCollectionEntry = {
|
|
@@ -1697,6 +1699,30 @@ function flushIncludesState(
|
|
|
1697
1699
|
)
|
|
1698
1700
|
}
|
|
1699
1701
|
}
|
|
1702
|
+
// Finally: entries with deep nested buffer changes (grandchild-or-deeper buffers
|
|
1703
|
+
// have pending data, but neither this level nor the immediate child level changed).
|
|
1704
|
+
// Without this pass, changes at depth 3+ are stranded because drainNestedBuffers
|
|
1705
|
+
// only drains one level and Phase 4 only flushes entries dirty from Phase 2/3.
|
|
1706
|
+
const deepBufferDirty = new Set<unknown>()
|
|
1707
|
+
if (state.nestedSetups) {
|
|
1708
|
+
for (const [correlationKey, entry] of state.childRegistry) {
|
|
1709
|
+
if (entriesWithChildChanges.has(correlationKey)) continue
|
|
1710
|
+
if (dirtyFromBuffers.has(correlationKey)) continue
|
|
1711
|
+
if (
|
|
1712
|
+
entry.includesStates &&
|
|
1713
|
+
hasPendingIncludesChanges(entry.includesStates)
|
|
1714
|
+
) {
|
|
1715
|
+
flushIncludesState(
|
|
1716
|
+
entry.includesStates,
|
|
1717
|
+
entry.collection,
|
|
1718
|
+
entry.collection.id,
|
|
1719
|
+
null,
|
|
1720
|
+
entry.syncMethods,
|
|
1721
|
+
)
|
|
1722
|
+
deepBufferDirty.add(correlationKey)
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1700
1726
|
|
|
1701
1727
|
// For inline materializations: re-emit affected parents with updated snapshots.
|
|
1702
1728
|
// We mutate items in-place (so collection.get() reflects changes immediately)
|
|
@@ -1705,7 +1731,11 @@ function flushIncludesState(
|
|
|
1705
1731
|
// deepEquals, but in-place mutation means both sides reference the same
|
|
1706
1732
|
// object, so the comparison always returns true and suppresses the event.
|
|
1707
1733
|
const inlineReEmitKeys = materializesInline(state)
|
|
1708
|
-
? new Set([
|
|
1734
|
+
? new Set([
|
|
1735
|
+
...(affectedCorrelationKeys || []),
|
|
1736
|
+
...dirtyFromBuffers,
|
|
1737
|
+
...deepBufferDirty,
|
|
1738
|
+
])
|
|
1709
1739
|
: null
|
|
1710
1740
|
if (parentSyncMethods && inlineReEmitKeys && inlineReEmitKeys.size > 0) {
|
|
1711
1741
|
const events: Array<ChangeMessage<any>> = []
|
|
@@ -332,12 +332,12 @@ export class CollectionSubscriber<
|
|
|
332
332
|
return true
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
const { dataNeeded } = orderByInfo
|
|
335
|
+
const { dataNeeded, index } = orderByInfo
|
|
336
336
|
|
|
337
|
-
if (!dataNeeded) {
|
|
338
|
-
// dataNeeded is not set when there's no index (e.g., non-ref expression
|
|
339
|
-
//
|
|
340
|
-
//
|
|
337
|
+
if (!dataNeeded || !index) {
|
|
338
|
+
// dataNeeded is not set when there's no index (e.g., non-ref expression
|
|
339
|
+
// or auto-indexing is disabled). Without an index, lazy loading can't work —
|
|
340
|
+
// all data was already loaded eagerly via requestSnapshot.
|
|
341
341
|
return true
|
|
342
342
|
}
|
|
343
343
|
|