@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.
- package/dist/cjs/collection/change-events.cjs +10 -12
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +1 -8
- package/dist/cjs/collection/index.cjs +19 -1
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +7 -5
- package/dist/cjs/collection/sync.cjs +7 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +9 -4
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +4 -1
- package/dist/cjs/index.cjs +21 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/indexes/auto-index.cjs +7 -3
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +2 -2
- package/dist/cjs/query/builder/functions.cjs +34 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +5 -0
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +18 -24
- package/dist/cjs/query/compiler/evaluators.cjs +57 -4
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +13 -0
- package/dist/cjs/query/compiler/expressions.cjs +4 -1
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +3 -3
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +2 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +18 -6
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +7 -1
- package/dist/cjs/query/expression-helpers.cjs +217 -0
- package/dist/cjs/query/expression-helpers.cjs.map +1 -0
- package/dist/cjs/query/expression-helpers.d.cts +216 -0
- package/dist/cjs/query/index.d.cts +2 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +34 -2
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -1
- package/dist/cjs/query/live/collection-registry.cjs +2 -1
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
- package/dist/cjs/query/live/collection-registry.d.cts +1 -1
- package/dist/cjs/query/live/internal.cjs +5 -0
- package/dist/cjs/query/live/internal.cjs.map +1 -0
- package/dist/cjs/query/live/internal.d.cts +13 -0
- package/dist/cjs/query/live/types.d.cts +6 -1
- package/dist/cjs/query/predicate-utils.cjs +816 -0
- package/dist/cjs/query/predicate-utils.cjs.map +1 -0
- package/dist/cjs/query/predicate-utils.d.cts +116 -0
- package/dist/cjs/query/subset-dedupe.cjs +111 -0
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -0
- package/dist/cjs/query/subset-dedupe.d.cts +66 -0
- package/dist/cjs/types.d.cts +31 -2
- package/dist/cjs/utils/comparison.cjs +30 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +7 -1
- package/dist/cjs/utils/index-optimization.cjs +26 -22
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.d.cts +5 -4
- package/dist/esm/collection/change-events.d.ts +1 -8
- package/dist/esm/collection/change-events.js +7 -9
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +7 -5
- package/dist/esm/collection/index.js +19 -1
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/sync.js +7 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +4 -1
- package/dist/esm/errors.js +9 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +19 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +7 -3
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/local-storage.d.ts +2 -2
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +5 -0
- package/dist/esm/query/builder/functions.js +34 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +2 -2
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +18 -24
- package/dist/esm/query/compiler/evaluators.d.ts +13 -0
- package/dist/esm/query/compiler/evaluators.js +59 -6
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.js +4 -1
- package/dist/esm/query/compiler/expressions.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +4 -4
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +3 -3
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +7 -1
- package/dist/esm/query/compiler/order-by.js +18 -6
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/expression-helpers.d.ts +216 -0
- package/dist/esm/query/expression-helpers.js +217 -0
- package/dist/esm/query/expression-helpers.js.map +1 -0
- package/dist/esm/query/index.d.ts +2 -0
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -1
- package/dist/esm/query/live/collection-config-builder.js +34 -2
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +1 -1
- package/dist/esm/query/live/collection-registry.js +2 -1
- package/dist/esm/query/live/collection-registry.js.map +1 -1
- package/dist/esm/query/live/internal.d.ts +13 -0
- package/dist/esm/query/live/internal.js +5 -0
- package/dist/esm/query/live/internal.js.map +1 -0
- package/dist/esm/query/live/types.d.ts +6 -1
- package/dist/esm/query/predicate-utils.d.ts +116 -0
- package/dist/esm/query/predicate-utils.js +816 -0
- package/dist/esm/query/predicate-utils.js.map +1 -0
- package/dist/esm/query/subset-dedupe.d.ts +66 -0
- package/dist/esm/query/subset-dedupe.js +111 -0
- package/dist/esm/query/subset-dedupe.js.map +1 -0
- package/dist/esm/types.d.ts +31 -2
- package/dist/esm/utils/comparison.d.ts +7 -1
- package/dist/esm/utils/comparison.js +30 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.d.ts +5 -4
- package/dist/esm/utils/index-optimization.js +26 -22
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/package.json +2 -2
- package/src/collection/change-events.ts +14 -24
- package/src/collection/index.ts +34 -6
- package/src/collection/sync.ts +9 -1
- package/src/errors.ts +20 -4
- package/src/index.ts +4 -0
- package/src/indexes/auto-index.ts +8 -4
- package/src/local-storage.ts +11 -3
- package/src/query/builder/functions.ts +39 -0
- package/src/query/builder/index.ts +2 -2
- package/src/query/builder/types.ts +19 -27
- package/src/query/compiler/evaluators.ts +103 -5
- package/src/query/compiler/expressions.ts +3 -0
- package/src/query/compiler/group-by.ts +4 -4
- package/src/query/compiler/index.ts +3 -3
- package/src/query/compiler/order-by.ts +33 -7
- package/src/query/expression-helpers.ts +522 -0
- package/src/query/index.ts +12 -0
- package/src/query/live/collection-config-builder.ts +54 -2
- package/src/query/live/collection-registry.ts +3 -2
- package/src/query/live/internal.ts +15 -0
- package/src/query/live/types.ts +11 -1
- package/src/query/predicate-utils.ts +1415 -0
- package/src/query/subset-dedupe.ts +243 -0
- package/src/types.ts +41 -2
- package/src/utils/comparison.ts +70 -1
- 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
|
|
69
|
+
* A record of utilities (functions or getters) that can be attached to a collection
|
|
39
70
|
*/
|
|
40
|
-
export type UtilsRecord = Record<string,
|
|
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
|
|
package/src/utils/comparison.ts
CHANGED
|
@@ -112,11 +112,80 @@ export const defaultComparator = makeComparator({
|
|
|
112
112
|
})
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
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
|
+
}
|