@tanstack/db 0.1.7 → 0.1.9

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 (34) hide show
  1. package/dist/cjs/collection.cjs +6 -21
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +0 -1
  4. package/dist/cjs/proxy.cjs +9 -58
  5. package/dist/cjs/proxy.cjs.map +1 -1
  6. package/dist/cjs/query/live/collection-config-builder.cjs +24 -17
  7. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  8. package/dist/cjs/query/live/collection-config-builder.d.cts +0 -2
  9. package/dist/cjs/query/live/collection-subscriber.cjs +25 -16
  10. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  11. package/dist/cjs/query/live/collection-subscriber.d.cts +1 -1
  12. package/dist/cjs/utils.cjs +75 -0
  13. package/dist/cjs/utils.cjs.map +1 -1
  14. package/dist/cjs/utils.d.cts +5 -0
  15. package/dist/esm/collection.d.ts +0 -1
  16. package/dist/esm/collection.js +6 -21
  17. package/dist/esm/collection.js.map +1 -1
  18. package/dist/esm/proxy.js +9 -58
  19. package/dist/esm/proxy.js.map +1 -1
  20. package/dist/esm/query/live/collection-config-builder.d.ts +0 -2
  21. package/dist/esm/query/live/collection-config-builder.js +24 -17
  22. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  23. package/dist/esm/query/live/collection-subscriber.d.ts +1 -1
  24. package/dist/esm/query/live/collection-subscriber.js +25 -16
  25. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  26. package/dist/esm/utils.d.ts +5 -0
  27. package/dist/esm/utils.js +76 -1
  28. package/dist/esm/utils.js.map +1 -1
  29. package/package.json +3 -2
  30. package/src/collection.ts +9 -26
  31. package/src/proxy.ts +16 -107
  32. package/src/query/live/collection-config-builder.ts +30 -21
  33. package/src/query/live/collection-subscriber.ts +44 -25
  34. package/src/utils.ts +125 -0
@@ -85,20 +85,20 @@ export class CollectionSubscriber<
85
85
  changes,
86
86
  this.collection.config.getKey
87
87
  )
88
- if (sentChanges > 0 || !this.collectionConfigBuilder.isCollectionReady()) {
89
- // Only run the graph if we sent any changes
90
- // otherwise we may get into an infinite loop
91
- // trying to load more data for the orderBy query
92
- // when there's no more data in the collection
93
- // EXCEPTION: if the collection is not yet ready
94
- // we need to run it even if there are no changes
95
- // in order for the collection to be marked as ready
96
- this.collectionConfigBuilder.maybeRunGraph(
97
- this.config,
98
- this.syncState,
99
- callback
100
- )
101
- }
88
+
89
+ // Do not provide the callback that loads more data
90
+ // if there's no more data to load
91
+ // otherwise we end up in an infinite loop trying to load more data
92
+ const dataLoader = sentChanges > 0 ? callback : undefined
93
+
94
+ // We need to call `maybeRunGraph` even if there's no data to load
95
+ // because we need to mark the collection as ready if it's not already
96
+ // and that's only done in `maybeRunGraph`
97
+ this.collectionConfigBuilder.maybeRunGraph(
98
+ this.config,
99
+ this.syncState,
100
+ dataLoader
101
+ )
102
102
  }
103
103
 
104
104
  // Wraps the sendChangesToPipeline function
