@tanstack/db
Version:
A reactive client store for building super fast apps on sync
113 lines (98 loc) • 3.09 kB
text/typescript
import type { CompareOptions } from "../query/builder/types"
// WeakMap to store stable IDs for objects
const objectIds = new WeakMap<object, number>()
let nextObjectId = 1
/**
* Get or create a stable ID for an object
*/
function getObjectId(obj: object): number {
if (objectIds.has(obj)) {
return objectIds.get(obj)!
}
const id = nextObjectId++
objectIds.set(obj, id)
return id
}
/**
* Universal comparison function for all data types
* Handles null/undefined, strings, arrays, dates, objects, and primitives
* Always sorts null/undefined values first
*/
export const ascComparator = (a: any, b: any, opts: CompareOptions): number => {
const { nulls } = opts
// Handle null/undefined
if (a == null && b == null) return 0
if (a == null) return nulls === `first` ? -1 : 1
if (b == null) return nulls === `first` ? 1 : -1
// if a and b are both strings, compare them based on locale
if (typeof a === `string` && typeof b === `string`) {
if (opts.stringSort === `locale`) {
return a.localeCompare(b, opts.locale, opts.localeOptions)
}
// For lexical sort we rely on direct comparison for primitive values
}
// if a and b are both arrays, compare them element by element
if (Array.isArray(a) && Array.isArray(b)) {
for (let i = 0; i < Math.min(a.length, b.length); i++) {
const result = ascComparator(a[i], b[i], opts)
if (result !== 0) {
return result
}
}
// All elements are equal up to the minimum length
return a.length - b.length
}
// If both are dates, compare them
if (a instanceof Date && b instanceof Date) {
return a.getTime() - b.getTime()
}
// If at least one of the values is an object, use stable IDs for comparison
const aIsObject = typeof a === `object`
const bIsObject = typeof b === `object`
if (aIsObject || bIsObject) {
// If both are objects, compare their stable IDs
if (aIsObject && bIsObject) {
const aId = getObjectId(a)
const bId = getObjectId(b)
return aId - bId
}
// If only one is an object, objects come after primitives
if (aIsObject) return 1
if (bIsObject) return -1
}
// For primitive values, use direct comparison
if (a < b) return -1
if (a > b) return 1
return 0
}
/**
* Descending comparator function for ordering values
* Handles null/undefined as largest values (opposite of ascending)
*/
export const descComparator = (
a: unknown,
b: unknown,
opts: CompareOptions
): number => {
return ascComparator(b, a, {
...opts,
nulls: opts.nulls === `first` ? `last` : `first`,
})
}
export function makeComparator(
opts: CompareOptions
): (a: any, b: any) => number {
return (a, b) => {
if (opts.direction === `asc`) {
return ascComparator(a, b, opts)
} else {
return descComparator(a, b, opts)
}
}
}
/** Default comparator orders values in ascending order with nulls first and locale string comparison. */
export const defaultComparator = makeComparator({
direction: `asc`,
nulls: `first`,
stringSort: `locale`,
})