UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

256 lines (236 loc) • 8 kB
import { compareKeys } from '@tanstack/db-ivm' /** * A Map implementation that keeps its entries sorted based on a comparator function * @template TKey - The type of keys in the map (must be string | number) * @template TValue - The type of values in the map */ export class SortedMap<TKey extends string | number, TValue> { private map: Map<TKey, TValue> private sortedKeys: Array<TKey> private comparator: ((a: TValue, b: TValue) => number) | undefined // Keys set or deleted with deferOrder since the last restoreOrder. A dirty // key can still hold a slot in sortedKeys that its old value ordered. private dirtyKeys = new Set<TKey>() /** * Creates a new SortedMap instance * * @param comparator - Optional function to compare values for sorting. * If not provided, entries are sorted by key only. */ constructor(comparator?: (a: TValue, b: TValue) => number) { this.map = new Map<TKey, TValue>() this.sortedKeys = [] this.comparator = comparator } /** * Finds the index where a key-value pair should be inserted to maintain sort order. * Uses binary search to find the correct position based on the value (if comparator provided), * with key-based tie-breaking for deterministic ordering when values compare as equal. * If no comparator is provided, sorts by key only. * Runs in O(log n) time. * * @param key - The key to find position for (used as tie-breaker or primary sort when no comparator) * @param value - The value to compare against (only used if comparator is provided) * @param keys - The ordered keys to search * @returns The index where the key should be inserted */ private indexOf( key: TKey, value: TValue, keys: Array<TKey> = this.sortedKeys, ): number { let left = 0 let right = keys.length // Fast path: no comparator means sort by key only if (!this.comparator) { while (left < right) { const mid = Math.floor((left + right) / 2) const midKey = keys[mid]! const keyComparison = compareKeys(key, midKey) if (keyComparison < 0) { right = mid } else if (keyComparison > 0) { left = mid + 1 } else { return mid } } return left } // With comparator: sort by value first, then key as tie-breaker while (left < right) { const mid = Math.floor((left + right) / 2) const midKey = keys[mid]! const midValue = this.map.get(midKey)! const valueComparison = this.comparator(value, midValue) if (valueComparison < 0) { right = mid } else if (valueComparison > 0) { left = mid + 1 } else { // Values are equal, use key as tie-breaker for deterministic ordering const keyComparison = compareKeys(key, midKey) if (keyComparison < 0) { right = mid } else if (keyComparison > 0) { left = mid + 1 } else { // Same key (shouldn't happen during insert, but handle for lookups) return mid } } } return left } /** * Restore the ordered view after a synchronous batch of deferred writes. * Dirty keys leave their old slots by Set membership, so a deleted value or * one a sync source mutated in place cannot strand a slot. The dirty keys * still present are sorted and merged into the rest in one pass: * O(n + k log n) for k dirty keys. The merge rewrites sortedKeys in place, * so an open key iterator reads the restored order. */ restoreOrder(): void { if (this.dirtyKeys.size === 0) return const keys = this.sortedKeys const kept = keys.filter((key) => !this.dirtyKeys.has(key)) const placed = [...this.dirtyKeys] .filter((key) => this.map.has(key)) .sort( (left, right) => (this.comparator?.(this.map.get(left)!, this.map.get(right)!) ?? 0) || compareKeys(left, right), ) keys.length = 0 let next = 0 for (const key of placed) { const index = this.indexOf(key, this.map.get(key)!, kept) while (next < index) keys.push(kept[next++]!) keys.push(key) } while (next < kept.length) keys.push(kept[next++]!) this.dirtyKeys.clear() } /** Removes a key's slot while its placed value is still mapped. */ private unplace(key: TKey): void { // Binary search misses a value that a sync source mutated in place after // it was placed, so fall back to a scan. Slots compare as Map keys do, so // a NaN key matches its own slot. const sameKey = (candidate: TKey) => candidate === key || (candidate !== candidate && key !== key) let index = this.indexOf(key, this.map.get(key)!) if (!sameKey(this.sortedKeys[index]!)) { index = this.sortedKeys.findIndex(sameKey) } this.sortedKeys.splice(index, 1) } /** * Sets a key-value pair in the map and maintains sort order * * @param key - The key to set * @param value - The value to associate with the key * @param deferOrder - Defer ordering until restoreOrder or the next ordered read * @returns This SortedMap instance for chaining */ set(key: TKey, value: TValue, deferOrder = false): this { // Key order cannot change when an existing key gets a new value. if (!this.comparator && this.map.has(key)) { this.map.set(key, value) return this } if (deferOrder) { this.map.set(key, value) this.dirtyKeys.add(key) return this } this.restoreOrder() // The old slot leaves before the search, since an in-place mutation can // misorder it. if (this.map.has(key)) this.unplace(key) this.sortedKeys.splice(this.indexOf(key, value), 0, key) this.map.set(key, value) return this } /** * Gets a value by its key * * @param key - The key to look up * @returns The value associated with the key, or undefined if not found */ get(key: TKey): TValue | undefined { return this.map.get(key) } /** * Removes a key-value pair from the map * * @param key - The key to remove * @param deferOrder - Defer ordering until restoreOrder or the next ordered read * @returns True if the key was found and removed, false otherwise */ delete(key: TKey, deferOrder = false): boolean { if (deferOrder) { const deleted = this.map.delete(key) if (deleted) this.dirtyKeys.add(key) return deleted } this.restoreOrder() if (this.map.has(key)) this.unplace(key) return this.map.delete(key) } /** * Checks if a key exists in the map * * @param key - The key to check * @returns True if the key exists, false otherwise */ has(key: TKey): boolean { return this.map.has(key) } /** * Removes all key-value pairs from the map */ clear(): void { this.map.clear() this.sortedKeys = [] this.dirtyKeys.clear() } /** * Gets the number of key-value pairs in the map */ get size(): number { return this.map.size } /** * Default iterator that returns entries in sorted order * * @returns An iterator for the map's entries */ *[Symbol.iterator](): IterableIterator<[TKey, TValue]> { for (const key of this.keys()) yield [key, this.map.get(key)!] } /** * Returns an iterator for the map's entries in sorted order * * @returns An iterator for the map's entries */ entries(): IterableIterator<[TKey, TValue]> { return this[Symbol.iterator]() } /** * Returns an iterator for the map's keys in sorted order * * @returns An iterator for the map's keys */ keys(): IterableIterator<TKey> { this.restoreOrder() return this.sortedKeys[Symbol.iterator]() } /** * Returns an iterator for the map's values in sorted order * * @returns An iterator for the map's values */ *values(): IterableIterator<TValue> { for (const key of this.keys()) yield this.map.get(key)! } }