@tanstack/db 0.5.10 → 0.5.12

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 (224) hide show
  1. package/dist/cjs/SortedMap.cjs +40 -26
  2. package/dist/cjs/SortedMap.cjs.map +1 -1
  3. package/dist/cjs/SortedMap.d.cts +10 -15
  4. package/dist/cjs/collection/change-events.cjs +1 -1
  5. package/dist/cjs/collection/change-events.cjs.map +1 -1
  6. package/dist/cjs/collection/changes.cjs.map +1 -1
  7. package/dist/cjs/collection/events.cjs.map +1 -1
  8. package/dist/cjs/collection/events.d.cts +12 -4
  9. package/dist/cjs/collection/index.cjs +2 -1
  10. package/dist/cjs/collection/index.cjs.map +1 -1
  11. package/dist/cjs/collection/indexes.cjs.map +1 -1
  12. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  13. package/dist/cjs/collection/mutations.cjs +5 -2
  14. package/dist/cjs/collection/mutations.cjs.map +1 -1
  15. package/dist/cjs/collection/state.cjs +6 -5
  16. package/dist/cjs/collection/state.cjs.map +1 -1
  17. package/dist/cjs/collection/state.d.cts +4 -1
  18. package/dist/cjs/collection/subscription.cjs +60 -53
  19. package/dist/cjs/collection/subscription.cjs.map +1 -1
  20. package/dist/cjs/collection/subscription.d.cts +18 -4
  21. package/dist/cjs/collection/sync.cjs.map +1 -1
  22. package/dist/cjs/errors.cjs +9 -0
  23. package/dist/cjs/errors.cjs.map +1 -1
  24. package/dist/cjs/errors.d.cts +3 -0
  25. package/dist/cjs/event-emitter.cjs.map +1 -1
  26. package/dist/cjs/index.cjs +4 -0
  27. package/dist/cjs/index.cjs.map +1 -1
  28. package/dist/cjs/index.d.cts +2 -1
  29. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  30. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  31. package/dist/cjs/indexes/btree-index.cjs +8 -6
  32. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  33. package/dist/cjs/indexes/lazy-index.cjs.map +1 -1
  34. package/dist/cjs/indexes/reverse-index.cjs.map +1 -1
  35. package/dist/cjs/local-only.cjs.map +1 -1
  36. package/dist/cjs/local-storage.cjs.map +1 -1
  37. package/dist/cjs/optimistic-action.cjs.map +1 -1
  38. package/dist/cjs/paced-mutations.cjs.map +1 -1
  39. package/dist/cjs/proxy.cjs.map +1 -1
  40. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  41. package/dist/cjs/query/builder/index.cjs.map +1 -1
  42. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  43. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  44. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  46. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  47. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  48. package/dist/cjs/query/compiler/order-by.cjs +91 -38
  49. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  50. package/dist/cjs/query/compiler/order-by.d.cts +6 -2
  51. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  52. package/dist/cjs/query/expression-helpers.cjs.map +1 -1
  53. package/dist/cjs/query/index.d.cts +1 -1
  54. package/dist/cjs/query/ir.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  56. package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
  57. package/dist/cjs/query/live/collection-subscriber.cjs +30 -15
  58. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  59. package/dist/cjs/query/live/internal.cjs.map +1 -1
  60. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  61. package/dist/cjs/query/optimizer.cjs.map +1 -1
  62. package/dist/cjs/query/predicate-utils.cjs +19 -2
  63. package/dist/cjs/query/predicate-utils.cjs.map +1 -1
  64. package/dist/cjs/query/predicate-utils.d.cts +32 -1
  65. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  66. package/dist/cjs/scheduler.cjs.map +1 -1
  67. package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -1
  68. package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
  69. package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -1
  70. package/dist/cjs/transactions.cjs.map +1 -1
  71. package/dist/cjs/types.d.cts +43 -5
  72. package/dist/cjs/utils/browser-polyfills.cjs.map +1 -1
  73. package/dist/cjs/utils/btree.cjs.map +1 -1
  74. package/dist/cjs/utils/comparison.cjs.map +1 -1
  75. package/dist/cjs/utils/cursor.cjs +39 -0
  76. package/dist/cjs/utils/cursor.cjs.map +1 -0
  77. package/dist/cjs/utils/cursor.d.cts +18 -0
  78. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  79. package/dist/cjs/utils.cjs.map +1 -1
  80. package/dist/esm/SortedMap.d.ts +10 -15
  81. package/dist/esm/SortedMap.js +40 -26
  82. package/dist/esm/SortedMap.js.map +1 -1
  83. package/dist/esm/collection/change-events.js +1 -1
  84. package/dist/esm/collection/change-events.js.map +1 -1
  85. package/dist/esm/collection/changes.js.map +1 -1
  86. package/dist/esm/collection/events.d.ts +12 -4
  87. package/dist/esm/collection/events.js.map +1 -1
  88. package/dist/esm/collection/index.js +2 -1
  89. package/dist/esm/collection/index.js.map +1 -1
  90. package/dist/esm/collection/indexes.js.map +1 -1
  91. package/dist/esm/collection/lifecycle.js.map +1 -1
  92. package/dist/esm/collection/mutations.js +6 -3
  93. package/dist/esm/collection/mutations.js.map +1 -1
  94. package/dist/esm/collection/state.d.ts +4 -1
  95. package/dist/esm/collection/state.js +6 -5
  96. package/dist/esm/collection/state.js.map +1 -1
  97. package/dist/esm/collection/subscription.d.ts +18 -4
  98. package/dist/esm/collection/subscription.js +61 -54
  99. package/dist/esm/collection/subscription.js.map +1 -1
  100. package/dist/esm/collection/sync.js.map +1 -1
  101. package/dist/esm/errors.d.ts +3 -0
  102. package/dist/esm/errors.js +9 -0
  103. package/dist/esm/errors.js.map +1 -1
  104. package/dist/esm/event-emitter.js.map +1 -1
  105. package/dist/esm/index.d.ts +2 -1
  106. package/dist/esm/index.js +6 -2
  107. package/dist/esm/index.js.map +1 -1
  108. package/dist/esm/indexes/auto-index.js.map +1 -1
  109. package/dist/esm/indexes/base-index.js.map +1 -1
  110. package/dist/esm/indexes/btree-index.js +8 -6
  111. package/dist/esm/indexes/btree-index.js.map +1 -1
  112. package/dist/esm/indexes/lazy-index.js.map +1 -1
  113. package/dist/esm/indexes/reverse-index.js.map +1 -1
  114. package/dist/esm/local-only.js.map +1 -1
  115. package/dist/esm/local-storage.js.map +1 -1
  116. package/dist/esm/optimistic-action.js.map +1 -1
  117. package/dist/esm/paced-mutations.js.map +1 -1
  118. package/dist/esm/proxy.js.map +1 -1
  119. package/dist/esm/query/builder/functions.js.map +1 -1
  120. package/dist/esm/query/builder/index.js.map +1 -1
  121. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  122. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  123. package/dist/esm/query/compiler/expressions.js.map +1 -1
  124. package/dist/esm/query/compiler/group-by.js.map +1 -1
  125. package/dist/esm/query/compiler/index.js.map +1 -1
  126. package/dist/esm/query/compiler/joins.js.map +1 -1
  127. package/dist/esm/query/compiler/order-by.d.ts +6 -2
  128. package/dist/esm/query/compiler/order-by.js +91 -38
  129. package/dist/esm/query/compiler/order-by.js.map +1 -1
  130. package/dist/esm/query/compiler/select.js.map +1 -1
  131. package/dist/esm/query/expression-helpers.js.map +1 -1
  132. package/dist/esm/query/index.d.ts +1 -1
  133. package/dist/esm/query/ir.js.map +1 -1
  134. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  135. package/dist/esm/query/live/collection-registry.js.map +1 -1
  136. package/dist/esm/query/live/collection-subscriber.js +30 -15
  137. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  138. package/dist/esm/query/live/internal.js.map +1 -1
  139. package/dist/esm/query/live-query-collection.js.map +1 -1
  140. package/dist/esm/query/optimizer.js.map +1 -1
  141. package/dist/esm/query/predicate-utils.d.ts +32 -1
  142. package/dist/esm/query/predicate-utils.js +19 -2
  143. package/dist/esm/query/predicate-utils.js.map +1 -1
  144. package/dist/esm/query/subset-dedupe.js.map +1 -1
  145. package/dist/esm/scheduler.js.map +1 -1
  146. package/dist/esm/strategies/debounceStrategy.js.map +1 -1
  147. package/dist/esm/strategies/queueStrategy.js.map +1 -1
  148. package/dist/esm/strategies/throttleStrategy.js.map +1 -1
  149. package/dist/esm/transactions.js.map +1 -1
  150. package/dist/esm/types.d.ts +43 -5
  151. package/dist/esm/utils/browser-polyfills.js.map +1 -1
  152. package/dist/esm/utils/btree.js.map +1 -1
  153. package/dist/esm/utils/comparison.js.map +1 -1
  154. package/dist/esm/utils/cursor.d.ts +18 -0
  155. package/dist/esm/utils/cursor.js +39 -0
  156. package/dist/esm/utils/cursor.js.map +1 -0
  157. package/dist/esm/utils/index-optimization.js.map +1 -1
  158. package/dist/esm/utils.js.map +1 -1
  159. package/package.json +30 -28
  160. package/src/SortedMap.ts +50 -31
  161. package/src/collection/change-events.ts +23 -21
  162. package/src/collection/changes.ts +12 -12
  163. package/src/collection/events.ts +20 -10
  164. package/src/collection/index.ts +47 -46
  165. package/src/collection/indexes.ts +14 -14
  166. package/src/collection/lifecycle.ts +16 -16
  167. package/src/collection/mutations.ts +25 -20
  168. package/src/collection/state.ts +43 -36
  169. package/src/collection/subscription.ts +114 -83
  170. package/src/collection/sync.ts +13 -13
  171. package/src/duplicate-instance-check.ts +1 -1
  172. package/src/errors.ts +49 -40
  173. package/src/event-emitter.ts +5 -5
  174. package/src/index.ts +21 -20
  175. package/src/indexes/auto-index.ts +11 -11
  176. package/src/indexes/base-index.ts +13 -13
  177. package/src/indexes/btree-index.ts +21 -17
  178. package/src/indexes/index-options.ts +3 -3
  179. package/src/indexes/lazy-index.ts +8 -8
  180. package/src/indexes/reverse-index.ts +5 -5
  181. package/src/local-only.ts +12 -12
  182. package/src/local-storage.ts +17 -17
  183. package/src/optimistic-action.ts +5 -5
  184. package/src/paced-mutations.ts +6 -6
  185. package/src/proxy.ts +43 -43
  186. package/src/query/builder/functions.ts +28 -28
  187. package/src/query/builder/index.ts +22 -22
  188. package/src/query/builder/ref-proxy.ts +4 -4
  189. package/src/query/builder/types.ts +8 -8
  190. package/src/query/compiler/evaluators.ts +9 -9
  191. package/src/query/compiler/expressions.ts +6 -6
  192. package/src/query/compiler/group-by.ts +24 -24
  193. package/src/query/compiler/index.ts +44 -44
  194. package/src/query/compiler/joins.ts +37 -37
  195. package/src/query/compiler/order-by.ts +170 -77
  196. package/src/query/compiler/select.ts +13 -13
  197. package/src/query/compiler/types.ts +2 -2
  198. package/src/query/expression-helpers.ts +16 -16
  199. package/src/query/index.ts +10 -9
  200. package/src/query/ir.ts +13 -13
  201. package/src/query/live/collection-config-builder.ts +53 -53
  202. package/src/query/live/collection-registry.ts +6 -6
  203. package/src/query/live/collection-subscriber.ts +87 -48
  204. package/src/query/live/internal.ts +1 -1
  205. package/src/query/live/types.ts +4 -4
  206. package/src/query/live-query-collection.ts +15 -15
  207. package/src/query/optimizer.ts +29 -29
  208. package/src/query/predicate-utils.ts +105 -50
  209. package/src/query/subset-dedupe.ts +6 -6
  210. package/src/scheduler.ts +3 -3
  211. package/src/strategies/debounceStrategy.ts +6 -6
  212. package/src/strategies/index.ts +4 -4
  213. package/src/strategies/queueStrategy.ts +5 -5
  214. package/src/strategies/throttleStrategy.ts +6 -6
  215. package/src/strategies/types.ts +2 -2
  216. package/src/transactions.ts +9 -9
  217. package/src/types.ts +51 -12
  218. package/src/utils/array-utils.ts +1 -1
  219. package/src/utils/browser-polyfills.ts +2 -2
  220. package/src/utils/btree.ts +22 -22
  221. package/src/utils/comparison.ts +3 -3
  222. package/src/utils/cursor.ts +78 -0
  223. package/src/utils/index-optimization.ts +14 -14
  224. package/src/utils.ts +4 -4
