@tanstack/db 0.1.8 → 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.
package/src/proxy.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * and provides a way to retrieve those changes.
4
4
  */
5
5
 
6
+ import { deepEquals, isTemporal } from "./utils"
7
+
6
8
  /**
7
9
  * Simple debug utility that only logs when debug mode is enabled
8
10
  * Set DEBUG to true in localStorage to enable debug logging
@@ -133,6 +135,13 @@ function deepClone<T extends unknown>(
133
135
  return clone as unknown as T
134
136
  }
135
137
 
138
+ // Handle Temporal objects
139
+ if (isTemporal(obj)) {
140
+ // Temporal objects are immutable, so we can return them directly
141
+ // This preserves all their internal state correctly
142
+ return obj
143
+ }
144
+
136
145
  const clone = {} as Record<string | symbol, unknown>
137
146
  visited.set(obj as object, clone)
138
147
 
@@ -156,107 +165,6 @@ function deepClone<T extends unknown>(
156
165
  return clone as T
157
166
  }
158
167
 
159
- /**
160
- * Deep equality check that handles special types like Date, RegExp, Map, and Set
161
- */
162
- function deepEqual<T>(a: T, b: T): boolean {
163
- // Handle primitive types
164
- if (a === b) return true
165
-
166
- // If either is null or not an object, they're not equal
167
- if (
168
- a === null ||
169
- b === null ||
170
- typeof a !== `object` ||
171
- typeof b !== `object`
172
- ) {
173
- return false
174
- }
175
-
176
- // Handle Date objects
177
- if (a instanceof Date && b instanceof Date) {
178
- return a.getTime() === b.getTime()
179
- }
180
-
181
- // Handle RegExp objects
182
- if (a instanceof RegExp && b instanceof RegExp) {
183
- return a.source === b.source && a.flags === b.flags
184
- }
185
-
186
- // Handle Map objects
187
- if (a instanceof Map && b instanceof Map) {
188
- if (a.size !== b.size) return false
189
-
190
- const entries = Array.from(a.entries())
191
- for (const [key, val] of entries) {
192
- if (!b.has(key) || !deepEqual(val, b.get(key))) {
193
- return false
194
- }
195
- }
196
-
197
- return true
198
- }
199
-
200
- // Handle Set objects
201
- if (a instanceof Set && b instanceof Set) {
202
- if (a.size !== b.size) return false
203
-
204
- // Convert to arrays for comparison
205
- const aValues = Array.from(a)
206
- const bValues = Array.from(b)
207
-
208
- // Simple comparison for primitive values
209
- if (aValues.every((val) => typeof val !== `object`)) {
210
- return aValues.every((val) => b.has(val))
211
- }
212
-
213
- // For objects in sets, we need to do a more complex comparison
214
- // This is a simplified approach and may not work for all cases
215
- return aValues.length === bValues.length
216
- }
217
-
218
- // Handle arrays
219
- if (Array.isArray(a) && Array.isArray(b)) {
220
- if (a.length !== b.length) return false
221
-
222
- for (let i = 0; i < a.length; i++) {
223
- if (!deepEqual(a[i], b[i])) return false
224
- }
225
-
226
- return true
227
- }
228
-
229
- // Handle TypedArrays
230
- if (
231
- ArrayBuffer.isView(a) &&
232
- ArrayBuffer.isView(b) &&
233
- !(a instanceof DataView) &&
234
- !(b instanceof DataView)
235
- ) {
236
- const typedA = a as unknown as TypedArray
237
- const typedB = b as unknown as TypedArray
238
- if (typedA.length !== typedB.length) return false
239
-
240
- for (let i = 0; i < typedA.length; i++) {
241
- if (typedA[i] !== typedB[i]) return false
242
- }
243
-
244
- return true
245
- }
246
-
247
- // Handle plain objects
248
- const keysA = Object.keys(a as object)
249
- const keysB = Object.keys(b as object)
250
-
251
- if (keysA.length !== keysB.length) return false
252
-
253
- return keysA.every(
254
- (key) =>
255
- Object.prototype.hasOwnProperty.call(b, key) &&
256
- deepEqual((a as any)[key], (b as any)[key])
257
- )
258
- }
259
-
260
168
  let count = 0
261
169
  function getProxyCount() {
262
170
  count += 1
@@ -392,7 +300,7 @@ export function createChangeProxy<
392
300
  )
393
301
 
394
302
  // If the value is not equal to original, something is still changed
395
- if (!deepEqual(currentValue, originalValue)) {
303
+ if (!deepEquals(currentValue, originalValue)) {
396
304
  debugLog(`Property ${String(prop)} is different, returning false`)
397
305
  return false
398
306
  }
@@ -411,7 +319,7 @@ export function createChangeProxy<
411
319
  const originalValue = (state.originalObject as any)[sym]
412
320
 
413
321
  // If the value is not equal to original, something is still changed
414
- if (!deepEqual(currentValue, originalValue)) {
322
+ if (!deepEquals(currentValue, originalValue)) {
415
323
  debugLog(`Symbol property is different, returning false`)
416
324
  return false
417
325
  }
@@ -741,12 +649,13 @@ export function createChangeProxy<
741
649
  return value.bind(ptarget)
742
650
  }
743
651
 
744
- // If the value is an object, create a proxy for it
652
+ // If the value is an object (but not Date, RegExp, or Temporal), create a proxy for it
745
653
  if (
746
654
  value &&
747
655
  typeof value === `object` &&
748
656
  !((value as any) instanceof Date) &&
749
- !((value as any) instanceof RegExp)
657
+ !((value as any) instanceof RegExp) &&
658
+ !isTemporal(value)
750
659
  ) {
751
660
  // Create a parent reference for the nested object
752
661
  const nestedParent = {
@@ -779,11 +688,11 @@ export function createChangeProxy<
779
688
  )
780
689
 
781
690
  // Only track the change if the value is actually different
782
- if (!deepEqual(currentValue, value)) {
691
+ if (!deepEquals(currentValue, value)) {
783
692
  // Check if the new value is equal to the original value
784
693
  // Important: Use the originalObject to get the true original value
785
694
  const originalValue = changeTracker.originalObject[prop as keyof T]
786
- const isRevertToOriginal = deepEqual(value, originalValue)
695
+ const isRevertToOriginal = deepEquals(value, originalValue)
787
696
  debugLog(
788
697
  `value:`,
789
698
  value,
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
+ }