@tanstack/db 0.0.26 → 0.0.29
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/change-events.cjs +141 -0
- package/dist/cjs/change-events.cjs.map +1 -0
- package/dist/cjs/change-events.d.cts +49 -0
- package/dist/cjs/collection.cjs +236 -90
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +95 -20
- package/dist/cjs/errors.cjs +509 -1
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +225 -1
- package/dist/cjs/index.cjs +82 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +5 -1
- package/dist/cjs/indexes/auto-index.cjs +64 -0
- package/dist/cjs/indexes/auto-index.cjs.map +1 -0
- package/dist/cjs/indexes/auto-index.d.cts +9 -0
- package/dist/cjs/indexes/base-index.cjs +46 -0
- package/dist/cjs/indexes/base-index.cjs.map +1 -0
- package/dist/cjs/indexes/base-index.d.cts +54 -0
- package/dist/cjs/indexes/index-options.d.cts +13 -0
- package/dist/cjs/indexes/lazy-index.cjs +193 -0
- package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
- package/dist/cjs/indexes/lazy-index.d.cts +96 -0
- package/dist/cjs/indexes/ordered-index.cjs +227 -0
- package/dist/cjs/indexes/ordered-index.cjs.map +1 -0
- package/dist/cjs/indexes/ordered-index.d.cts +72 -0
- package/dist/cjs/local-storage.cjs +9 -15
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +11 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +4 -0
- package/dist/cjs/query/builder/index.cjs +6 -7
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.cjs +37 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +12 -0
- package/dist/cjs/query/compiler/evaluators.cjs +83 -58
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +8 -0
- package/dist/cjs/query/compiler/expressions.cjs +61 -0
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -0
- package/dist/cjs/query/compiler/expressions.d.cts +25 -0
- package/dist/cjs/query/compiler/group-by.cjs +5 -10
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +23 -17
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +12 -3
- package/dist/cjs/query/compiler/joins.cjs +61 -12
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +4 -34
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/types.d.cts +2 -2
- package/dist/cjs/query/live-query-collection.cjs +54 -12
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/optimizer.cjs +45 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +13 -3
- package/dist/cjs/transactions.cjs +5 -4
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +31 -0
- package/dist/cjs/utils/array-utils.cjs +18 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +8 -0
- package/dist/cjs/utils/comparison.cjs +52 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -0
- package/dist/cjs/utils/comparison.d.cts +11 -0
- package/dist/cjs/utils/index-optimization.cjs +270 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -0
- package/dist/cjs/utils/index-optimization.d.cts +29 -0
- package/dist/esm/change-events.d.ts +49 -0
- package/dist/esm/change-events.js +141 -0
- package/dist/esm/change-events.js.map +1 -0
- package/dist/esm/collection.d.ts +95 -20
- package/dist/esm/collection.js +234 -88
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/errors.d.ts +225 -1
- package/dist/esm/errors.js +510 -2
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.js +81 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.d.ts +9 -0
- package/dist/esm/indexes/auto-index.js +64 -0
- package/dist/esm/indexes/auto-index.js.map +1 -0
- package/dist/esm/indexes/base-index.d.ts +54 -0
- package/dist/esm/indexes/base-index.js +46 -0
- package/dist/esm/indexes/base-index.js.map +1 -0
- package/dist/esm/indexes/index-options.d.ts +13 -0
- package/dist/esm/indexes/lazy-index.d.ts +96 -0
- package/dist/esm/indexes/lazy-index.js +193 -0
- package/dist/esm/indexes/lazy-index.js.map +1 -0
- package/dist/esm/indexes/ordered-index.d.ts +72 -0
- package/dist/esm/indexes/ordered-index.js +227 -0
- package/dist/esm/indexes/ordered-index.js.map +1 -0
- package/dist/esm/local-storage.js +9 -15
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +4 -0
- package/dist/esm/query/builder/functions.js +11 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +6 -7
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
- package/dist/esm/query/builder/ref-proxy.js +37 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.d.ts +8 -0
- package/dist/esm/query/compiler/evaluators.js +84 -59
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.d.ts +25 -0
- package/dist/esm/query/compiler/expressions.js +61 -0
- package/dist/esm/query/compiler/expressions.js.map +1 -0
- package/dist/esm/query/compiler/group-by.js +5 -10
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +12 -3
- package/dist/esm/query/compiler/index.js +23 -17
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +61 -12
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js +1 -31
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/types.d.ts +2 -2
- package/dist/esm/query/live-query-collection.js +54 -12
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +13 -3
- package/dist/esm/query/optimizer.js +40 -2
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/transactions.js +5 -4
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +31 -0
- package/dist/esm/utils/array-utils.d.ts +8 -0
- package/dist/esm/utils/array-utils.js +18 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.d.ts +11 -0
- package/dist/esm/utils/comparison.js +52 -0
- package/dist/esm/utils/comparison.js.map +1 -0
- package/dist/esm/utils/index-optimization.d.ts +29 -0
- package/dist/esm/utils/index-optimization.js +270 -0
- package/dist/esm/utils/index-optimization.js.map +1 -0
- package/package.json +3 -2
- package/src/change-events.ts +257 -0
- package/src/collection.ts +321 -110
- package/src/errors.ts +545 -1
- package/src/index.ts +7 -1
- package/src/indexes/auto-index.ts +108 -0
- package/src/indexes/base-index.ts +119 -0
- package/src/indexes/index-options.ts +42 -0
- package/src/indexes/lazy-index.ts +251 -0
- package/src/indexes/ordered-index.ts +305 -0
- package/src/local-storage.ts +16 -17
- package/src/query/builder/functions.ts +14 -0
- package/src/query/builder/index.ts +12 -7
- package/src/query/builder/ref-proxy.ts +65 -0
- package/src/query/compiler/evaluators.ts +114 -62
- package/src/query/compiler/expressions.ts +92 -0
- package/src/query/compiler/group-by.ts +10 -10
- package/src/query/compiler/index.ts +52 -22
- package/src/query/compiler/joins.ts +114 -15
- package/src/query/compiler/order-by.ts +1 -45
- package/src/query/compiler/types.ts +2 -2
- package/src/query/live-query-collection.ts +95 -15
- package/src/query/optimizer.ts +94 -5
- package/src/transactions.ts +10 -4
- package/src/types.ts +38 -0
- package/src/utils/array-utils.ts +28 -0
- package/src/utils/comparison.ts +79 -0
- package/src/utils/index-optimization.ts +546 -0
package/src/query/optimizer.ts
CHANGED
|
@@ -116,11 +116,13 @@
|
|
|
116
116
|
*/
|
|
117
117
|
|
|
118
118
|
import { deepEquals } from "../utils.js"
|
|
119
|
+
import { CannotCombineEmptyExpressionListError } from "../errors.js"
|
|
119
120
|
import {
|
|
120
121
|
CollectionRef as CollectionRefClass,
|
|
121
122
|
Func,
|
|
122
123
|
QueryRef as QueryRefClass,
|
|
123
124
|
} from "./ir.js"
|
|
125
|
+
import { isConvertibleToCollectionFilter } from "./compiler/expressions.js"
|
|
124
126
|
import type { BasicExpression, From, QueryIR } from "./ir.js"
|
|
125
127
|
|
|
126
128
|
/**
|
|
@@ -143,6 +145,16 @@ export interface GroupedWhereClauses {
|
|
|
143
145
|
multiSource?: BasicExpression<boolean>
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Result of query optimization including both the optimized query and collection-specific WHERE clauses
|
|
150
|
+
*/
|
|
151
|
+
export interface OptimizationResult {
|
|
152
|
+
/** The optimized query with WHERE clauses potentially moved to subqueries */
|
|
153
|
+
optimizedQuery: QueryIR
|
|
154
|
+
/** Map of collection aliases to their extracted WHERE clauses for index optimization */
|
|
155
|
+
collectionWhereClauses: Map<string, BasicExpression<boolean>>
|
|
156
|
+
}
|
|
157
|
+
|
|
146
158
|
/**
|
|
147
159
|
* Main query optimizer entry point that lifts WHERE clauses into subqueries.
|
|
148
160
|
*
|
|
@@ -151,7 +163,7 @@ export interface GroupedWhereClauses {
|
|
|
151
163
|
* sources as possible, then removing redundant subqueries.
|
|
152
164
|
*
|
|
153
165
|
* @param query - The QueryIR to optimize
|
|
154
|
-
* @returns
|
|
166
|
+
* @returns An OptimizationResult with the optimized query and collection WHERE clause mapping
|
|
155
167
|
*
|
|
156
168
|
* @example
|
|
157
169
|
* ```typescript
|
|
@@ -161,11 +173,15 @@ export interface GroupedWhereClauses {
|
|
|
161
173
|
* where: [eq(u.dept_id, 1), gt(p.views, 100)]
|
|
162
174
|
* }
|
|
163
175
|
*
|
|
164
|
-
* const
|
|
176
|
+
* const { optimizedQuery, collectionWhereClauses } = optimizeQuery(originalQuery)
|
|
165
177
|
* // Result: Single-source clauses moved to deepest possible subqueries
|
|
178
|
+
* // collectionWhereClauses: Map { 'u' => eq(u.dept_id, 1), 'p' => gt(p.views, 100) }
|
|
166
179
|
* ```
|
|
167
180
|
*/
|
|
168
|
-
export function optimizeQuery(query: QueryIR):
|
|
181
|
+
export function optimizeQuery(query: QueryIR): OptimizationResult {
|
|
182
|
+
// First, extract collection WHERE clauses before optimization
|
|
183
|
+
const collectionWhereClauses = extractCollectionWhereClauses(query)
|
|
184
|
+
|
|
169
185
|
// Apply multi-level predicate pushdown with iterative convergence
|
|
170
186
|
let optimized = query
|
|
171
187
|
let previousOptimized: QueryIR | undefined
|
|
@@ -185,7 +201,80 @@ export function optimizeQuery(query: QueryIR): QueryIR {
|
|
|
185
201
|
// Remove redundant subqueries
|
|
186
202
|
const cleaned = removeRedundantSubqueries(optimized)
|
|
187
203
|
|
|
188
|
-
return
|
|
204
|
+
return {
|
|
205
|
+
optimizedQuery: cleaned,
|
|
206
|
+
collectionWhereClauses,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extracts collection-specific WHERE clauses from a query for index optimization.
|
|
212
|
+
* This analyzes the original query to identify WHERE clauses that can be pushed down
|
|
213
|
+
* to specific collections, but only for simple queries without joins.
|
|
214
|
+
*
|
|
215
|
+
* @param query - The original QueryIR to analyze
|
|
216
|
+
* @returns Map of collection aliases to their WHERE clauses
|
|
217
|
+
*/
|
|
218
|
+
function extractCollectionWhereClauses(
|
|
219
|
+
query: QueryIR
|
|
220
|
+
): Map<string, BasicExpression<boolean>> {
|
|
221
|
+
const collectionWhereClauses = new Map<string, BasicExpression<boolean>>()
|
|
222
|
+
|
|
223
|
+
// Only analyze queries that have WHERE clauses
|
|
224
|
+
if (!query.where || query.where.length === 0) {
|
|
225
|
+
return collectionWhereClauses
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Split all AND clauses at the root level for granular analysis
|
|
229
|
+
const splitWhereClauses = splitAndClauses(query.where)
|
|
230
|
+
|
|
231
|
+
// Analyze each WHERE clause to determine which sources it touches
|
|
232
|
+
const analyzedClauses = splitWhereClauses.map((clause) =>
|
|
233
|
+
analyzeWhereClause(clause)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
// Group clauses by single-source vs multi-source
|
|
237
|
+
const groupedClauses = groupWhereClauses(analyzedClauses)
|
|
238
|
+
|
|
239
|
+
// Only include single-source clauses that reference collections directly
|
|
240
|
+
// and can be converted to BasicExpression format for collection indexes
|
|
241
|
+
for (const [sourceAlias, whereClause] of groupedClauses.singleSource) {
|
|
242
|
+
// Check if this source alias corresponds to a collection reference
|
|
243
|
+
if (isCollectionReference(query, sourceAlias)) {
|
|
244
|
+
// Check if the WHERE clause can be converted to collection-compatible format
|
|
245
|
+
if (isConvertibleToCollectionFilter(whereClause)) {
|
|
246
|
+
collectionWhereClauses.set(sourceAlias, whereClause)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return collectionWhereClauses
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Determines if a source alias refers to a collection reference (not a subquery).
|
|
256
|
+
* This is used to identify WHERE clauses that can be pushed down to collection subscriptions.
|
|
257
|
+
*
|
|
258
|
+
* @param query - The query to analyze
|
|
259
|
+
* @param sourceAlias - The source alias to check
|
|
260
|
+
* @returns True if the alias refers to a collection reference
|
|
261
|
+
*/
|
|
262
|
+
function isCollectionReference(query: QueryIR, sourceAlias: string): boolean {
|
|
263
|
+
// Check the FROM clause
|
|
264
|
+
if (query.from.alias === sourceAlias) {
|
|
265
|
+
return query.from.type === `collectionRef`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check JOIN clauses
|
|
269
|
+
if (query.join) {
|
|
270
|
+
for (const joinClause of query.join) {
|
|
271
|
+
if (joinClause.from.alias === sourceAlias) {
|
|
272
|
+
return joinClause.from.type === `collectionRef`
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return false
|
|
189
278
|
}
|
|
190
279
|
|
|
191
280
|
/**
|
|
@@ -726,7 +815,7 @@ function combineWithAnd(
|
|
|
726
815
|
expressions: Array<BasicExpression<boolean>>
|
|
727
816
|
): BasicExpression<boolean> {
|
|
728
817
|
if (expressions.length === 0) {
|
|
729
|
-
throw new
|
|
818
|
+
throw new CannotCombineEmptyExpressionListError()
|
|
730
819
|
}
|
|
731
820
|
|
|
732
821
|
if (expressions.length === 1) {
|
package/src/transactions.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { createDeferred } from "./deferred"
|
|
2
|
+
import {
|
|
3
|
+
MissingMutationFunctionError,
|
|
4
|
+
TransactionAlreadyCompletedRollbackError,
|
|
5
|
+
TransactionNotPendingCommitError,
|
|
6
|
+
TransactionNotPendingMutateError,
|
|
7
|
+
} from "./errors"
|
|
2
8
|
import type { Deferred } from "./deferred"
|
|
3
9
|
import type {
|
|
4
10
|
MutationFn,
|
|
@@ -124,7 +130,7 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
124
130
|
|
|
125
131
|
constructor(config: TransactionConfig<T>) {
|
|
126
132
|
if (typeof config.mutationFn === `undefined`) {
|
|
127
|
-
throw
|
|
133
|
+
throw new MissingMutationFunctionError()
|
|
128
134
|
}
|
|
129
135
|
this.id = config.id ?? crypto.randomUUID()
|
|
130
136
|
this.mutationFn = config.mutationFn
|
|
@@ -186,7 +192,7 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
186
192
|
*/
|
|
187
193
|
mutate(callback: () => void): Transaction<T> {
|
|
188
194
|
if (this.state !== `pending`) {
|
|
189
|
-
throw
|
|
195
|
+
throw new TransactionNotPendingMutateError()
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
registerTransaction(this)
|
|
@@ -260,7 +266,7 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
260
266
|
rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {
|
|
261
267
|
const isSecondaryRollback = config?.isSecondaryRollback ?? false
|
|
262
268
|
if (this.state === `completed`) {
|
|
263
|
-
throw
|
|
269
|
+
throw new TransactionAlreadyCompletedRollbackError()
|
|
264
270
|
}
|
|
265
271
|
|
|
266
272
|
this.setState(`failed`)
|
|
@@ -342,7 +348,7 @@ class Transaction<T extends object = Record<string, unknown>> {
|
|
|
342
348
|
*/
|
|
343
349
|
async commit(): Promise<Transaction<T>> {
|
|
344
350
|
if (this.state !== `pending`) {
|
|
345
|
-
throw
|
|
351
|
+
throw new TransactionNotPendingCommitError()
|
|
346
352
|
}
|
|
347
353
|
|
|
348
354
|
this.setState(`persisting`)
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type { Collection } from "./collection"
|
|
|
3
3
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
4
4
|
import type { Transaction } from "./transactions"
|
|
5
5
|
|
|
6
|
+
import type { SingleRowRefProxy } from "./query/builder/ref-proxy"
|
|
7
|
+
import type { BasicExpression } from "./query/ir.js"
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Helper type to extract the output type from a standard schema
|
|
8
11
|
*
|
|
@@ -374,6 +377,15 @@ export interface CollectionConfig<
|
|
|
374
377
|
* Defaults to false for lazy loading. Set to true to immediately sync.
|
|
375
378
|
*/
|
|
376
379
|
startSync?: boolean
|
|
380
|
+
/**
|
|
381
|
+
* Auto-indexing mode for the collection.
|
|
382
|
+
* When enabled, indexes will be automatically created for simple where expressions.
|
|
383
|
+
* @default "eager"
|
|
384
|
+
* @description
|
|
385
|
+
* - "off": No automatic indexing
|
|
386
|
+
* - "eager": Automatically create indexes for simple where expressions in subscribeChanges (default)
|
|
387
|
+
*/
|
|
388
|
+
autoIndex?: `off` | `eager`
|
|
377
389
|
/**
|
|
378
390
|
* Optional function to compare two items.
|
|
379
391
|
* This is used to order the items in the collection.
|
|
@@ -555,6 +567,32 @@ export type KeyedNamespacedRow = [unknown, NamespacedRow]
|
|
|
555
567
|
*/
|
|
556
568
|
export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>
|
|
557
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Options for subscribing to collection changes
|
|
572
|
+
*/
|
|
573
|
+
export interface SubscribeChangesOptions<
|
|
574
|
+
T extends object = Record<string, unknown>,
|
|
575
|
+
> {
|
|
576
|
+
/** Whether to include the current state as initial changes */
|
|
577
|
+
includeInitialState?: boolean
|
|
578
|
+
/** Filter changes using a where expression */
|
|
579
|
+
where?: (row: SingleRowRefProxy<T>) => any
|
|
580
|
+
/** Pre-compiled expression for filtering changes */
|
|
581
|
+
whereExpression?: BasicExpression<boolean>
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Options for getting current state as changes
|
|
586
|
+
*/
|
|
587
|
+
export interface CurrentStateAsChangesOptions<
|
|
588
|
+
T extends object = Record<string, unknown>,
|
|
589
|
+
> {
|
|
590
|
+
/** Filter the current state using a where expression */
|
|
591
|
+
where?: (row: SingleRowRefProxy<T>) => any
|
|
592
|
+
/** Pre-compiled expression for filtering the current state */
|
|
593
|
+
whereExpression?: BasicExpression<boolean>
|
|
594
|
+
}
|
|
595
|
+
|
|
558
596
|
/**
|
|
559
597
|
* Function type for listening to collection changes
|
|
560
598
|
* @param changes - Array of change messages describing what happened
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the correct insert position for a value in a sorted array using binary search
|
|
3
|
+
* @param sortedArray The sorted array to search in
|
|
4
|
+
* @param value The value to find the position for
|
|
5
|
+
* @param compareFn Comparison function to use for ordering
|
|
6
|
+
* @returns The index where the value should be inserted to maintain order
|
|
7
|
+
*/
|
|
8
|
+
export function findInsertPosition<T>(
|
|
9
|
+
sortedArray: Array<[T, any]>,
|
|
10
|
+
value: T,
|
|
11
|
+
compareFn: (a: T, b: T) => number
|
|
12
|
+
): number {
|
|
13
|
+
let left = 0
|
|
14
|
+
let right = sortedArray.length
|
|
15
|
+
|
|
16
|
+
while (left < right) {
|
|
17
|
+
const mid = Math.floor((left + right) / 2)
|
|
18
|
+
const comparison = compareFn(sortedArray[mid]![0], value)
|
|
19
|
+
|
|
20
|
+
if (comparison < 0) {
|
|
21
|
+
left = mid + 1
|
|
22
|
+
} else {
|
|
23
|
+
right = mid
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return left
|
|
28
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// WeakMap to store stable IDs for objects
|
|
2
|
+
const objectIds = new WeakMap<object, number>()
|
|
3
|
+
let nextObjectId = 1
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get or create a stable ID for an object
|
|
7
|
+
*/
|
|
8
|
+
function getObjectId(obj: object): number {
|
|
9
|
+
if (objectIds.has(obj)) {
|
|
10
|
+
return objectIds.get(obj)!
|
|
11
|
+
}
|
|
12
|
+
const id = nextObjectId++
|
|
13
|
+
objectIds.set(obj, id)
|
|
14
|
+
return id
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Universal comparison function for all data types
|
|
19
|
+
* Handles null/undefined, strings, arrays, dates, objects, and primitives
|
|
20
|
+
* Always sorts null/undefined values first
|
|
21
|
+
*/
|
|
22
|
+
export const ascComparator = (a: any, b: any): number => {
|
|
23
|
+
// Handle null/undefined
|
|
24
|
+
if (a == null && b == null) return 0
|
|
25
|
+
if (a == null) return -1
|
|
26
|
+
if (b == null) return 1
|
|
27
|
+
|
|
28
|
+
// if a and b are both strings, compare them based on locale
|
|
29
|
+
if (typeof a === `string` && typeof b === `string`) {
|
|
30
|
+
return a.localeCompare(b)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// if a and b are both arrays, compare them element by element
|
|
34
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
35
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
36
|
+
const result = ascComparator(a[i], b[i])
|
|
37
|
+
if (result !== 0) {
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// All elements are equal up to the minimum length
|
|
42
|
+
return a.length - b.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If both are dates, compare them
|
|
46
|
+
if (a instanceof Date && b instanceof Date) {
|
|
47
|
+
return a.getTime() - b.getTime()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If at least one of the values is an object, use stable IDs for comparison
|
|
51
|
+
const aIsObject = typeof a === `object`
|
|
52
|
+
const bIsObject = typeof b === `object`
|
|
53
|
+
|
|
54
|
+
if (aIsObject || bIsObject) {
|
|
55
|
+
// If both are objects, compare their stable IDs
|
|
56
|
+
if (aIsObject && bIsObject) {
|
|
57
|
+
const aId = getObjectId(a)
|
|
58
|
+
const bId = getObjectId(b)
|
|
59
|
+
return aId - bId
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If only one is an object, objects come after primitives
|
|
63
|
+
if (aIsObject) return 1
|
|
64
|
+
if (bIsObject) return -1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// For primitive values, use direct comparison
|
|
68
|
+
if (a < b) return -1
|
|
69
|
+
if (a > b) return 1
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Descending comparator function for ordering values
|
|
75
|
+
* Handles null/undefined as largest values (opposite of ascending)
|
|
76
|
+
*/
|
|
77
|
+
export const descComparator = (a: unknown, b: unknown): number => {
|
|
78
|
+
return ascComparator(b, a)
|
|
79
|
+
}
|