@@ -1,19 +1,19 @@
1
- import { MultiSet } from "@tanstack/db-ivm"
1
+ import { MultiSet } from '@tanstack/db-ivm'
2
2
  import {
3
3
  normalizeExpressionPaths,
4
4
  normalizeOrderByPaths,
5
- } from "../compiler/expressions.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 { Context, GetResult } from "../builder/types.js"
10
- import type { BasicExpression } from "../ir.js"
11
- import type { OrderByOptimizationInfo } from "../compiler/order-by.js"
12
- import type { CollectionConfigBuilder } from "./collection-config-builder.js"
13
- import type { CollectionSubscription } from "../../collection/subscription.js"
5
+ } from '../compiler/expressions.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 { Context, GetResult } from '../builder/types.js'
10
+ import type { BasicExpression } from '../ir.js'
11
+ import type { OrderByOptimizationInfo } from '../compiler/order-by.js'
12
+ import type { CollectionConfigBuilder } from './collection-config-builder.js'
13
+ import type { CollectionSubscription } from '../../collection/subscription.js'
14
14
 
15
15
  const loadMoreCallbackSymbol = Symbol.for(
16
- `@tanstack/db.collection-config-builder`
16
+ `@tanstack/db.collection-config-builder`,
17
17
  )
18
18
 
