@tanstack/db
Version:
A reactive client store for building super fast apps on sync
362 lines (361 loc) • 10.5 kB
JavaScript
import { defaultComparator, normalizeValue } from "../utils/comparison.js";
import { findInsertPositionInArray, deleteInSortedArray } 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;
this.compareFn = options?.compareFn ?? defaultComparator;
if (options?.compareOptions) {
this.compareOptions = options.compareOptions;
}
}
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);
if (this.valueMap.has(normalizedValue)) {
this.valueMap.get(normalizedValue).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);
}
this.indexedKeys.add(key);
this.updateTimestamp();
}
/**
* 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);
this.updateTimestamp();
return;
}
const normalizedValue = normalizeValue(indexedValue);
if (this.valueMap.has(normalizedValue)) {
const keySet = this.valueMap.get(normalizedValue);
keySet.delete(key);
if (keySet.size === 0) {
this.valueMap.delete(normalizedValue);
deleteInSortedArray(this.sortedValues, normalizedValue, this.compareFn);
}
}
this.indexedKeys.delete(key);
this.updateTimestamp();
}
/**
* Updates a value in the index
*/
update(key, oldItem, newItem) {
this.remove(key, oldItem);
this.add(key, newItem);
}
/**
* 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.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);
this.updateTimestamp();
}
/**
* Clears all data from the index
*/
clear() {
this.valueMap.clear();
this.sortedValues = [];
this.indexedKeys.clear();
this.updateTimestamp();
}
/**
* Performs a lookup operation
*/
lookup(operation, value) {
const startTime = performance.now();
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`);
}
this.trackLookup(startTime);
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);
let startIdx = 0;
if (normalizedFrom !== void 0) {
startIdx = findInsertPositionInArray(
this.sortedValues,
normalizedFrom,
this.compareFn
);
if (!fromInclusive && startIdx < this.sortedValues.length && this.compareFn(this.sortedValues[startIdx], normalizedFrom) === 0) {
startIdx++;
}
}
let endIdx = this.sortedValues.length;
if (normalizedTo !== void 0) {
endIdx = findInsertPositionInArray(
this.sortedValues,
normalizedTo,
this.compareFn
);
if (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;
}
/**
* Performs a reversed range query
*/
rangeQueryReversed(options = {}) {
const { from, to, fromInclusive = true, toInclusive = true } = options;
const swappedFrom = to ?? (this.sortedValues.length > 0 ? this.sortedValues[this.sortedValues.length - 1] : void 0);
const swappedTo = from ?? (this.sortedValues.length > 0 ? this.sortedValues[0] : void 0);
return this.rangeQuery({
from: swappedFrom,
to: swappedTo,
fromInclusive: toInclusive,
toInclusive: fromInclusive
});
}
/**
* Returns the next n items in sorted order
*/
take(n, from, filterFn) {
const result = [];
let startIdx = 0;
if (from !== void 0) {
const normalizedFrom = normalizeValue(from);
startIdx = findInsertPositionInArray(
this.sortedValues,
normalizedFrom,
this.compareFn
);
while (startIdx < this.sortedValues.length && this.compareFn(this.sortedValues[startIdx], normalizedFrom) <= 0) {
startIdx++;
}
}
for (let i = startIdx; i < this.sortedValues.length && result.length < n; i++) {
const keys = this.valueMap.get(this.sortedValues[i]);
if (keys) {
for (const key of keys) {
if (result.length >= n) break;
if (!filterFn || filterFn(key)) {
result.push(key);
}
}
}
}
return result;
}
/**
* Returns the next n items in reverse sorted order
*/
takeReversed(n, from, filterFn) {
const result = [];
let startIdx = this.sortedValues.length - 1;
if (from !== void 0) {
const normalizedFrom = normalizeValue(from);
startIdx = findInsertPositionInArray(
this.sortedValues,
normalizedFrom,
this.compareFn
) - 1;
while (startIdx >= 0 && this.compareFn(this.sortedValues[startIdx], normalizedFrom) >= 0) {
startIdx--;
}
}
for (let i = startIdx; i >= 0 && result.length < n; i--) {
const keys = this.valueMap.get(this.sortedValues[i]);
if (keys) {
for (const key of keys) {
if (result.length >= n) break;
if (!filterFn || filterFn(key)) {
result.push(key);
}
}
}
}
return result;
}
/**
* Returns the first n items in sorted order (from the start)
*/
takeFromStart(n, filterFn) {
const result = [];
for (let i = 0; i < this.sortedValues.length && result.length < n; i++) {
const keys = this.valueMap.get(this.sortedValues[i]);
if (keys) {
for (const key of keys) {
if (result.length >= n) break;
if (!filterFn || filterFn(key)) {
result.push(key);
}
}
}
}
return result;
}
/**
* Returns the first n items in reverse sorted order (from the end)
*/
takeReversedFromEnd(n, filterFn) {
const result = [];
for (let i = this.sortedValues.length - 1; i >= 0 && result.length < n; i--) {
const keys = this.valueMap.get(this.sortedValues[i]);
if (keys) {
for (const key of keys) {
if (result.length >= n) break;
if (!filterFn || filterFn(key)) {
result.push(key);
}
}
}
}
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;
}
// Getter methods for testing/compatibility
get indexedKeysSet() {
return this.indexedKeys;
}
get orderedEntriesArray() {
return this.sortedValues.map((value) => [
value,
this.valueMap.get(value) ?? /* @__PURE__ */ new Set()
]);
}
get orderedEntriesArrayReversed() {
const result = [];
for (let i = this.sortedValues.length - 1; i >= 0; i--) {
const value = this.sortedValues[i];
result.push([value, this.valueMap.get(value) ?? /* @__PURE__ */ new Set()]);
}
return result;
}
get valueMapData() {
return this.valueMap;
}
}
export {
BasicIndex
};
//# sourceMappingURL=basic-index.js.map