UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

250 lines (229 loc) • 7.67 kB
import { compareKeys } from '@tanstack/db-ivm' import { compareKeysReversed } from '../utils/array-utils.js' import { BTree } from '../utils/btree.js' import { areSameValueZeroEqual, normalizeValue } from '../utils/comparison.js' import { codedMessage, devBuild } from '../error-message.js' import { BaseIndex, builtInIndexResolverNames, warnRemovalFailure, } from './base-index.js' import type { IndexComparisonOptions, IndexOperation } from './base-index.js' export type BTreeIndexOptions = IndexComparisonOptions export interface RangeQueryOptions { from?: any to?: any fromInclusive?: boolean toInclusive?: boolean } type SortKey = { value: any } type Group<TKey> = SortKey & { keys: Map<TKey, any> buckets?: Map<unknown, Set<TKey>> representativeKey: TKey nextRepresentative?: IterableIterator<[TKey, any]> } export class BTreeIndex< TKey extends string | number = string | number, > extends BaseIndex<TKey> { private orderedEntries = new BTree<SortKey, Group<TKey>>((a, b) => this.compareFn(a.value, b.value), ) private valueMap = new Map<any, Group<TKey>>() private indexedKeys = new Set<TKey>() add(key: TKey, item: any): void { const value = this.evaluateAddedValue(key, item) const exact = normalizeValue(value) this.addToBucket(key, value, exact) this.addRangeValue(value) this.indexedKeys.add(key) } private addToBucket(key: TKey, value: any, exact: unknown): void { const group = this.valueMap.get(exact) if (group) { group.keys.set(key, value) group.buckets?.get(exact)!.add(key) return } const ordered = this.orderedEntries.get({ value }) if (ordered) { ordered.buckets ??= new Map([ [normalizeValue(ordered.value), new Set(ordered.keys.keys())], ]) ordered.buckets.set(exact, new Set([key])) ordered.keys.set(key, value) this.valueMap.set(exact, ordered) } else { const created = { value, keys: new Map([[key, value]]), representativeKey: key, } // The tree compares before the exact lookup map changes. this.orderedEntries.set(created, created) this.valueMap.set(exact, created) } } remove(key: TKey, item: any): void { let value: any try { value = this.evaluateIndexExpression(item) } catch (error) { warnRemovalFailure(key, error) return } const exact = normalizeValue(value) this.removeFromBucket(key, exact) this.removeRangeValue(value) this.indexedKeys.delete(key) } private removeFromBucket(key: TKey, exact: unknown): void { const group = this.valueMap.get(exact) if (!group?.keys.has(key)) return const bucket = group.buckets?.get(exact) if (group.buckets && !bucket?.has(key)) return if (group.keys.size === 1) { this.orderedEntries.delete(group) } bucket?.delete(key) group.keys.delete(key) if (bucket?.size === 0 || group.keys.size === 0) { this.valueMap.delete(exact) group.buckets?.delete(exact) } if (group.keys.size && group.representativeKey === key) { // Advancing one iterator skips each removed Map slot at most once. const [nextKey, nextValue] = (group.nextRepresentative ??= group.keys.entries()).next().value as [TKey, any] group.representativeKey = nextKey group.value = nextValue } } update(key: TKey, oldItem: any, newItem: any): void { let oldValue: any let newValue: any try { oldValue = this.evaluateIndexExpression(oldItem) newValue = this.evaluateIndexExpression(newItem) } catch { this.remove(key, oldItem) this.add(key, newItem) return } const oldExact = normalizeValue(oldValue) const newExact = normalizeValue(newValue) const group = this.valueMap.get(newExact) const containsKey = group?.buckets ? group.buckets.get(newExact)?.has(key) : group?.keys.has(key) if (areSameValueZeroEqual(oldExact, newExact) && containsKey) { group!.keys.set(key, newValue) if (group!.representativeKey === key) group!.value = newValue this.removeRangeValue(oldValue) this.addRangeValue(newValue) return } this.removeFromBucket(key, oldExact) this.removeRangeValue(oldValue) this.addToBucket(key, newValue, newExact) this.addRangeValue(newValue) this.indexedKeys.add(key) } build(entries: Iterable<[TKey, any]>): void { this.clear() for (const [key, item] of entries) this.add(key, item) } clear(): void { this.orderedEntries.clear() this.valueMap.clear() this.indexedKeys.clear() this.clearRangeValues() } lookup(operation: IndexOperation, value: any): Set<TKey> { const result = this.lookupSupportedOperation(operation, value) if (result !== undefined) return result throw new Error( devBuild() && process.env.NODE_ENV !== `production` ? `Operation ${operation} not supported by BTreeIndex` : codedMessage(101, { operation }), ) } get keyCount(): number { return this.indexedKeys.size } equalityLookup(value: any): Set<TKey> { return new Set(this.exactBucketKeys(value)) } protected exactBucketKeys(value: any): Iterable<TKey> | undefined { const exact = normalizeValue(value) const group = this.valueMap.get(exact) return group?.buckets ? group.buckets.get(exact) : group?.keys.keys() } rangeQuery(options: RangeQueryOptions = {}): Set<TKey> { const result = new Set<TKey>() if (this.orderedEntries.size === 0) return result const { from, to, fromInclusive = true, toInclusive = true } = options const low = 'from' in options ? { value: from } : this.orderedEntries.minKey()! const high = 'to' in options ? { value: to } : this.orderedEntries.maxKey()! this.orderedEntries.forRange( low, high, !('to' in options) || toInclusive, (sortKey, group) => { if ( 'from' in options && !fromInclusive && this.compareFn(sortKey.value, from) === 0 ) return for (const key of group.keys.keys()) result.add(key) }, ) return result } private takeInternal( n: number, from: SortKey | undefined, filterFn: ((key: TKey) => boolean) | undefined, reversed: boolean, ): Array<TKey> { const result: Array<TKey> = [] let pair = reversed ? this.orderedEntries.nextLowerPair(from) : this.orderedEntries.nextHigherPair(from) while (pair && result.length < n) { for (const key of [...pair[1].keys.keys()].sort( reversed ? compareKeysReversed : compareKeys, )) { if (filterFn?.(key) ?? true) result.push(key) if (result.length >= n) break } if (result.length >= n) break pair = reversed ? this.orderedEntries.nextLowerPair(pair[0]) : this.orderedEntries.nextHigherPair(pair[0]) } return result } take(n: number, from: any, filterFn?: (key: TKey) => boolean): Array<TKey> { return this.takeInternal(n, { value: from }, filterFn, false) } takeFromStart(n: number, filterFn?: (key: TKey) => boolean): Array<TKey> { return this.takeInternal(n, undefined, filterFn, false) } takeReversed( n: number, from: any, filterFn?: (key: TKey) => boolean, ): Array<TKey> { return this.takeInternal(n, { value: from }, filterFn, true) } takeReversedFromEnd( n: number, filterFn?: (key: TKey) => boolean, ): Array<TKey> { return this.takeInternal(n, undefined, filterFn, true) } } builtInIndexResolverNames.set(BTreeIndex, 'BTreeIndex')