19
19
  export class CollectionSubscriber<
@@ -33,7 +33,7 @@ export class CollectionSubscriber<
33
33
  private alias: string,
34
34
  private collectionId: string,
35
35
  private collection: Collection,
36
- private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>
36
+ private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,
37
37
  ) {}
38
38
 
39
39
  subscribe(): CollectionSubscription {
@@ -53,17 +53,17 @@ export class CollectionSubscriber<
53
53
  if (orderByInfo) {
54
54
  subscription = this.subscribeToOrderedChanges(
55
55
  whereExpression,
56
- orderByInfo
56
+ orderByInfo,
57
57
  )
58
58
  } else {
59
59
  // If the source alias is lazy then we should not include the initial state
60
60
  const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
61
- this.alias
61
+ this.alias,
62
62
  )
63
63
 
64
64
  subscription = this.subscribeToMatchingChanges(
65
65
  whereExpression,
66
- includeInitialState
66
+ includeInitialState,
67
67
  )
68
68
  }
69
69
 
@@ -79,7 +79,7 @@ export class CollectionSubscriber<
79
79
  resolve: resolve!,
80
80
  })
81
81
  this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(
82
- promise
82
+ promise,
83
83
  )
84
84
  }
85
85
  }
@@ -120,14 +120,14 @@ export class CollectionSubscriber<
120
120
  // currentSyncState is always defined when subscribe() is called
