@tanstack/db
Version:
A reactive client store for building super fast apps on sync
335 lines (334 loc) • 10.2 kB
JavaScript
import { compareKeys } from "@tanstack/db-ivm";
import { defaultComparator, makeComparator, normalizeValue, areSameValueZeroEqual } from "../utils/comparison.js";
import { findInsertPositionInArray, compareKeysReversed } from "../utils/array-utils.js";
import { BaseIndex } from "./base-index.js";
class BasicIndex extends BaseIndex {
constructor(id, expression, name, options) {
super(id, expression, name, options);
this.supportedOperations = /* @__PURE__ */ new Set([
`eq`,
`gt`,
`gte`,
`lt`,
`lte`,
`in`
]);
this.valueMap = /* @__PURE__ */ new Map();
this.sortedValues = [];
this.indexedKeys = /* @__PURE__ */ new Set();
this.compareFn = defaultComparator;
if (options?.compareOptions) {
this.compareOptions = options.compareOptions;
}
this.compareFn = options?.compareFn ?? makeComparator(this.compareOptions);
this.hasCustomComparator = options?.compareFn != null;
}
initialize(_options) {
}
/**
* Adds a value to the index
*/
add(key, item) {
let indexedValue;
try {
indexedValue = this.evaluateIndexExpression(item);
} catch (error) {
throw new Error(
`Failed to evaluate index expression for key ${key}: ${error}`,
{ cause: error }
);
}
const normalizedValue = normalizeValue(indexedValue);
this.addToBucket(key, normalizedValue);
this.addRangeValue(indexedValue);
this.indexedKeys.add(key);
}
addToBucket(key, normalizedValue) {
const keySet = this.valueMap.get(normalizedValue);
if (keySet) {
keySet.add(key);
} else {
this.valueMap.set(normalizedValue, /* @__PURE__ */ new Set([key]));
const insertIdx = findInsertPositionInArray(
this.sortedValues,
normalizedValue,
this.compareFn
);
this.sortedValues.splice(insertIdx, 0, normalizedValue);
}
}
/**
* Removes a value from the index
*/
remove(key, item) {
let indexedValue;
try {
indexedValue = this.evaluateIndexExpression(item);
} catch (error) {
console.warn(
`Failed to evaluate index expression for key ${key} during removal:`,
error
);
this.indexedKeys.delete(key);
return;
}
const normalizedValue = normalizeValue(indexedValue);
this.removeFromBucket(key, normalizedValue);
this.removeRangeValue(indexedValue);
this.indexedKeys.delete(key);
}
removeFromBucket(key, normalizedValue) {
const keySet = this.valueMap.get(normalizedValue);
if (keySet) {
keySet.delete(key);
if (keySet.size === 0) {
this.valueMap.delete(normalizedValue);
let sortedIndex = findInsertPositionInArray(
this.sortedValues,
normalizedValue,
this.compareFn
);
while (sortedIndex < this.sortedValues.length && this.compareFn(this.sortedValues[sortedIndex], normalizedValue) === 0) {
if (areSameValueZeroEqual(
this.sortedValues[sortedIndex],
normalizedValue
)) {
this.sortedValues.splice(sortedIndex, 1);
break;
}
sortedIndex++;
}
}
}
}
/**
* Updates a value in the index
*/
update(key, oldItem, newItem) {
let oldIndexedValue;
let newIndexedValue;
try {
oldIndexedValue = this.evaluateIndexExpression(oldItem);
newIndexedValue = this.evaluateIndexExpression(newItem);
} catch {
this.remove(key, oldItem);
this.add(key, newItem);
return;
}
const oldValue = normalizeValue(oldIndexedValue);
const newValue = normalizeValue(newIndexedValue);
if (areSameValueZeroEqual(oldValue, newValue) && this.valueMap.get(newValue)?.has(key) && this.indexedKeys.has(key)) {
this.removeRangeValue(oldIndexedValue);
this.addRangeValue(newIndexedValue);
return;
}
this.removeFromBucket(key, oldValue);
this.removeRangeValue(oldIndexedValue);
this.addToBucket(key, newValue);
this.addRangeValue(newIndexedValue);
this.indexedKeys.add(key);
}
/**
* Builds the index from a collection of entries
*/
build(entries) {
this.clear();
const entriesArray = [];
for (const [key, item] of entries) {
let indexedValue;
try {
indexedValue = this.evaluateIndexExpression(item);
} catch (error) {
throw new Error(
`Failed to evaluate index expression for key ${key}: ${error}`,
{ cause: error }
);
}
entriesArray.push({ key, value: normalizeValue(indexedValue) });
this.addRangeValue(indexedValue);
this.indexedKeys.add(key);
}
for (const { key, value } of entriesArray) {
if (this.valueMap.has(value)) {
this.valueMap.get(value).add(key);
} else {
this.valueMap.set(value, /* @__PURE__ */ new Set([key]));
}
}
this.sortedValues = Array.from(this.valueMap.keys()).sort(this.compareFn);
}
/**
* Clears all data from the index
*/
clear() {
this.valueMap.clear();
this.sortedValues = [];
this.indexedKeys.clear();
this.clearRangeValues();
}
/**
* Performs a lookup operation
*/
lookup(operation, value) {
let result;
switch (operation) {
case `eq`:
result = this.equalityLookup(value);
break;
case `gt`:
result = this.rangeQuery({ from: value, fromInclusive: false });
break;
case `gte`:
result = this.rangeQuery({ from: value, fromInclusive: true });
break;
case `lt`:
result = this.rangeQuery({ to: value, toInclusive: false });
break;
case `lte`:
result = this.rangeQuery({ to: value, toInclusive: true });
break;
case `in`:
result = this.inArrayLookup(value);
break;
default:
throw new Error(`Operation ${operation} not supported by BasicIndex`);
}
return result;
}
/**
* Gets the number of indexed keys
*/
get keyCount() {
return this.indexedKeys.size;
}
/**
* Performs an equality lookup - O(1)
*/
equalityLookup(value) {
const normalizedValue = normalizeValue(value);
return this.valueMap.get(normalizedValue) ?? /* @__PURE__ */ new Set();
}
/**
* Performs a range query using binary search - O(log n + m)
*/
rangeQuery(options = {}) {
const { from, to, fromInclusive = true, toInclusive = true } = options;
const result = /* @__PURE__ */ new Set();
if (this.sortedValues.length === 0) {
return result;
}
const normalizedFrom = normalizeValue(from);
const normalizedTo = normalizeValue(to);
const hasFrom = `from` in options;
const hasTo = `to` in options;
let startIdx = 0;
if (hasFrom) {
startIdx = findInsertPositionInArray(
this.sortedValues,
normalizedFrom,
this.compareFn
);
while (!fromInclusive && startIdx < this.sortedValues.length && this.compareFn(this.sortedValues[startIdx], normalizedFrom) === 0) {
startIdx++;
}
}
let endIdx = this.sortedValues.length;
if (hasTo) {
endIdx = findInsertPositionInArray(
this.sortedValues,
normalizedTo,
this.compareFn
);
while (toInclusive && endIdx < this.sortedValues.length && this.compareFn(this.sortedValues[endIdx], normalizedTo) === 0) {
endIdx++;
}
}
for (let i = startIdx; i < endIdx; i++) {
const keys = this.valueMap.get(this.sortedValues[i]);
if (keys) {
keys.forEach((key) => result.add(key));
}
}
return result;
}
/**
* Returns the next n items in sorted order
*/
take(n, from, filterFn) {
const normalizedFrom = normalizeValue(from);
let startIdx = findInsertPositionInArray(
this.sortedValues,
normalizedFrom,
this.compareFn
);
while (startIdx < this.sortedValues.length && this.compareFn(this.sortedValues[startIdx], normalizedFrom) <= 0) {
startIdx++;
}
return this.takeFromIndex(n, startIdx, 1, filterFn);
}
/**
* Returns the next n items in reverse sorted order
*/
takeReversed(n, from, filterFn) {
const normalizedFrom = normalizeValue(from);
let startIdx = findInsertPositionInArray(
this.sortedValues,
normalizedFrom,
this.compareFn
) - 1;
while (startIdx >= 0 && this.compareFn(this.sortedValues[startIdx], normalizedFrom) >= 0) {
startIdx--;
}
return this.takeFromIndex(n, startIdx, -1, filterFn);
}
/**
* Returns the first n items in sorted order (from the start)
*/
takeFromStart(n, filterFn) {
return this.takeFromIndex(n, 0, 1, filterFn);
}
/**
* Returns the first n items in reverse sorted order (from the end)
*/
takeReversedFromEnd(n, filterFn) {
return this.takeFromIndex(n, this.sortedValues.length - 1, -1, filterFn);
}
takeFromIndex(n, startIndex, step, filterFn) {
const result = [];
let index = startIndex;
while (index >= 0 && index < this.sortedValues.length && result.length < n) {
const groupValue = this.sortedValues[index];
const groupKeys = [];
do {
for (const key of this.valueMap.get(this.sortedValues[index]) ?? []) {
groupKeys.push(key);
}
index += step;
} while (index >= 0 && index < this.sortedValues.length && this.compareFn(this.sortedValues[index], groupValue) === 0);
groupKeys.sort(step === 1 ? compareKeys : compareKeysReversed);
for (const key of groupKeys) {
if (filterFn?.(key) ?? true) result.push(key);
if (result.length >= n) break;
}
}
return result;
}
/**
* Performs an IN array lookup - O(k) where k is values.length
*/
inArrayLookup(values) {
const result = /* @__PURE__ */ new Set();
for (const value of values) {
const normalizedValue = normalizeValue(value);
const keys = this.valueMap.get(normalizedValue);
if (keys) {
keys.forEach((key) => result.add(key));
}
}
return result;
}
}
export {
BasicIndex
};
//# sourceMappingURL=basic-index.js.map