@@ -229,7 +229,7 @@ export class CollectionSubscriber<
229
229
  private subscribeToOrderedChanges(
230
230
  whereExpression: BasicExpression<boolean> | undefined
231
231
  ) {
232
- const { offset, limit, comparator } =
232
+ const { offset, limit, comparator, dataNeeded } =
233
233
  this.collectionConfigBuilder.optimizableOrderByCollections[
234
234
  this.collectionId
235
235
  ]!
@@ -245,11 +245,18 @@ export class CollectionSubscriber<
245
245
  // and filter out changes that are bigger than the biggest value we've sent so far
246
246
  // because they can't affect the topK
247
247
  const splittedChanges = splitUpdates(changes)
248
- const filteredChanges = filterChangesSmallerOrEqualToMax(
249
- splittedChanges,
250
- comparator,
251
- this.biggest
252
- )
248
+ let filteredChanges = splittedChanges
249
+ if (dataNeeded!() === 0) {
250
+ // If the topK is full [..., maxSentValue] then we do not need to send changes > maxSentValue
251
+ // because they can never make it into the topK.
252
+ // However, if the topK isn't full yet, we need to also send changes > maxSentValue
253
+ // because they will make it into the topK
254
+ filteredChanges = filterChangesSmallerOrEqualToMax(
255
+ splittedChanges,
256
+ comparator,
257
+ this.biggest
258
+ )
259
+ }
253
260
  this.sendChangesToPipeline(
254
261
  filteredChanges,
255
262
  this.loadMoreIfNeeded.bind(this)
@@ -268,11 +275,19 @@ export class CollectionSubscriber<
268
275
  // This function is called by maybeRunGraph
269
276
  // after each iteration of the query pipeline
270
277
  // to ensure that the orderBy operator has enough data to work with
271
- private loadMoreIfNeeded() {
272
- const { dataNeeded } =
278
+ loadMoreIfNeeded() {
279
+ const orderByInfo =
273
280
  this.collectionConfigBuilder.optimizableOrderByCollections[
274
281
  this.collectionId
275
- ]!
282
+ ]
283
+
284
+ if (!orderByInfo) {
285
+ // This query has no orderBy operator
286
+ // so there's no data to load, just return true
287
+ return true
288
+ }
289
+
290
+ const { dataNeeded } = orderByInfo
276
291
 
277
292
  if (!dataNeeded) {
278
293
  // This should never happen because the topK operator should always set the size callback
@@ -285,12 +300,15 @@ export class CollectionSubscriber<
285
300
  // `dataNeeded` probes the orderBy operator to see if it needs more data
286
301
  // if it needs more data, it returns the number of items it needs
287
302
  const n = dataNeeded()
303
+ let noMoreNextItems = false
288
304
  if (n > 0) {
289
- this.loadNextItems(n)
305
+ const loadedItems = this.loadNextItems(n)
306
+ noMoreNextItems = loadedItems === 0
290
307
  }
291
308
 
292
309
  // Indicate that we're done loading data if we didn't need to load more data
293
- return n === 0
310
+ // or there's no more data to load
311
+ return n === 0 || noMoreNextItems
294
312
  }
295
313
 
296
314
  private sendChangesToPipelineWithTracking(
@@ -322,6 +340,7 @@ export class CollectionSubscriber<
322
340
  return { type: `insert`, key, value: this.collection.get(key) }
323
341
  })
324
342
  this.sendChangesToPipelineWithTracking(nextInserts)
343
+ return nextInserts.length
325
344
  }
326
345
 
327
346
  private getWhereClauseFromAlias(
package/src/utils.ts CHANGED
@@ -2,8 +2,14 @@
2
2
  * Generic utility functions
3
3
  */
4
4
 
5
+ interface TypedArray {
6
+ length: number
7
+ [index: number]: number
8
+ }
9
+
5
10
  /**
6
11
  * Deep equality function that compares two values recursively
12
+ * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects
7
13
  *
8
14
  * @param a - First value to compare
9
15
  * @param b - Second value to compare
@@ -14,6 +20,8 @@
14
20
  * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)
15
21
  * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true
16
22
  * deepEquals({ a: 1 }, { a: 2 }) // false
23
+ * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true
24
+ * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true
17
25
  * ```
18
26
  */
19
27
  export function deepEquals(a: any, b: any): boolean {
@@ -37,6 +45,102 @@ function deepEqualsInternal(
37
45
  // Handle different types
38
46
  if (typeof a !== typeof b) return false
39
47
 
48
+ // Handle Date objects
49
+ if (a instanceof Date) {
50
+ if (!(b instanceof Date)) return false
51
+ return a.getTime() === b.getTime()
52
+ }
53
+
54
+ // Handle RegExp objects
55
+ if (a instanceof RegExp) {
56
+ if (!(b instanceof RegExp)) return false
57
+ return a.source === b.source && a.flags === b.flags
58
+ }
59
+
60
+ // Handle Map objects - only if both are Maps
61
+ if (a instanceof Map) {
62
+ if (!(b instanceof Map)) return false
63
+ if (a.size !== b.size) return false
64
+
65
+ // Check for circular references
66
+ if (visited.has(a)) {
67
+ return visited.get(a) === b
68
+ }
69
+ visited.set(a, b)
70
+
71
+ const entries = Array.from(a.entries())
72
+ const result = entries.every(([key, val]) => {
73
+ return b.has(key) && deepEqualsInternal(val, b.get(key), visited)
74
+ })
75
+
76
+ visited.delete(a)
77
+ return result
78
+ }
79
+
80
+ // Handle Set objects - only if both are Sets
81
+ if (a instanceof Set) {
82
+ if (!(b instanceof Set)) return false
83
+ if (a.size !== b.size) return false
84
+
85
+ // Check for circular references
86
+ if (visited.has(a)) {
87
+ return visited.get(a) === b
88
+ }
89
+ visited.set(a, b)
90
+
91
+ // Convert to arrays for comparison
92
+ const aValues = Array.from(a)
93
+ const bValues = Array.from(b)
94
+
95
+ // Simple comparison for primitive values
96
+ if (aValues.every((val) => typeof val !== `object`)) {
97
+ visited.delete(a)
98
+ return aValues.every((val) => b.has(val))
99
+ }
100
+
101
+ // For objects in sets, we need to do a more complex comparison
102
+ // This is a simplified approach and may not work for all cases
103
+ const result = aValues.length === bValues.length
104
+ visited.delete(a)
105
+ return result
106
+ }
107
+
108
+ // Handle TypedArrays
109
+ if (
110
+ ArrayBuffer.isView(a) &&
111
+ ArrayBuffer.isView(b) &&
112
+ !(a instanceof DataView) &&
113
+ !(b instanceof DataView)
114
+ ) {
115
+ const typedA = a as unknown as TypedArray
116
+ const typedB = b as unknown as TypedArray
117
+ if (typedA.length !== typedB.length) return false
118
+
119
+ for (let i = 0; i < typedA.length; i++) {
120
+ if (typedA[i] !== typedB[i]) return false
121
+ }
122
+
123
+ return true
124
+ }
125
+
126
+ // Handle Temporal objects
127
+ // Check if both are Temporal objects of the same type
128
+ if (isTemporal(a) && isTemporal(b)) {
129
+ const aTag = getStringTag(a)
130
+ const bTag = getStringTag(b)
131
+
132
+ // If they're different Temporal types, they're not equal
133
+ if (aTag !== bTag) return false
134
+
135
+ // Use Temporal's built-in equals method if available
136
+ if (typeof a.equals === `function`) {
137
+ return a.equals(b)
138
+ }
139
+
140
+ // Fallback to toString comparison for other types
141
+ return a.toString() === b.toString()
142
+ }
143
+
40
144
  // Handle arrays
41
145
  if (Array.isArray(a)) {
42
146
  if (!Array.isArray(b) || a.length !== b.length) return false
@@ -84,3 +188,24 @@ function deepEqualsInternal(
84
188
  // For primitives that aren't strictly equal
85
189
  return false
86
190
  }
191
+
192
+ const temporalTypes = [
193
+ `Temporal.Duration`,
194
+ `Temporal.Instant`,
195
+ `Temporal.PlainDate`,
196
+ `Temporal.PlainDateTime`,
197
+ `Temporal.PlainMonthDay`,
198
+ `Temporal.PlainTime`,
199
+ `Temporal.PlainYearMonth`,
200
+ `Temporal.ZonedDateTime`,
201
+ ]
202
+
203
+ function getStringTag(a: any): any {
204
+ return a[Symbol.toStringTag]
205
+ }
206
+
207
+ /** Checks if the value is a Temporal object by checking for the Temporal brand */
208
+ export function isTemporal(a: any): boolean {
209
+ const tag = getStringTag(a)
210
+ return typeof tag === `string` && temporalTypes.includes(tag)
211
+ }