121
121
  // (called during sync session setup)
122
122
  this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(
123
- unsubscribe
123
+ unsubscribe,
124
124
  )
125
125
  return subscription
126
126
  }
127
127
 
128
128
  private sendChangesToPipeline(
129
129
  changes: Iterable<ChangeMessage<any, string | number>>,
130
- callback?: () => boolean
130
+ callback?: () => boolean,
131
131
  ) {
132
132
  // currentSyncState and input are always defined when this method is called
133
133
  // (only called from active subscriptions during a sync session)
@@ -136,7 +136,7 @@ export class CollectionSubscriber<
136
136
  const sentChanges = sendChangesToInput(
137
137
  input,
138
138
  changes,
139
- this.collection.config.getKey
139
+ this.collection.config.getKey,
140
140
  )
141
141
 
142
142
  // Do not provide the callback that loads more data
@@ -154,10 +154,10 @@ export class CollectionSubscriber<
154
154
 
155
155
  private subscribeToMatchingChanges(
156
156
  whereExpression: BasicExpression<boolean> | undefined,
157
- includeInitialState: boolean = false
157
+ includeInitialState: boolean = false,
158
158
  ) {
159
159
  const sendChanges = (
160
- changes: Array<ChangeMessage<any, string | number>>
160
+ changes: Array<ChangeMessage<any, string | number>>,
161
161
  ) => {
162
162
  this.sendChangesToPipeline(changes)
163
163
  }
@@ -172,12 +172,12 @@ export class CollectionSubscriber<
172
172
 
173
173
  private subscribeToOrderedChanges(
174
174
  whereExpression: BasicExpression<boolean> | undefined,
175
- orderByInfo: OrderByOptimizationInfo
175
+ orderByInfo: OrderByOptimizationInfo,
176
176
  ) {
177
177
  const { orderBy, offset, limit, index } = orderByInfo
178
178
 
179
179
  const sendChangesInRange = (
180
- changes: Iterable<ChangeMessage<any, string | number>>
180
+ changes: Iterable<ChangeMessage<any, string | number>>,
181
181
  ) => {
182
182
  // Split live updates into a delete of the old value and an insert of the new value
183
183
  const splittedChanges = splitUpdates(changes)
@@ -190,17 +190,41 @@ export class CollectionSubscriber<
190
190
  whereExpression,
191
191
  })
192
192
 
193
- subscription.setOrderByIndex(index)
193
+ // Listen for truncate events to reset cursor tracking state
194
+ // This ensures that after a must-refetch/truncate, we don't use stale cursor data
195
+ const truncateUnsubscribe = this.collection.on(`truncate`, () => {
196
+ this.biggest = undefined
197
+ })
198
+
199
+ // Clean up truncate listener when subscription is unsubscribed
200
+ subscription.on(`unsubscribed`, () => {
201
+ truncateUnsubscribe()
202
+ })
194
203
 
195
204
  // Normalize the orderBy clauses such that the references are relative to the collection
196
205
  const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
197
206
 
198
- // Load the first `offset + limit` values from the index
199
- // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[
200
- subscription.requestLimitedSnapshot({
201
- limit: offset + limit,
202
- orderBy: normalizedOrderBy,
203
- })
207
+ if (index) {
208
+ // We have an index on the first orderBy column - use lazy loading optimization
209
+ // This works for both single-column and multi-column orderBy:
210
+ // - Single-column: index provides exact ordering
211
+ // - Multi-column: index provides ordering on first column, secondary sort in memory
212
+ subscription.setOrderByIndex(index)
213
+
214
+ // Load the first `offset + limit` values from the index
215
+ // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[
216
+ subscription.requestLimitedSnapshot({
217
+ limit: offset + limit,
218
+ orderBy: normalizedOrderBy,
219
+ })
220
+ } else {
221
+ // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset
222
+ // so the sync layer can optimize if the backend supports it
223
+ subscription.requestSnapshot({
224
+ orderBy: normalizedOrderBy,
225
+ limit: offset + limit,
226
+ })
227
+ }
204
228
 
205
229
  return subscription
206
230
  }
@@ -220,11 +244,10 @@ export class CollectionSubscriber<
220
244
  const { dataNeeded } = orderByInfo
221
245
 
222
246
  if (!dataNeeded) {
223
- // This should never happen because the topK operator should always set the size callback
224
- // which in turn should lead to the orderBy operator setting the dataNeeded callback
225
- throw new Error(
226
- `Missing dataNeeded callback for collection ${this.collectionId}`
227
- )
247
+ // dataNeeded is not set when there's no index (e.g., non-ref expression).
248
+ // In this case, we've already loaded all data via requestSnapshot
249
+ // and don't need to lazily load more.
250
+ return true
228
251
  }
229
252
 
230
253
  // `dataNeeded` probes the orderBy operator to see if it needs more data
@@ -238,7 +261,7 @@ export class CollectionSubscriber<
238
261
 
239
262
  private sendChangesToPipelineWithTracking(
240
263
  changes: Iterable<ChangeMessage<any, string | number>>,
241
- subscription: CollectionSubscription
264
+ subscription: CollectionSubscription,
242
265
  ) {
243
266
  const orderByInfo = this.getOrderByInfo()
244
267
  if (!orderByInfo) {
@@ -262,7 +285,7 @@ export class CollectionSubscriber<
262
285
 
263
286
  this.sendChangesToPipeline(
264
287
  trackedChanges,
265
- subscriptionWithLoader[loadMoreCallbackSymbol]
288
+ subscriptionWithLoader[loadMoreCallbackSymbol],
266
289
  )
267
290
  }
268
291
 
@@ -273,20 +296,33 @@ export class CollectionSubscriber<
273
296
  if (!orderByInfo) {
274
297
  return
275
298
  }
276
- const { orderBy, valueExtractorForRawRow } = orderByInfo
299
+ const { orderBy, valueExtractorForRawRow, offset } = orderByInfo
277
300
  const biggestSentRow = this.biggest
278
- const biggestSentValue = biggestSentRow
301
+
302
+ // Extract all orderBy column values from the biggest sent row
303
+ // For single-column: returns single value, for multi-column: returns array
304
+ const extractedValues = biggestSentRow
279
305
  ? valueExtractorForRawRow(biggestSentRow)
280
- : biggestSentRow
306
+ : undefined
307
+
308
+ // Normalize to array format for minValues
309
+ const minValues =
310
+ extractedValues !== undefined
311
+ ? Array.isArray(extractedValues)
312
+ ? extractedValues
313
+ : [extractedValues]
314
+ : undefined
281
315
 
282
316
  // Normalize the orderBy clauses such that the references are relative to the collection
283
317
  const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
284
318
 
285
319
  // Take the `n` items after the biggest sent value
320
+ // Pass the current window offset to ensure proper deduplication
286
321
  subscription.requestLimitedSnapshot({
287
322
  orderBy: normalizedOrderBy,
288
323
  limit: n,
289
- minValue: biggestSentValue,
324
+ minValues,
325
+ offset,
290
326
  })
291
327
  }
292
328
 
@@ -312,13 +348,16 @@ export class CollectionSubscriber<
312
348
 
313
349
  private *trackSentValues(
314
350
  changes: Iterable<ChangeMessage<any, string | number>>,
315
- comparator: (a: any, b: any) => number
351
+ comparator: (a: any, b: any) => number,
316
352
  ) {
317
353
  for (const change of changes) {
318
- if (!this.biggest) {
319
- this.biggest = change.value
320
- } else if (comparator(this.biggest, change.value) < 0) {
321
- this.biggest = change.value
354
+ // Only track inserts/updates for cursor positioning, not deletes
355
+ if (change.type !== `delete`) {
356
+ if (!this.biggest) {
357
+ this.biggest = change.value
358
+ } else if (comparator(this.biggest, change.value) < 0) {
359
+ this.biggest = change.value
360
+ }
322
361
  }
323
362
 
324
363
  yield change
@@ -332,7 +371,7 @@ export class CollectionSubscriber<
332
371
  function sendChangesToInput(
333
372
  input: RootStreamBuilder<unknown>,
334
373
  changes: Iterable<ChangeMessage>,
335
- getKey: (item: ChangeMessage[`value`]) => any
374
+ getKey: (item: ChangeMessage[`value`]) => any,
336
375
  ): number {
337
376
  const multiSetArray: MultiSetArray<unknown> = []
338
377
  for (const change of changes) {
@@ -360,7 +399,7 @@ function* splitUpdates<
360
399
  T extends object = Record<string, unknown>,
361
400
  TKey extends string | number = string | number,
362
401
  >(
363
- changes: Iterable<ChangeMessage<T, TKey>>
402
+ changes: Iterable<ChangeMessage<T, TKey>>,
364
403
  ): Generator<ChangeMessage<T, TKey>> {
365
404
  for (const change of changes) {
366
405
  if (change.type === `update`) {
@@ -1,4 +1,4 @@
1
- import type { CollectionConfigBuilder } from "./collection-config-builder.js"
1
+ import type { CollectionConfigBuilder } from './collection-config-builder.js'
2
2
 
3
3
  /**
4
4
  * Symbol for accessing internal utilities that should not be part of the public API
@@ -1,11 +1,11 @@
1
- import type { D2, RootStreamBuilder } from "@tanstack/db-ivm"
1
+ import type { D2, RootStreamBuilder } from '@tanstack/db-ivm'
2
2
  import type {
3
3
  CollectionConfig,
4
4
  ResultStream,
5
5
  StringCollationConfig,
6
- } from "../../types.js"
7
- import type { InitialQueryBuilder, QueryBuilder } from "../builder/index.js"
8
- import type { Context, GetResult } from "../builder/types.js"
6
+ } from '../../types.js'
7
+ import type { InitialQueryBuilder, QueryBuilder } from '../builder/index.js'
8
+ import type { Context, GetResult } from '../builder/types.js'
9
9
 
10
10
  export type Changes<T> = {
11
11
  deletes: number
@@ -1,21 +1,21 @@
1
- import { createCollection } from "../collection/index.js"
2
- import { CollectionConfigBuilder } from "./live/collection-config-builder.js"
1
+ import { createCollection } from '../collection/index.js'
2
+ import { CollectionConfigBuilder } from './live/collection-config-builder.js'
3
3
  import {
4
4
  getBuilderFromConfig,
5
5
  registerCollectionBuilder,
6
- } from "./live/collection-registry.js"
7
- import type { LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
8
- import type { LiveQueryCollectionConfig } from "./live/types.js"
9
- import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
10
- import type { Collection } from "../collection/index.js"
6
+ } from './live/collection-registry.js'
7
+ import type { LiveQueryCollectionUtils } from './live/collection-config-builder.js'
8
+ import type { LiveQueryCollectionConfig } from './live/types.js'
9
+ import type { InitialQueryBuilder, QueryBuilder } from './builder/index.js'
10
+ import type { Collection } from '../collection/index.js'
11
11
  import type {
12
12
  CollectionConfig,
13
13
  CollectionConfigSingleRowOption,
14
14
  NonSingleResult,
15
15
  SingleResult,
16
16
  UtilsRecord,
17
- } from "../types.js"
18
- import type { Context, GetResult } from "./builder/types.js"
17
+ } from '../types.js'
18
+ import type { Context, GetResult } from './builder/types.js'
19
19
 
20
20
  type CollectionConfigForContext<
21
21
  TContext extends Context,
@@ -63,7 +63,7 @@ export function liveQueryCollectionOptions<
63
63
  TContext extends Context,
64
64
  TResult extends object = GetResult<TContext>,
65
65
  >(
66
- config: LiveQueryCollectionConfig<TContext, TResult>
66
+ config: LiveQueryCollectionConfig<TContext, TResult>,
67
67
  ): CollectionConfigForContext<TContext, TResult> & {
68
68
  utils: LiveQueryCollectionUtils
69
69
  } {
@@ -116,7 +116,7 @@ export function createLiveQueryCollection<
116
116
  TContext extends Context,
117
117
  TResult extends object = GetResult<TContext>,
118
118
  >(
119
- query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
119
+ query: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
120
120
  ): CollectionForContext<TContext, TResult> & {
121
121
  utils: LiveQueryCollectionUtils
122
122
  }
@@ -127,7 +127,7 @@ export function createLiveQueryCollection<
127
127
  TResult extends object = GetResult<TContext>,
128
128
  TUtils extends UtilsRecord = {},
129
129
  >(
130
- config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
130
+ config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils },
131
131
  ): CollectionForContext<TContext, TResult> & {
132
132
  utils: LiveQueryCollectionUtils & TUtils
133
133
  }
@@ -140,7 +140,7 @@ export function createLiveQueryCollection<
140
140
  >(
141
141
  configOrQuery:
142
142
  | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
143
- | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
143
+ | ((q: InitialQueryBuilder) => QueryBuilder<TContext>),
144
144
  ): CollectionForContext<TContext, TResult> & {
145
145
  utils: LiveQueryCollectionUtils & TUtils
146
146
  } {
@@ -149,7 +149,7 @@ export function createLiveQueryCollection<
149
149
  // Simple query function case
150
150
  const config: LiveQueryCollectionConfig<TContext, TResult> = {
151
151
  query: configOrQuery as (
152
- q: InitialQueryBuilder
152
+ q: InitialQueryBuilder,
153
153
  ) => QueryBuilder<TContext>,
154
154
  }
155
155
  const options = liveQueryCollectionOptions<TContext, TResult>(config)
@@ -185,7 +185,7 @@ function bridgeToCreateCollection<
185
185
  TResult extends object,
186
186
  TUtils extends UtilsRecord = {},
187
187
  >(
188
- options: CollectionConfig<TResult> & { utils: TUtils }
188
+ options: CollectionConfig<TResult> & { utils: TUtils },
189
189
  ): Collection<TResult, string | number, TUtils> {
190
190
  const collection = createCollection(options as any) as unknown as Collection<
191
191
  TResult,
@@ -120,8 +120,8 @@
120
120
  * transformed into a D2Mini pipeline.
121
121
  */
122
122
 
123
- import { deepEquals } from "../utils.js"
124
- import { CannotCombineEmptyExpressionListError } from "../errors.js"
123
+ import { deepEquals } from '../utils.js'
124
+ import { CannotCombineEmptyExpressionListError } from '../errors.js'
125
125
  import {
126
126
  CollectionRef as CollectionRefClass,
127
127
  Func,
@@ -130,8 +130,8 @@ import {
130
130
  createResidualWhere,
131
131
  getWhereExpression,
132
132
  isResidualWhere,
133
- } from "./ir.js"
134
- import type { BasicExpression, From, QueryIR, Select, Where } from "./ir.js"
133
+ } from './ir.js'
134
+ import type { BasicExpression, From, QueryIR, Select, Where } from './ir.js'
135
135
 
136
136
  /**
137
137
  * Represents a WHERE clause after source analysis
@@ -226,7 +226,7 @@ export function optimizeQuery(query: QueryIR): OptimizationResult {
226
226
  * @returns Map of source aliases to their WHERE clauses
227
227
  */
228
228
  function extractSourceWhereClauses(
229
- query: QueryIR
229
+ query: QueryIR,
230
230
  ): Map<string, BasicExpression<boolean>> {
231
231
  const sourceWhereClauses = new Map<string, BasicExpression<boolean>>()
232
232
 
@@ -240,7 +240,7 @@ function extractSourceWhereClauses(
240
240
 
241
241
  // Analyze each WHERE clause to determine which sources it touches
242
242
  const analyzedClauses = splitWhereClauses.map((clause) =>
243
- analyzeWhereClause(clause)
243
+ analyzeWhereClause(clause),
244
244
  )
245
245
 
246
246
  // Group clauses by single-source vs multi-source
@@ -297,7 +297,7 @@ function applyRecursiveOptimization(query: QueryIR): QueryIR {
297
297
  query.from.type === `queryRef`
298
298
  ? new QueryRefClass(
299
299
  applyRecursiveOptimization(query.from.query),
300
- query.from.alias
300
+ query.from.alias,
301
301
  )
302
302
  : query.from,
303
303
  join: query.join?.map((joinClause) => ({
@@ -306,7 +306,7 @@ function applyRecursiveOptimization(query: QueryIR): QueryIR {
306
306
  joinClause.from.type === `queryRef`
307
307
  ? new QueryRefClass(
308
308
  applyRecursiveOptimization(joinClause.from.query),
309
- joinClause.from.alias
309
+ joinClause.from.alias,
310
310
  )
311
311
  : joinClause.from,
312
312
  })),
@@ -346,7 +346,7 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
346
346
 
347
347
  // Filter out residual WHERE clauses to prevent them from being optimized again
348
348
  const nonResidualWhereClauses = query.where.filter(
349
- (where) => !isResidualWhere(where)
349
+ (where) => !isResidualWhere(where),
350
350
  )
351
351
 
352
352
  // Step 1: Split all AND clauses at the root level for granular optimization
@@ -354,7 +354,7 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
354
354
 
355
355
  // Step 2: Analyze each WHERE clause to determine which sources it touches
356
356
  const analyzedClauses = splitWhereClauses.map((clause) =>
357
- analyzeWhereClause(clause)
357
+ analyzeWhereClause(clause),
358
358
  )
359
359
 
360
360
  // Step 3: Group clauses by single-source vs multi-source
@@ -365,7 +365,7 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
365
365
 
366
366
  // Add back any residual WHERE clauses that were filtered out
367
367
  const residualWhereClauses = query.where.filter((where) =>
368
- isResidualWhere(where)
368
+ isResidualWhere(where),
369
369
  )
370
370
  if (residualWhereClauses.length > 0) {
371
371
  optimizedQuery.where = [
@@ -461,7 +461,7 @@ function isRedundantSubquery(query: QueryIR): boolean {
461
461
  * ```
462
462
  */
463
463
  function splitAndClauses(
464
- whereClauses: Array<Where>
464
+ whereClauses: Array<Where>,
465
465
  ): Array<BasicExpression<boolean>> {
466
466
  const result: Array<BasicExpression<boolean>> = []
467
467
 
@@ -475,7 +475,7 @@ function splitAndClauses(
475
475
 
476
476
  // Helper function for recursive splitting of BasicExpression arrays
477
477
  function splitAndClausesRecursive(
478
- clause: BasicExpression<boolean>
478
+ clause: BasicExpression<boolean>,
479
479
  ): Array<BasicExpression<boolean>> {
480
480
  if (clause.type === `func` && clause.name === `and`) {
481
481
  // Recursively split nested AND clauses to handle complex expressions
@@ -515,7 +515,7 @@ function splitAndClausesRecursive(
515
515
  * ```
516
516
  */
517
517
  function analyzeWhereClause(
518
- clause: BasicExpression<boolean>
518
+ clause: BasicExpression<boolean>,
519
519
  ): AnalyzedWhereClause {
520
520
  // Track which table aliases this WHERE clause touches
521
521
  const touchedSources = new Set<string>()
@@ -580,7 +580,7 @@ function analyzeWhereClause(
580
580
  * @returns Grouped clauses ready for optimization
581
581
  */
582
582
  function groupWhereClauses(
583
- analyzedClauses: Array<AnalyzedWhereClause>
583
+ analyzedClauses: Array<AnalyzedWhereClause>,
584
584
  ): GroupedWhereClauses {
585
585
  const singleSource = new Map<string, Array<BasicExpression<boolean>>>()
586
586
  const multiSource: Array<BasicExpression<boolean>> = []
@@ -630,7 +630,7 @@ function groupWhereClauses(
630
630
  */
631
631
  function applyOptimizations(
632
632
  query: QueryIR,
633
- groupedClauses: GroupedWhereClauses
633
+ groupedClauses: GroupedWhereClauses,
634
634
  ): QueryIR {
635
635
  // Track which single-source clauses were actually optimized
636
636
  const actuallyOptimized = new Set<string>()
@@ -639,7 +639,7 @@ function applyOptimizations(
639
639
  const optimizedFrom = optimizeFromWithTracking(
640
640
  query.from,
641
641
  groupedClauses.singleSource,
642
- actuallyOptimized
642
+ actuallyOptimized,
643
643
  )
644
644
 
645
645
  // Optimize JOIN clauses and track what was optimized
@@ -649,7 +649,7 @@ function applyOptimizations(
649
649
  from: optimizeFromWithTracking(
650
650
  joinClause.from,
651
651
  groupedClauses.singleSource,
652
- actuallyOptimized
652
+ actuallyOptimized,
653
653
  ),
654
654
  }))
655
655
  : undefined
@@ -667,7 +667,7 @@ function applyOptimizations(
667
667
  query.join &&
668
668
  query.join.some(
669
669
  (join) =>
670
- join.type === `left` || join.type === `right` || join.type === `full`
670
+ join.type === `left` || join.type === `right` || join.type === `full`,
671
671
  )
672
672
 
673
673
  // Add single-source clauses
@@ -690,8 +690,8 @@ function applyOptimizations(
690
690
  ? [
691
691
  combineWithAnd(
692
692
  remainingWhereClauses.flatMap((clause) =>
693
- splitAndClausesRecursive(getWhereExpression(clause))
694
- )
693
+ splitAndClausesRecursive(getWhereExpression(clause)),
694
+ ),
695
695
  ),
696
696
  ]
697
697
  : remainingWhereClauses
@@ -749,11 +749,11 @@ function deepCopyQuery(query: QueryIR): QueryIR {
749
749
  joinClause.from.type === `collectionRef`
750
750
  ? new CollectionRefClass(
751
751
  joinClause.from.collection,
752
- joinClause.from.alias
752
+ joinClause.from.alias,
753
753
  )
754
754
  : new QueryRefClass(
755
755
  deepCopyQuery(joinClause.from.query),
756
- joinClause.from.alias
756
+ joinClause.from.alias,
757
757
  ),
758
758
  }))
759
759
  : undefined,
@@ -780,7 +780,7 @@ function deepCopyQuery(query: QueryIR): QueryIR {
780
780
  function optimizeFromWithTracking(
781
781
  from: From,
782
782
  singleSourceClauses: Map<string, BasicExpression<boolean>>,
783
- actuallyOptimized: Set<string>
783
+ actuallyOptimized: Set<string>,
784
784
  ): From {
785
785
  const whereClause = singleSourceClauses.get(from.alias)
786
786
 
@@ -833,7 +833,7 @@ function optimizeFromWithTracking(
833
833
  function unsafeSelect(
834
834
  query: QueryIR,
835
835
  whereClause: BasicExpression<boolean>,
836
- outerAlias: string
836
+ outerAlias: string,
837
837
  ): boolean {
838
838
  if (!query.select) return false
839
839
 
@@ -870,7 +870,7 @@ function unsafeFnSelect(query: QueryIR) {
870
870
  function isSafeToPushIntoExistingSubquery(
871
871
  query: QueryIR,
872
872
  whereClause: BasicExpression<boolean>,
873
- outerAlias: string
873
+ outerAlias: string,
874
874
  ): boolean {
875
875
  return !(
876
876
  unsafeSelect(query, whereClause, outerAlias) ||
@@ -945,7 +945,7 @@ function collectRefs(expr: any): Array<PropRef> {
945
945
  function whereReferencesComputedSelectFields(
946
946
  select: Select,
947
947
  whereClause: BasicExpression<boolean>,
948
- outerAlias: string
948
+ outerAlias: string,
949
949
  ): boolean {
950
950
  // Build a set of computed field names at the top-level of the subquery output
951
951
  const computed = new Set<string>()
@@ -979,7 +979,7 @@ function whereReferencesComputedSelectFields(
979
979
  function referencesAliasWithRemappedSelect(
980
980
  subquery: QueryIR,
981
981
  whereClause: BasicExpression<boolean>,
982
- outerAlias: string
982
+ outerAlias: string,
983
983
  ): boolean {
984
984
  const refs = collectRefs(whereClause)
985
985
  // Only care about clauses that actually reference the outer alias.
@@ -1046,7 +1046,7 @@ function referencesAliasWithRemappedSelect(
1046
1046
  * @throws Error if the expressions array is empty
1047
1047
  */
1048
1048
  function combineWithAnd(
1049
- expressions: Array<BasicExpression<boolean>>
1049
+ expressions: Array<BasicExpression<boolean>>,
1050
1050
  ): BasicExpression<boolean> {
1051
1051
  if (expressions.length === 0) {
1052
1052
  throw new CannotCombineEmptyExpressionListError()