@tanstack/db 0.1.8 → 0.1.10

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 (51) hide show
  1. package/dist/cjs/collection.cjs +29 -30
  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/builder/index.cjs +7 -4
  7. package/dist/cjs/query/builder/index.cjs.map +1 -1
  8. package/dist/cjs/query/compiler/group-by.cjs +7 -6
  9. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  10. package/dist/cjs/query/compiler/group-by.d.cts +5 -1
  11. package/dist/cjs/query/compiler/index.cjs +1 -0
  12. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  13. package/dist/cjs/query/compiler/order-by.cjs +13 -5
  14. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  15. package/dist/cjs/query/compiler/order-by.d.cts +2 -2
  16. package/dist/cjs/query/live/collection-config-builder.cjs +3 -0
  17. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  18. package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
  19. package/dist/cjs/utils.cjs +75 -0
  20. package/dist/cjs/utils.cjs.map +1 -1
  21. package/dist/cjs/utils.d.cts +5 -0
  22. package/dist/esm/collection.d.ts +0 -1
  23. package/dist/esm/collection.js +29 -30
  24. package/dist/esm/collection.js.map +1 -1
  25. package/dist/esm/proxy.js +9 -58
  26. package/dist/esm/proxy.js.map +1 -1
  27. package/dist/esm/query/builder/index.js +7 -4
  28. package/dist/esm/query/builder/index.js.map +1 -1
  29. package/dist/esm/query/compiler/group-by.d.ts +5 -1
  30. package/dist/esm/query/compiler/group-by.js +8 -7
  31. package/dist/esm/query/compiler/group-by.js.map +1 -1
  32. package/dist/esm/query/compiler/index.js +1 -0
  33. package/dist/esm/query/compiler/index.js.map +1 -1
  34. package/dist/esm/query/compiler/order-by.d.ts +2 -2
  35. package/dist/esm/query/compiler/order-by.js +13 -5
  36. package/dist/esm/query/compiler/order-by.js.map +1 -1
  37. package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
  38. package/dist/esm/query/live/collection-config-builder.js +3 -0
  39. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  40. package/dist/esm/utils.d.ts +5 -0
  41. package/dist/esm/utils.js +76 -1
  42. package/dist/esm/utils.js.map +1 -1
  43. package/package.json +3 -2
  44. package/src/collection.ts +31 -35
  45. package/src/proxy.ts +16 -107
  46. package/src/query/builder/index.ts +11 -6
  47. package/src/query/compiler/group-by.ts +9 -8
  48. package/src/query/compiler/index.ts +1 -0
  49. package/src/query/compiler/order-by.ts +14 -5
  50. package/src/query/live/collection-config-builder.ts +7 -6
  51. package/src/utils.ts +125 -0
package/src/collection.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { withArrayChangeTracking, withChangeTracking } from "./proxy"
2
+ import { deepEquals } from "./utils"
2
3
  import { SortedMap } from "./SortedMap"
3
4
  import {
4
5
  createSingleRowRefProxy,
@@ -1194,8 +1195,29 @@ export class CollectionImpl<
1194
1195
  }
1195
1196
  }
1196
1197
 
