@tanstack/db 0.4.19 → 0.5.0

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 (153) hide show
  1. package/dist/cjs/collection/change-events.cjs +10 -12
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/change-events.d.cts +1 -8
  4. package/dist/cjs/collection/index.cjs +19 -1
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +7 -5
  7. package/dist/cjs/collection/sync.cjs +7 -1
  8. package/dist/cjs/collection/sync.cjs.map +1 -1
  9. package/dist/cjs/errors.cjs +9 -4
  10. package/dist/cjs/errors.cjs.map +1 -1
  11. package/dist/cjs/errors.d.cts +4 -1
  12. package/dist/cjs/index.cjs +21 -3
  13. package/dist/cjs/index.cjs.map +1 -1
  14. package/dist/cjs/index.d.cts +2 -0
  15. package/dist/cjs/indexes/auto-index.cjs +7 -3
  16. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  17. package/dist/cjs/local-storage.cjs.map +1 -1
  18. package/dist/cjs/local-storage.d.cts +2 -2
  19. package/dist/cjs/query/builder/functions.cjs +34 -0
  20. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  21. package/dist/cjs/query/builder/functions.d.cts +5 -0
  22. package/dist/cjs/query/builder/index.cjs +2 -2
  23. package/dist/cjs/query/builder/index.cjs.map +1 -1
  24. package/dist/cjs/query/builder/types.d.cts +18 -24
  25. package/dist/cjs/query/compiler/evaluators.cjs +57 -4
  26. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  27. package/dist/cjs/query/compiler/evaluators.d.cts +13 -0
  28. package/dist/cjs/query/compiler/expressions.cjs +4 -1
  29. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  30. package/dist/cjs/query/compiler/group-by.cjs +3 -3
  31. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  32. package/dist/cjs/query/compiler/index.cjs +2 -2
  33. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  34. package/dist/cjs/query/compiler/order-by.cjs +18 -6
  35. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  36. package/dist/cjs/query/compiler/order-by.d.cts +7 -1
  37. package/dist/cjs/query/expression-helpers.cjs +217 -0
  38. package/dist/cjs/query/expression-helpers.cjs.map +1 -0
  39. package/dist/cjs/query/expression-helpers.d.cts +216 -0
  40. package/dist/cjs/query/index.d.cts +2 -0
  41. package/dist/cjs/query/live/collection-config-builder.cjs +34 -2
  42. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  43. package/dist/cjs/query/live/collection-config-builder.d.cts +7 -1
  44. package/dist/cjs/query/live/collection-registry.cjs +2 -1
  45. package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
  46. package/dist/cjs/query/live/collection-registry.d.cts +1 -1
  47. package/dist/cjs/query/live/internal.cjs +5 -0
  48. package/dist/cjs/query/live/internal.cjs.map +1 -0
  49. package/dist/cjs/query/live/internal.d.cts +13 -0
  50. package/dist/cjs/query/live/types.d.cts +6 -1
  51. package/dist/cjs/query/predicate-utils.cjs +816 -0
  52. package/dist/cjs/query/predicate-utils.cjs.map +1 -0
  53. package/dist/cjs/query/predicate-utils.d.cts +116 -0
  54. package/dist/cjs/query/subset-dedupe.cjs +111 -0
  55. package/dist/cjs/query/subset-dedupe.cjs.map +1 -0
  56. package/dist/cjs/query/subset-dedupe.d.cts +66 -0
  57. package/dist/cjs/types.d.cts +31 -2
  58. package/dist/cjs/utils/comparison.cjs +30 -0
  59. package/dist/cjs/utils/comparison.cjs.map +1 -1
  60. package/dist/cjs/utils/comparison.d.cts +7 -1
  61. package/dist/cjs/utils/index-optimization.cjs +26 -22
  62. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  63. package/dist/cjs/utils/index-optimization.d.cts +5 -4
  64. package/dist/esm/collection/change-events.d.ts +1 -8
  65. package/dist/esm/collection/change-events.js +7 -9
  66. package/dist/esm/collection/change-events.js.map +1 -1
  67. package/dist/esm/collection/index.d.ts +7 -5
  68. package/dist/esm/collection/index.js +19 -1
  69. package/dist/esm/collection/index.js.map +1 -1
  70. package/dist/esm/collection/sync.js +7 -1
  71. package/dist/esm/collection/sync.js.map +1 -1
  72. package/dist/esm/errors.d.ts +4 -1
  73. package/dist/esm/errors.js +9 -4
  74. package/dist/esm/errors.js.map +1 -1
  75. package/dist/esm/index.d.ts +2 -0
  76. package/dist/esm/index.js +19 -1
  77. package/dist/esm/index.js.map +1 -1
  78. package/dist/esm/indexes/auto-index.js +7 -3
  79. package/dist/esm/indexes/auto-index.js.map +1 -1
  80. package/dist/esm/local-storage.d.ts +2 -2
  81. package/dist/esm/local-storage.js.map +1 -1
  82. package/dist/esm/query/builder/functions.d.ts +5 -0
  83. package/dist/esm/query/builder/functions.js +34 -0
  84. package/dist/esm/query/builder/functions.js.map +1 -1
  85. package/dist/esm/query/builder/index.js +2 -2
  86. package/dist/esm/query/builder/index.js.map +1 -1
  87. package/dist/esm/query/builder/types.d.ts +18 -24
  88. package/dist/esm/query/compiler/evaluators.d.ts +13 -0
  89. package/dist/esm/query/compiler/evaluators.js +59 -6
  90. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  91. package/dist/esm/query/compiler/expressions.js +4 -1
  92. package/dist/esm/query/compiler/expressions.js.map +1 -1
  93. package/dist/esm/query/compiler/group-by.js +4 -4
  94. package/dist/esm/query/compiler/group-by.js.map +1 -1
  95. package/dist/esm/query/compiler/index.js +3 -3
  96. package/dist/esm/query/compiler/index.js.map +1 -1
  97. package/dist/esm/query/compiler/order-by.d.ts +7 -1
  98. package/dist/esm/query/compiler/order-by.js +18 -6
  99. package/dist/esm/query/compiler/order-by.js.map +1 -1
  100. package/dist/esm/query/expression-helpers.d.ts +216 -0
  101. package/dist/esm/query/expression-helpers.js +217 -0
  102. package/dist/esm/query/expression-helpers.js.map +1 -0
  103. package/dist/esm/query/index.d.ts +2 -0
  104. package/dist/esm/query/live/collection-config-builder.d.ts +7 -1
  105. package/dist/esm/query/live/collection-config-builder.js +34 -2
  106. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  107. package/dist/esm/query/live/collection-registry.d.ts +1 -1
  108. package/dist/esm/query/live/collection-registry.js +2 -1
  109. package/dist/esm/query/live/collection-registry.js.map +1 -1
  110. package/dist/esm/query/live/internal.d.ts +13 -0
  111. package/dist/esm/query/live/internal.js +5 -0
  112. package/dist/esm/query/live/internal.js.map +1 -0
  113. package/dist/esm/query/live/types.d.ts +6 -1
  114. package/dist/esm/query/predicate-utils.d.ts +116 -0
  115. package/dist/esm/query/predicate-utils.js +816 -0
  116. package/dist/esm/query/predicate-utils.js.map +1 -0
  117. package/dist/esm/query/subset-dedupe.d.ts +66 -0
  118. package/dist/esm/query/subset-dedupe.js +111 -0
  119. package/dist/esm/query/subset-dedupe.js.map +1 -0
  120. package/dist/esm/types.d.ts +31 -2
  121. package/dist/esm/utils/comparison.d.ts +7 -1
  122. package/dist/esm/utils/comparison.js +30 -0
  123. package/dist/esm/utils/comparison.js.map +1 -1
  124. package/dist/esm/utils/index-optimization.d.ts +5 -4
  125. package/dist/esm/utils/index-optimization.js +26 -22
  126. package/dist/esm/utils/index-optimization.js.map +1 -1
  127. package/package.json +2 -2
  128. package/src/collection/change-events.ts +14 -24
  129. package/src/collection/index.ts +34 -6
  130. package/src/collection/sync.ts +9 -1
  131. package/src/errors.ts +20 -4
  132. package/src/index.ts +4 -0
  133. package/src/indexes/auto-index.ts +8 -4
  134. package/src/local-storage.ts +11 -3
  135. package/src/query/builder/functions.ts +39 -0
  136. package/src/query/builder/index.ts +2 -2
  137. package/src/query/builder/types.ts +19 -27
  138. package/src/query/compiler/evaluators.ts +103 -5
  139. package/src/query/compiler/expressions.ts +3 -0
  140. package/src/query/compiler/group-by.ts +4 -4
  141. package/src/query/compiler/index.ts +3 -3
  142. package/src/query/compiler/order-by.ts +33 -7
  143. package/src/query/expression-helpers.ts +522 -0
  144. package/src/query/index.ts +12 -0
  145. package/src/query/live/collection-config-builder.ts +54 -2
  146. package/src/query/live/collection-registry.ts +3 -2
  147. package/src/query/live/internal.ts +15 -0
  148. package/src/query/live/types.ts +11 -1
  149. package/src/query/predicate-utils.ts +1415 -0
  150. package/src/query/subset-dedupe.ts +243 -0
  151. package/src/types.ts +41 -2
  152. package/src/utils/comparison.ts +70 -1
  153. package/src/utils/index-optimization.ts +77 -63
