@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.
Files changed (42) hide show
  1. package/dist/cjs/index.cjs +14 -14
  2. package/dist/cjs/query/builder/functions.cjs +2 -0
  3. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  4. package/dist/cjs/query/builder/functions.d.cts +2 -0
  5. package/dist/cjs/query/builder/ref-proxy.cjs +6 -0
  6. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  7. package/dist/cjs/query/builder/ref-proxy.d.cts +4 -2
  8. package/dist/cjs/query/compiler/joins.cjs +13 -1
  9. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  10. package/dist/cjs/query/compiler/order-by.cjs +14 -5
  11. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  12. package/dist/cjs/query/effect.cjs +1 -1
  13. package/dist/cjs/query/effect.cjs.map +1 -1
  14. package/dist/cjs/query/live/collection-config-builder.cjs +25 -3
  15. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  16. package/dist/cjs/query/live/collection-subscriber.cjs +2 -2
  17. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  18. package/dist/esm/index.js +4 -4
  19. package/dist/esm/query/builder/functions.d.ts +2 -0
  20. package/dist/esm/query/builder/functions.js +2 -0
  21. package/dist/esm/query/builder/functions.js.map +1 -1
  22. package/dist/esm/query/builder/ref-proxy.d.ts +4 -2
  23. package/dist/esm/query/builder/ref-proxy.js +6 -0
  24. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  25. package/dist/esm/query/compiler/joins.js +13 -1
  26. package/dist/esm/query/compiler/joins.js.map +1 -1
  27. package/dist/esm/query/compiler/order-by.js +14 -5
  28. package/dist/esm/query/compiler/order-by.js.map +1 -1
  29. package/dist/esm/query/effect.js +1 -1
  30. package/dist/esm/query/effect.js.map +1 -1
  31. package/dist/esm/query/live/collection-config-builder.js +25 -3
  32. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  33. package/dist/esm/query/live/collection-subscriber.js +2 -2
  34. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/query/builder/functions.ts +2 -0
  37. package/src/query/builder/ref-proxy.ts +18 -2
  38. package/src/query/compiler/joins.ts +22 -2
  39. package/src/query/compiler/order-by.ts +21 -6
  40. package/src/query/effect.ts +1 -1
  41. package/src/query/live/collection-config-builder.ts +32 -2
  42. 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 Ref, otherwise creates a Value
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
- // Request filtered snapshot from lazy collection for matching join keys
306
- const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)
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
- setSizeCallback = (getSize: () => number) => {
296
- optimizableOrderByCollections[targetCollectionId]![`dataNeeded`] =
297
- () => {
298
- const size = getSize()
299
- return Math.max(0, orderByOptimizationInfo!.limit - size)
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
  }
@@ -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) => this.resultKeys.get(item) as string | number),
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([...(affectedCorrelationKeys || []), ...dirtyFromBuffers])
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
- // In this case, we've already loaded all data via requestSnapshot
340
- // and don't need to lazily load more.
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