1197
- const hasTruncateSync = this.pendingSyncedTransactions.some(
1198
- (t) => t.truncate === true
1198
+ // pending synced transactions could be either `committed` or still open.
1199
+ // we only want to process `committed` transactions here
1200
+ const {
1201
+ committedSyncedTransactions,
1202
+ uncommittedSyncedTransactions,
1203
+ hasTruncateSync,
1204
+ } = this.pendingSyncedTransactions.reduce(
1205
+ (acc, t) => {
1206
+ if (t.committed) {
1207
+ acc.committedSyncedTransactions.push(t)
1208
+ if (t.truncate === true) {
1209
+ acc.hasTruncateSync = true
1210
+ }
1211
+ } else {
1212
+ acc.uncommittedSyncedTransactions.push(t)
1213
+ }
1214
+ return acc
1215
+ },
1216
+ {
1217
+ committedSyncedTransactions: [] as Array<PendingSyncedTransaction<T>>,
1218
+ uncommittedSyncedTransactions: [] as Array<PendingSyncedTransaction<T>>,
1219
+ hasTruncateSync: false,
1220
+ }
1199
1221
  )
1200
1222
 
1201
1223
  if (!hasPersistingTransaction || hasTruncateSync) {
@@ -1204,7 +1226,7 @@ export class CollectionImpl<
1204
1226
 
1205
1227
  // First collect all keys that will be affected by sync operations
1206
1228
  const changedKeys = new Set<TKey>()
1207
- for (const transaction of this.pendingSyncedTransactions) {
1229
+ for (const transaction of committedSyncedTransactions) {
1208
1230
  for (const operation of transaction.operations) {
1209
1231
  changedKeys.add(operation.key as TKey)
1210
1232
  }
@@ -1227,7 +1249,7 @@ export class CollectionImpl<
1227
1249
  const events: Array<ChangeMessage<T, TKey>> = []
1228
1250
  const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`
1229
1251
 
1230
- for (const transaction of this.pendingSyncedTransactions) {
1252
+ for (const transaction of committedSyncedTransactions) {
1231
1253
  // Handle truncate operations first
1232
1254
  if (transaction.truncate) {
1233
1255
  // TRUNCATE PHASE
@@ -1303,13 +1325,10 @@ export class CollectionImpl<
1303
1325
  // re-apply optimistic mutations on top of the fresh synced base. This ensures
1304
1326
  // the UI preserves local intent while respecting server rebuild semantics.
1305
1327
  // Ordering: deletes (above) -> server ops (just applied) -> optimistic upserts.
1306
- const hadTruncate = this.pendingSyncedTransactions.some(
1307
- (t) => t.truncate === true
1308
- )
1309
- if (hadTruncate) {
1328
+ if (hasTruncateSync) {
1310
1329
  // Avoid duplicating keys that were inserted/updated by synced operations in this commit
1311
1330
  const syncedInsertedOrUpdatedKeys = new Set<TKey>()
1312
- for (const t of this.pendingSyncedTransactions) {
1331
+ for (const t of committedSyncedTransactions) {
1313
1332
  for (const op of t.operations) {
1314
1333
  if (op.type === `insert` || op.type === `update`) {
1315
1334
  syncedInsertedOrUpdatedKeys.add(op.key as TKey)
@@ -1448,7 +1467,7 @@ export class CollectionImpl<
1448
1467
  const isRedundantSync =
1449
1468
  completedOp &&
1450
1469
  newVisibleValue !== undefined &&
1451
- this.deepEqual(completedOp.value, newVisibleValue)
1470
+ deepEquals(completedOp.value, newVisibleValue)
1452
1471
 
1453
1472
  if (!isRedundantSync) {
1454
1473
  if (
@@ -1472,7 +1491,7 @@ export class CollectionImpl<
1472
1491
  } else if (
1473
1492
  previousVisibleValue !== undefined &&
1474
1493
  newVisibleValue !== undefined &&
1475
- !this.deepEqual(previousVisibleValue, newVisibleValue)
1494
+ !deepEquals(previousVisibleValue, newVisibleValue)
1476
1495
  ) {
1477
1496
  events.push({
1478
1497
  type: `update`,
@@ -1495,7 +1514,7 @@ export class CollectionImpl<
1495
1514
  // End batching and emit all events (combines any batched events with sync events)
1496
1515
  this.emitEvents(events, true)
1497
1516
 
1498
- this.pendingSyncedTransactions = []
1517
+ this.pendingSyncedTransactions = uncommittedSyncedTransactions
1499
1518
 
1500
1519
  // Clear the pre-sync state since sync operations are complete
1501
1520
  this.preSyncVisibleState.clear()
@@ -1715,29 +1734,6 @@ export class CollectionImpl<
1715
1734
  }
1716
1735
  }
1717
1736
 
1718
- private deepEqual(a: any, b: any): boolean {
1719
- if (a === b) return true
1720
- if (a == null || b == null) return false
1721
- if (typeof a !== typeof b) return false
1722
-
1723
- if (typeof a === `object`) {
1724
- if (Array.isArray(a) !== Array.isArray(b)) return false
1725
-
1726
- const keysA = Object.keys(a)
1727
- const keysB = Object.keys(b)
1728
- if (keysA.length !== keysB.length) return false
1729
-
1730
- const keysBSet = new Set(keysB)
1731
- for (const key of keysA) {
1732
- if (!keysBSet.has(key)) return false
1733
- if (!this.deepEqual(a[key], b[key])) return false
1734
- }
1735
- return true
1736
- }
1737
-
1738
- return false
1739
- }
1740
-
1741
1737
  public validateData(
1742
1738
  data: unknown,
1743
1739
  type: `insert` | `update`,
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,
@@ -14,7 +14,6 @@ import type {
14
14
  BasicExpression,
15
15
  JoinClause,
16
16
  OrderBy,
17
- OrderByClause,
18
17
  OrderByDirection,
19
18
  QueryIR,
20
19
  } from "../ir.js"
@@ -501,17 +500,23 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
501
500
  : undefined,
502
501
  }
503
502
 
504
- // Create the new OrderBy structure with expression and direction
505
- const orderByClause: OrderByClause = {
506
- expression: toExpression(result),
507
- compareOptions: opts,
503
+ const makeOrderByClause = (res: any) => {
504
+ return {
505
+ expression: toExpression(res),
506
+ compareOptions: opts,
507
+ }
508
508
  }
509
509
 
510
+ // Create the new OrderBy structure with expression and direction
511
+ const orderByClauses = Array.isArray(result)
512
+ ? result.map((r) => makeOrderByClause(r))
513
+ : [makeOrderByClause(result)]
514
+
510
515
  const existingOrderBy: OrderBy = this.query.orderBy || []
511
516
 
512
517
  return new BaseQueryBuilder({
513
518
  ...this.query,
514
- orderBy: [...existingOrderBy, orderByClause],
519
+ orderBy: [...existingOrderBy, ...orderByClauses],
515
520
  }) as any
516
521
  }
517
522
 
@@ -130,7 +130,7 @@ export function processGroupBy(
130
130
  if (havingClauses && havingClauses.length > 0) {
131
131
  for (const havingClause of havingClauses) {
132
132
  const havingExpression = getHavingExpression(havingClause)
133
- const transformedHavingClause = transformHavingClause(
133
+ const transformedHavingClause = replaceAggregatesByRefs(
134
134
  havingExpression,
135
135
  selectClause || {}
136
136
  )
@@ -265,7 +265,7 @@ export function processGroupBy(
265
265
  if (havingClauses && havingClauses.length > 0) {
266
266
  for (const havingClause of havingClauses) {
267
267
  const havingExpression = getHavingExpression(havingClause)
268
- const transformedHavingClause = transformHavingClause(
268
+ const transformedHavingClause = replaceAggregatesByRefs(
269
269
  havingExpression,
270
270
  selectClause || {}
271
271
  )
@@ -367,11 +367,12 @@ function getAggregateFunction(aggExpr: Aggregate) {
367
367
  }
368
368
 
369
369
  /**
370
- * Transforms a HAVING clause to replace Agg expressions with references to computed values
370
+ * Transforms basic expressions and aggregates to replace Agg expressions with references to computed values
371
371
  */
372
- function transformHavingClause(
372
+ export function replaceAggregatesByRefs(
373
373
  havingExpr: BasicExpression | Aggregate,
374
- selectClause: Select
374
+ selectClause: Select,
375
+ resultAlias: string = `result`
375
376
  ): BasicExpression {
376
377
  switch (havingExpr.type) {
377
378
  case `agg`: {
@@ -380,7 +381,7 @@ function transformHavingClause(
380
381
  for (const [alias, selectExpr] of Object.entries(selectClause)) {
381
382
  if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {
382
383
  // Replace with a reference to the computed aggregate
383
- return new PropRef([`result`, alias])
384
+ return new PropRef([resultAlias, alias])
384
385
  }
385
386
  }
386
387
  // If no matching aggregate found in SELECT, throw error
@@ -392,7 +393,7 @@ function transformHavingClause(
392
393
  // Transform function arguments recursively
393
394
  const transformedArgs = funcExpr.args.map(
394
395
  (arg: BasicExpression | Aggregate) =>
395
- transformHavingClause(arg, selectClause)
396
+ replaceAggregatesByRefs(arg, selectClause)
396
397
  )
397
398
  return new Func(funcExpr.name, transformedArgs)
398
399
  }
@@ -404,7 +405,7 @@ function transformHavingClause(
404
405
  const alias = refExpr.path[0]!
405
406
  if (selectClause[alias]) {
406
407
  // This is a reference to a SELECT alias, convert to result.alias
407
- return new PropRef([`result`, alias])
408
+ return new PropRef([resultAlias, alias])
408
409
  }
409
410
  }
410
411
  // Return as-is for other refs
@@ -259,6 +259,7 @@ export function compileQuery(
259
259
  rawQuery,
260
260
  pipeline,
261
261
  query.orderBy,
262
+ query.select || {},
262
263
  collections[mainCollectionId]!,
263
264
  optimizableOrderByCollections,
264
265
  query.limit,
@@ -4,9 +4,10 @@ import { PropRef } from "../ir.js"
4
4
  import { ensureIndexForField } from "../../indexes/auto-index.js"
5
5
  import { findIndexForField } from "../../utils/index-optimization.js"
6
6
  import { compileExpression } from "./evaluators.js"
7
+ import { replaceAggregatesByRefs } from "./group-by.js"
7
8
  import { followRef } from "./index.js"
8
9
  import type { CompiledSingleRowExpression } from "./evaluators.js"
9
- import type { OrderByClause, QueryIR } from "../ir.js"
10
+ import type { OrderByClause, QueryIR, Select } from "../ir.js"
10
11
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
11
12
  import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
12
13
  import type { BaseIndex } from "../../indexes/base-index.js"
@@ -33,16 +34,24 @@ export function processOrderBy(
33
34
  rawQuery: QueryIR,
34
35
  pipeline: NamespacedAndKeyedStream,
35
36
  orderByClause: Array<OrderByClause>,
37
+ selectClause: Select,
36
38
  collection: Collection,
37
39
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
38
40
  limit?: number,
39
41
  offset?: number
40
42
  ): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
41
43
  // Pre-compile all order by expressions
42
- const compiledOrderBy = orderByClause.map((clause) => ({
43
- compiledExpression: compileExpression(clause.expression),
44
- compareOptions: clause.compareOptions,
45
- }))
44
+ const compiledOrderBy = orderByClause.map((clause) => {
45
+ const clauseWithoutAggregates = replaceAggregatesByRefs(
46
+ clause.expression,
47
+ selectClause,
48
+ `__select_results`
49
+ )
50
+ return {
51
+ compiledExpression: compileExpression(clauseWithoutAggregates),
52
+ compareOptions: clause.compareOptions,
53
+ }
54
+ })
46
55
 
47
56
  // Create a value extractor function for the orderBy operator
48
57
  const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {
@@ -49,15 +49,11 @@ export class CollectionConfigBuilder<
49
49
  | undefined
50
50
 
51
51
  // Map of collection IDs to functions that load keys for that lazy collection
52
- readonly lazyCollectionsCallbacks: Record<string, LazyCollectionCallbacks> =
53
- {}
52
+ lazyCollectionsCallbacks: Record<string, LazyCollectionCallbacks> = {}
54
53
  // Set of collection IDs that are lazy collections
55
54
  readonly lazyCollections = new Set<string>()
56
55
  // Set of collection IDs that include an optimizable ORDER BY clause
57
- readonly optimizableOrderByCollections: Record<
58
- string,
59
- OrderByOptimizationInfo
60
- > = {}
56
+ optimizableOrderByCollections: Record<string, OrderByOptimizationInfo> = {}
61
57
 
62
58
  constructor(
63
59
  private readonly config: LiveQueryCollectionConfig<TContext, TResult>
@@ -168,6 +164,11 @@ export class CollectionConfigBuilder<
168
164
  this.inputsCache = undefined
169
165
  this.pipelineCache = undefined
170
166
  this.collectionWhereClausesCache = undefined
167
+
168
+ // Reset lazy collection state
169
+ this.lazyCollections.clear()
170
+ this.optimizableOrderByCollections = {}
171
+ this.lazyCollectionsCallbacks = {}
171
172
  }
172
173
  }
173
174
 
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
+ }