@@ -0,0 +1,243 @@
1
+ import {
2
+ isPredicateSubset,
3
+ isWhereSubset,
4
+ minusWherePredicates,
5
+ unionWherePredicates,
6
+ } from "./predicate-utils.js"
7
+ import type { BasicExpression } from "./ir.js"
8
+ import type { LoadSubsetOptions } from "../types.js"
9
+
10
+ /**
11
+ * Deduplicated wrapper for a loadSubset function.
12
+ * Tracks what data has been loaded and avoids redundant calls by applying
13
+ * subset logic to predicates.
14
+ *
15
+ * @param opts - The options for the DeduplicatedLoadSubset
16
+ * @param opts.loadSubset - The underlying loadSubset function to wrap
17
+ * @param opts.onDeduplicate - An optional callback function that is invoked when a loadSubset call is deduplicated.
18
+ * If the call is deduplicated because the requested data is being loaded by an inflight request,
19
+ * then this callback is invoked when the inflight request completes successfully and the data is fully loaded.
20
+ * This callback is useful if you need to track rows per query, in which case you can't ignore deduplicated calls
21
+ * because you need to know which rows were loaded for each query.
22
+ * @example
23
+ * const dedupe = new DeduplicatedLoadSubset({ loadSubset: myLoadSubset, onDeduplicate: (opts) => console.log(`Call was deduplicated:`, opts) })
24
+ *
25
+ * // First call - fetches data
26
+ * await dedupe.loadSubset({ where: gt(ref('age'), val(10)) })
27
+ *
28
+ * // Second call - subset of first, returns true immediately
29
+ * await dedupe.loadSubset({ where: gt(ref('age'), val(20)) })
30
+ *
31
+ * // Clear state to start fresh
32
+ * dedupe.reset()
33
+ */
34
+ export class DeduplicatedLoadSubset {
35
+ // The underlying loadSubset function to wrap
36
+ private readonly _loadSubset: (
37
+ options: LoadSubsetOptions
38
+ ) => true | Promise<void>
39
+
40
+ // An optional callback function that is invoked when a loadSubset call is deduplicated.
41
+ private readonly onDeduplicate:
42
+ | ((options: LoadSubsetOptions) => void)
43
+ | undefined
44
+
45
+ // Combined where predicate for all unlimited calls (no limit)
46
+ private unlimitedWhere: BasicExpression<boolean> | undefined = undefined
47
+
48
+ // Flag to track if we've loaded all data (unlimited call with no where clause)
49
+ private hasLoadedAllData = false
50
+
51
+ // List of all limited calls (with limit, possibly with orderBy)
52
+ // We clone options before storing to prevent mutation of stored predicates
53
+ private limitedCalls: Array<LoadSubsetOptions> = []
54
+
55
+ // Track in-flight calls to prevent concurrent duplicate requests
56
+ // We store both the options and the promise so we can apply subset logic
57
+ private inflightCalls: Array<{
58
+ options: LoadSubsetOptions
59
+ promise: Promise<void>
60
+ }> = []
61
+
62
+ // Generation counter to invalidate in-flight requests after reset()
63
+ // When reset() is called, this increments, and any in-flight completion handlers
64
+ // check if their captured generation matches before updating tracking state
65
+ private generation = 0
66
+
67
+ constructor(opts: {
68
+ loadSubset: (options: LoadSubsetOptions) => true | Promise<void>
69
+ onDeduplicate?: (options: LoadSubsetOptions) => void
70
+ }) {
71
+ this._loadSubset = opts.loadSubset
72
+ this.onDeduplicate = opts.onDeduplicate
73
+ }
74
+
75
+ /**
76
+ * Load a subset of data, with automatic deduplication based on previously
77
+ * loaded predicates and in-flight requests.
78
+ *
79
+ * This method is auto-bound, so it can be safely passed as a callback without
80
+ * losing its `this` context (e.g., `loadSubset: dedupe.loadSubset` in a sync config).
81
+ *
82
+ * @param options - The predicate options (where, orderBy, limit)
83
+ * @returns true if data is already loaded, or a Promise that resolves when data is loaded
84
+ */
85
+ loadSubset = (options: LoadSubsetOptions): true | Promise<void> => {
86
+ // If we've loaded all data, everything is covered
87
+ if (this.hasLoadedAllData) {
88
+ this.onDeduplicate?.(options)
89
+ return true
90
+ }
91
+
92
+ // Check against unlimited combined predicate
93
+ // If we've loaded all data matching a where clause, we don't need to refetch subsets
94
+ if (this.unlimitedWhere !== undefined && options.where !== undefined) {
95
+ if (isWhereSubset(options.where, this.unlimitedWhere)) {
96
+ this.onDeduplicate?.(options)
97
+ return true // Data already loaded via unlimited call
98
+ }
99
+ }
100
+
101
+ // Check against limited calls
102
+ if (options.limit !== undefined) {
103
+ const alreadyLoaded = this.limitedCalls.some((loaded) =>
104
+ isPredicateSubset(options, loaded)
105
+ )
106
+
107
+ if (alreadyLoaded) {
108
+ this.onDeduplicate?.(options)
109
+ return true // Already loaded
110
+ }
111
+ }
112
+
113
+ // Check against in-flight calls using the same subset logic as resolved calls
114
+ // This prevents duplicate requests when concurrent calls have subset relationships
115
+ const matchingInflight = this.inflightCalls.find((inflight) =>
116
+ isPredicateSubset(options, inflight.options)
117
+ )
118
+
119
+ if (matchingInflight !== undefined) {
120
+ // An in-flight call will load data that covers this request
121
+ // Return the same promise so this caller waits for the data to load
122
+ // The in-flight promise already handles tracking updates when it completes
123
+ const prom = matchingInflight.promise
124
+ // Call `onDeduplicate` when the inflight request has loaded the data
125
+ prom.then(() => this.onDeduplicate?.(options)).catch() // ignore errors
126
+ return prom
127
+ }
128
+
129
+ // Not fully covered by existing data
130
+ // Compute the subset of data that is not covered by the existing data
131
+ // such that we only have to load that subset of missing data
132
+ const clonedOptions = cloneOptions(options)
133
+ if (this.unlimitedWhere !== undefined && options.limit === undefined) {
134
+ // Compute difference to get only the missing data
135
+ // We can only do this for unlimited queries
136
+ // and we can only remove data that was loaded from unlimited queries
137
+ // because with limited queries we have no way to express that we already loaded part of the matching data
138
+ clonedOptions.where =
139
+ minusWherePredicates(clonedOptions.where, this.unlimitedWhere) ??
140
+ clonedOptions.where
141
+ }
142
+
143
+ // Call underlying loadSubset to load the missing data
144
+ const resultPromise = this._loadSubset(clonedOptions)
145
+
146
+ // Handle both sync (true) and async (Promise<void>) return values
147
+ if (resultPromise === true) {
148
+ // Sync return - update tracking synchronously
149
+ // Clone options before storing to protect against caller mutation
150
+ this.updateTracking(clonedOptions)
151
+ return true
152
+ } else {
153
+ // Async return - track the promise and update tracking after it resolves
154
+
155
+ // Capture the current generation - this lets us detect if reset() was called
156
+ // while this request was in-flight, so we can skip updating tracking state
157
+ const capturedGeneration = this.generation
158
+
159
+ // We need to create a reference to the in-flight entry so we can remove it later
160
+ const inflightEntry = {
161
+ options: clonedOptions, // Store cloned options for subset matching
162
+ promise: resultPromise
163
+ .then((result) => {
164
+ // Only update tracking if this request is still from the current generation
165
+ // If reset() was called, the generation will have incremented and we should
166
+ // not repopulate the state that was just cleared
167
+ if (capturedGeneration === this.generation) {
168
+ // Use the cloned options that we captured before any caller mutations
169
+ // This ensures we track exactly what was loaded, not what the caller changed
170
+ this.updateTracking(clonedOptions)
171
+ }
172
+ return result
173
+ })
174
+ .finally(() => {
175
+ // Always remove from in-flight array on completion OR rejection
176
+ // This ensures failed requests can be retried instead of being cached forever
177
+ const index = this.inflightCalls.indexOf(inflightEntry)
178
+ if (index !== -1) {
179
+ this.inflightCalls.splice(index, 1)
180
+ }
181
+ }),
182
+ }
183
+
184
+ // Store the in-flight entry so concurrent subset calls can wait for it
185
+ this.inflightCalls.push(inflightEntry)
186
+ return inflightEntry.promise
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Reset all tracking state.
192
+ * Clears the history of loaded predicates and in-flight calls.
193
+ * Use this when you want to start fresh, for example after clearing the underlying data store.
194
+ *
195
+ * Note: Any in-flight requests will still complete, but they will not update the tracking
196
+ * state after the reset. This prevents old requests from repopulating cleared state.
197
+ */
198
+ reset(): void {
199
+ this.unlimitedWhere = undefined
200
+ this.hasLoadedAllData = false
201
+ this.limitedCalls = []
202
+ this.inflightCalls = []
203
+ // Increment generation to invalidate any in-flight completion handlers
204
+ // This ensures requests that were started before reset() don't repopulate the state
205
+ this.generation++
206
+ }
207
+
208
+ private updateTracking(options: LoadSubsetOptions): void {
209
+ // Update tracking based on whether this was a limited or unlimited call
210
+ if (options.limit === undefined) {
211
+ // Unlimited call - update combined where predicate
212
+ // We ignore orderBy for unlimited calls as mentioned in requirements
213
+ if (options.where === undefined) {
214
+ // No where clause = all data loaded
215
+ this.hasLoadedAllData = true
216
+ this.unlimitedWhere = undefined
217
+ this.limitedCalls = []
218
+ this.inflightCalls = []
219
+ } else if (this.unlimitedWhere === undefined) {
220
+ this.unlimitedWhere = options.where
221
+ } else {
222
+ this.unlimitedWhere = unionWherePredicates([
223
+ this.unlimitedWhere,
224
+ options.where,
225
+ ])
226
+ }
227
+ } else {
228
+ // Limited call - add to list for future subset checks
229
+ // Options are already cloned by caller to prevent mutation issues
230
+ this.limitedCalls.push(options)
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Clones a LoadSubsetOptions object to prevent mutation of stored predicates.
237
+ * This is crucial because callers often reuse the same options object and mutate
238
+ * properties like limit or where between calls. Without cloning, our stored history
239
+ * would reflect the mutated values rather than what was actually loaded.
240
+ */
241
+ export function cloneOptions(options: LoadSubsetOptions): LoadSubsetOptions {
242
+ return { ...options }
243
+ }
package/src/types.ts CHANGED
@@ -5,6 +5,37 @@ import type { Transaction } from "./transactions"
5
5
  import type { BasicExpression, OrderBy } from "./query/ir.js"
6
6
  import type { EventEmitter } from "./event-emitter.js"
7
7
 
8
+ /**
9
+ * Interface for a collection-like object that provides the necessary methods
10
+ * for the change events system to work
11
+ */
12
+ export interface CollectionLike<
13
+ T extends object = Record<string, unknown>,
14
+ TKey extends string | number = string | number,
15
+ > extends Pick<
16
+ Collection<T, TKey>,
17
+ `get` | `has` | `entries` | `indexes` | `id` | `compareOptions`
18
+ > {}
19
+
20
+ /**
21
+ * StringSortOpts - Options for string sorting behavior
22
+ *
23
+ * This discriminated union allows for two types of string sorting:
24
+ * - **Lexical**: Simple character-by-character comparison (default)
25
+ * - **Locale**: Locale-aware sorting with optional customization
26
+ *
27
+ * The union ensures that locale options are only available when locale sorting is selected.
28
+ */
29
+ export type StringCollationConfig =
30
+ | {
31
+ stringSort?: `lexical`
32
+ }
33
+ | {
34
+ stringSort?: `locale`
35
+ locale?: string
36
+ localeOptions?: object
37
+ }
38
+
8
39
  /**
9
40
  * Helper type to extract the output type from a standard schema
10
41
  *
@@ -35,9 +66,9 @@ export type TransactionState = `pending` | `persisting` | `completed` | `failed`
35
66
  export type Fn = (...args: Array<any>) => any
36
67
 
37
68
  /**
38
- * A record of utility functions that can be attached to a collection
69
+ * A record of utilities (functions or getters) that can be attached to a collection
39
70
  */
40
- export type UtilsRecord = Record<string, Fn>
71
+ export type UtilsRecord = Record<string, any>
41
72
 
42
73
  /**
43
74
  *
@@ -582,6 +613,14 @@ export interface BaseCollectionConfig<
582
613
  */
583
614
  onDelete?: DeleteMutationFn<T, TKey, TUtils, TReturn>
584
615
 
616
+ /**
617
+ * Specifies how to compare data in the collection.
618
+ * This should be configured to match data ordering on the backend.
619
+ * E.g., when using the Electric DB collection these options
620
+ * should match the database's collation settings.
621
+ */
622
+ defaultStringCollation?: StringCollationConfig
623
+
585
624
  utils?: TUtils
586
625
  }
587
626
 
@@ -112,11 +112,80 @@ export const defaultComparator = makeComparator({
112
112
  })
113
113
 
114
114
  /**
115
- * Normalize a value for comparison
115
+ * Compare two Uint8Arrays for content equality
116
+ */
117
+ function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {
118
+ if (a.byteLength !== b.byteLength) {
119
+ return false
120
+ }
121
+ for (let i = 0; i < a.byteLength; i++) {
122
+ if (a[i] !== b[i]) {
123
+ return false
124
+ }
125
+ }
126
+ return true
127
+ }
128
+
129
+ /**
130
+ * Threshold for normalizing Uint8Arrays to string representations.
131
+ * Arrays larger than this will use reference equality to avoid memory overhead.
132
+ * 128 bytes is enough for common ID formats (ULIDs are 16 bytes, UUIDs are 16 bytes)
133
+ * while avoiding excessive string allocation for large binary data.
134
+ */
135
+ const UINT8ARRAY_NORMALIZE_THRESHOLD = 128
136
+
137
+ /**
138
+ * Normalize a value for comparison and Map key usage
139
+ * Converts values that can't be directly compared or used as Map keys
140
+ * into comparable primitive representations
116
141
  */
117
142
  export function normalizeValue(value: any): any {
118
143
  if (value instanceof Date) {
119
144
  return value.getTime()
120
145
  }
146
+
147
+ // Normalize Uint8Arrays/Buffers to a string representation for Map key usage
148
+ // This enables content-based equality for binary data like ULIDs
149
+ const isUint8Array =
150
+ (typeof Buffer !== `undefined` && value instanceof Buffer) ||
151
+ value instanceof Uint8Array
152
+
153
+ if (isUint8Array) {
154
+ // Only normalize small arrays to avoid memory overhead for large binary data
155
+ if (value.byteLength <= UINT8ARRAY_NORMALIZE_THRESHOLD) {
156
+ // Convert to a string representation that can be used as a Map key
157
+ // Use a special prefix to avoid collisions with user strings
158
+ return `__u8__${Array.from(value).join(`,`)}`
159
+ }
160
+ // For large arrays, fall back to reference equality
161
+ // Users working with large binary data should use a derived key if needed
162
+ }
163
+
121
164
  return value
122
165
  }
166
+
167
+ /**
168
+ * Compare two values for equality, with special handling for Uint8Arrays and Buffers
169
+ */
170
+ export function areValuesEqual(a: any, b: any): boolean {
171
+ // Fast path for reference equality
172
+ if (a === b) {
173
+ return true
174
+ }
175
+
176
+ // Check for Uint8Array/Buffer comparison
177
+ const aIsUint8Array =
178
+ (typeof Buffer !== `undefined` && a instanceof Buffer) ||
179
+ a instanceof Uint8Array
180
+ const bIsUint8Array =
181
+ (typeof Buffer !== `undefined` && b instanceof Buffer) ||
182
+ b instanceof Uint8Array
183
+
184
+ // If both are Uint8Arrays, compare by content
185
+ if (aIsUint8Array && bIsUint8Array) {
186
+ return areUint8ArraysEqual(a, b)
187
+ }
188
+
189
+ // Different types or not Uint8Arrays
190
+ return false
191
+ }