@entropy-tamer/reynard-algorithms
Version:
Algorithm primitives and data structures for Reynard applications
1,796 lines • 927 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
class UnionFindSetOperations {
constructor(nodes, stats) {
this.nodes = nodes;
this.stats = stats;
}
/**
* Find the root of a node with path compression
*/
find(x) {
if (this.nodes[x].parent !== x) {
this.nodes[x].parent = this.find(this.nodes[x].parent);
this.stats.compressionCount++;
}
return this.nodes[x].parent;
}
/**
* Get the size of the set containing x
*/
getSetSize(x) {
const root = this.find(x);
return this.nodes.filter((node) => this.find(this.nodes.indexOf(node)) === root).length;
}
/**
* Get all nodes in the same set as x
*/
getSetMembers(x) {
const root = this.find(x);
return this.nodes.map((_, index) => index).filter((index) => this.find(index) === root);
}
/**
* Get statistics about the Union-Find structure
*/
getStats() {
const roots = /* @__PURE__ */ new Set();
let maxRank = 0;
let totalRank = 0;
for (let i = 0; i < this.nodes.length; i++) {
const root = this.find(i);
roots.add(root);
maxRank = Math.max(maxRank, this.nodes[root].rank);
totalRank += this.nodes[root].rank;
}
return {
totalNodes: this.nodes.length,
totalSets: roots.size,
maxRank,
averageRank: roots.size > 0 ? totalRank / roots.size : 0,
compressionCount: this.stats.compressionCount,
unionCount: this.stats.unionCount
};
}
}
class UnionFind {
nodes;
stats = {
compressionCount: 0,
unionCount: 0
};
setOps;
constructor(size) {
this.nodes = Array.from({ length: size }, (_, i) => ({
parent: i,
rank: 0
}));
this.setOps = new UnionFindSetOperations(this.nodes, this.stats);
}
find(x) {
return this.setOps.find(x);
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) return false;
this.stats.unionCount++;
if (this.nodes[rootX].rank < this.nodes[rootY].rank) {
this.nodes[rootX].parent = rootY;
} else if (this.nodes[rootX].rank > this.nodes[rootY].rank) {
this.nodes[rootY].parent = rootX;
} else {
this.nodes[rootY].parent = rootX;
this.nodes[rootX].rank++;
}
return true;
}
connected(x, y) {
return this.find(x) === this.find(y);
}
getSetSize(x) {
return this.setOps.getSetSize(x);
}
getSetMembers(x) {
return this.setOps.getSetMembers(x);
}
getStats() {
return this.setOps.getStats();
}
reset() {
this.nodes = this.nodes.map((_, i) => ({
parent: i,
rank: 0
}));
this.stats.compressionCount = 0;
this.stats.unionCount = 0;
this.setOps = new UnionFindSetOperations(this.nodes, this.stats);
}
clone() {
const clone = new UnionFind(this.nodes.length);
clone.nodes = this.nodes.map((node) => ({ ...node }));
clone.stats = { ...this.stats };
clone.setOps = new UnionFindSetOperations(clone.nodes, clone.stats);
return clone;
}
}
function estimateMemoryUsage(cellCount, objectCount, objectToCellCount) {
let usage = 0;
usage += cellCount * 50;
usage += objectToCellCount * 30;
usage += objectCount * 100;
return usage;
}
class SpatialHash {
cells = /* @__PURE__ */ new Map();
objectToCells = /* @__PURE__ */ new Map();
config;
stats = {
queryCount: 0,
insertCount: 0,
removeCount: 0
};
lastCleanup = Date.now();
constructor(config = {}) {
this.config = {
cellSize: 100,
maxObjectsPerCell: 50,
enableAutoResize: true,
resizeThreshold: 0.8,
cleanupInterval: 6e4,
// 1 minute
...config
};
}
/**
* Insert an object into the spatial hash
*/
insert(object) {
const cellKeys = this.getObjectCells(object);
for (const cellKey of Array.from(cellKeys)) {
if (!this.cells.has(cellKey)) {
this.cells.set(cellKey, []);
}
this.cells.get(cellKey).push(object);
}
this.objectToCells.set(object.id, cellKeys);
this.stats.insertCount++;
this.checkAutoResize();
this.checkCleanup();
}
/**
* Remove an object from the spatial hash
*/
remove(objectId) {
const cellKeys = this.objectToCells.get(objectId);
if (!cellKeys) return false;
for (const cellKey of Array.from(cellKeys)) {
const cell = this.cells.get(cellKey);
if (cell) {
const index = cell.findIndex((obj) => obj.id === objectId);
if (index !== -1) {
cell.splice(index, 1);
if (cell.length === 0) {
this.cells.delete(cellKey);
}
}
}
}
this.objectToCells.delete(objectId);
this.stats.removeCount++;
return true;
}
/**
* Update an object's position in the spatial hash
*/
update(object) {
if (this.remove(object.id)) {
this.insert(object);
return true;
}
return false;
}
/**
* Query for objects in a rectangular area
*/
queryRect(x, y, width, height) {
const cellKeys = this.getRectCells(x, y, width, height);
const results = /* @__PURE__ */ new Map();
for (const cellKey of Array.from(cellKeys)) {
const cell = this.cells.get(cellKey);
if (cell) {
for (const obj of cell) {
if (this.isObjectInRect(obj, x, y, width, height) && obj.data !== void 0) {
results.set(obj.id, obj);
}
}
}
}
this.stats.queryCount++;
return Array.from(results.values());
}
/**
* Query for objects within a radius of a point
*/
queryRadius(centerX, centerY, radius) {
const cellKeys = this.getRadiusCells(centerX, centerY, radius);
const results = [];
for (const cellKey of Array.from(cellKeys)) {
const cell = this.cells.get(cellKey);
if (cell) {
for (const obj of cell) {
const distance2 = this.getDistance(centerX, centerY, obj.x, obj.y);
if (distance2 <= radius) {
results.push({
object: obj,
distance: distance2,
cellKey
});
}
}
}
}
this.stats.queryCount++;
return results.sort((a, b) => a.distance - b.distance);
}
/**
* Find the nearest object to a point
*/
findNearest(x, y, maxDistance) {
const radius = maxDistance || this.config.cellSize * 2;
const results = this.queryRadius(x, y, radius);
if (results.length === 0) {
const expandedResults = this.queryRadius(x, y, radius * 2);
return expandedResults[0] || null;
}
return results[0];
}
/**
* Get all objects in the spatial hash
*/
getAllObjects() {
const objects = /* @__PURE__ */ new Map();
for (const cell of Array.from(this.cells.values())) {
for (const obj of cell) {
if (obj.data !== void 0) {
objects.set(obj.id, obj);
}
}
}
return Array.from(objects.values());
}
/**
* Clear all objects from the spatial hash
*/
clear() {
this.cells.clear();
this.objectToCells.clear();
this.stats.queryCount = 0;
this.stats.insertCount = 0;
this.stats.removeCount = 0;
}
/**
* Get statistics about the spatial hash
*/
getStats() {
let totalObjects = 0;
let maxObjectsInCell = 0;
let emptyCells = 0;
for (const cell of Array.from(this.cells.values())) {
totalObjects += cell.length;
maxObjectsInCell = Math.max(maxObjectsInCell, cell.length);
}
emptyCells = this.cells.size === 0 ? 0 : Array.from(this.cells.values()).filter((cell) => cell.length === 0).length;
return {
totalCells: this.cells.size,
totalObjects,
averageObjectsPerCell: this.cells.size > 0 ? totalObjects / this.cells.size : 0,
maxObjectsInCell,
emptyCells,
memoryUsage: estimateMemoryUsage(this.cells.size, totalObjects, this.objectToCells.size),
queryCount: this.stats.queryCount,
insertCount: this.stats.insertCount,
removeCount: this.stats.removeCount
};
}
/**
* Resize the spatial hash with a new cell size
*/
resize(newCellSize) {
if (newCellSize === this.config.cellSize) return;
const oldCells = this.cells;
const oldObjectToCells = this.objectToCells;
this.config.cellSize = newCellSize;
this.cells = /* @__PURE__ */ new Map();
this.objectToCells = /* @__PURE__ */ new Map();
for (const [objectId, cellKeys] of Array.from(oldObjectToCells.entries())) {
const firstCellKey = Array.from(cellKeys)[0];
const object = oldCells.get(firstCellKey)?.find((obj) => obj.id === objectId);
if (object && object.data !== void 0) {
this.insert(object);
}
}
}
getObjectCells(object) {
const width = object.width || 0;
const height = object.height || 0;
return this.getRectCells(object.x, object.y, width, height);
}
getRectCells(x, y, width, height) {
const minCellX = Math.floor(x / this.config.cellSize);
const maxCellX = Math.floor((x + width) / this.config.cellSize);
const minCellY = Math.floor(y / this.config.cellSize);
const maxCellY = Math.floor((y + height) / this.config.cellSize);
const cellKeys = /* @__PURE__ */ new Set();
for (let cellX = minCellX; cellX <= maxCellX; cellX++) {
for (let cellY = minCellY; cellY <= maxCellY; cellY++) {
cellKeys.add(`${cellX},${cellY}`);
}
}
return cellKeys;
}
getRadiusCells(centerX, centerY, radius) {
const minCellX = Math.floor((centerX - radius) / this.config.cellSize);
const maxCellX = Math.floor((centerX + radius) / this.config.cellSize);
const minCellY = Math.floor((centerY - radius) / this.config.cellSize);
const maxCellY = Math.floor((centerY + radius) / this.config.cellSize);
const cellKeys = /* @__PURE__ */ new Set();
for (let cellX = minCellX; cellX <= maxCellX; cellX++) {
for (let cellY = minCellY; cellY <= maxCellY; cellY++) {
cellKeys.add(`${cellX},${cellY}`);
}
}
return cellKeys;
}
isObjectInRect(object, rectX, rectY, rectWidth, rectHeight) {
const objWidth = object.width || 0;
const objHeight = object.height || 0;
return object.x < rectX + rectWidth && object.x + objWidth > rectX && object.y < rectY + rectHeight && object.y + objHeight > rectY;
}
getDistance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
checkAutoResize() {
if (!this.config.enableAutoResize) return;
const stats = this.getStats();
const loadFactor = stats.averageObjectsPerCell / this.config.maxObjectsPerCell;
if (loadFactor > this.config.resizeThreshold) {
const newCellSize = this.config.cellSize * 1.5;
this.resize(newCellSize);
}
}
checkCleanup() {
const now = Date.now();
if (now - this.lastCleanup > this.config.cleanupInterval) {
this.cleanup();
this.lastCleanup = now;
}
}
cleanup() {
for (const [cellKey, cell] of Array.from(this.cells.entries())) {
if (cell.length === 0) {
this.cells.delete(cellKey);
}
}
}
}
class PriorityQueue {
heap = [];
config;
stats;
comparator;
eventHandler;
/**
* Creates a new Priority Queue instance
*
* @param options - Configuration options for the priority queue
*/
constructor(options = {}) {
this.config = {
initialCapacity: options.initialCapacity ?? 16,
maxHeap: options.maxHeap ?? false,
growthFactor: options.growthFactor ?? 2,
maxCapacity: options.maxCapacity ?? Number.MAX_SAFE_INTEGER
};
this.comparator = options.comparator;
this.eventHandler = options.onEvent;
this.stats = {
insertCount: 0,
extractCount: 0,
heapifyCount: 0,
size: 0,
maxSize: 0,
averageInsertTime: 0,
averageExtractTime: 0
};
this.heap = new Array(this.config.initialCapacity);
this.heap.length = 0;
}
/**
* Inserts an element into the priority queue
*
* Mathematical Process:
* 1. Add element to the end of the heap array
* 2. Bubble up to maintain heap property: while parent has lower priority, swap with parent
* 3. Time complexity: O(log n) where n is the number of elements
*
* @param data - The data to insert
* @param priority - The priority of the data (lower = higher priority for min-heap)
* @returns True if insertion was successful
*/
insert(data, priority) {
const startTime = performance.now();
try {
if (this.heap.length >= this.config.maxCapacity) {
this.emitEvent("insert", data, priority, { error: "Capacity exceeded" });
return false;
}
if (this.heap.length >= this.heap.length) {
this.resize();
}
const newNode = { data, priority };
this.heap.push(newNode);
this.stats.size++;
this.stats.maxSize = Math.max(this.stats.maxSize, this.stats.size);
this.bubbleUp(this.heap.length - 1);
this.stats.insertCount++;
this.updateAverageInsertTime(performance.now() - startTime);
this.emitEvent("insert", data, priority);
return true;
} catch (error) {
this.emitEvent("insert", data, priority, { error: error instanceof Error ? error.message : "Unknown error" });
return false;
}
}
/**
* Extracts and returns the highest priority element
*
* Mathematical Process:
* 1. Remove root element (highest priority)
* 2. Move last element to root position
* 3. Bubble down to maintain heap property: while children have higher priority, swap with highest priority child
* 4. Time complexity: O(log n)
*
* @returns The highest priority element, or undefined if queue is empty
*/
extract() {
const startTime = performance.now();
if (this.isEmpty()) {
return void 0;
}
const root = this.heap[0];
const lastElement = this.heap.pop();
this.stats.size--;
if (!this.isEmpty()) {
this.heap[0] = lastElement;
this.bubbleDown(0);
}
this.stats.extractCount++;
this.updateAverageExtractTime(performance.now() - startTime);
this.emitEvent("extract", root.data, root.priority);
return root.data;
}
/**
* Returns the highest priority element without removing it
*
* Time complexity: O(1)
*
* @returns Peek result with element, priority, and empty status
*/
peek() {
if (this.isEmpty()) {
return {
element: void 0,
priority: 0,
isEmpty: true
};
}
const root = this.heap[0];
return {
element: root.data,
priority: root.priority,
isEmpty: false
};
}
/**
* Returns the current size of the priority queue
*
* @returns Number of elements in the queue
*/
size() {
return this.stats.size;
}
/**
* Checks if the priority queue is empty
*
* @returns True if the queue is empty
*/
isEmpty() {
return this.stats.size === 0;
}
/**
* Clears all elements from the priority queue
*/
clear() {
this.heap.length = 0;
this.stats.size = 0;
this.emitEvent("clear");
}
/**
* Inserts multiple elements in batch
*
* @param elements - Array of {data, priority} objects
* @returns Batch operation result
*/
batchInsert(elements) {
const inserted = [];
const failed = [];
for (const { data, priority } of elements) {
if (this.insert(data, priority)) {
inserted.push(data);
} else {
failed.push(data);
}
}
return {
inserted,
failed,
total: elements.length,
successRate: inserted.length / elements.length
};
}
/**
* Returns performance statistics
*
* @returns Current statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Returns the heap as an array (for debugging)
*
* @returns Copy of the internal heap array
*/
toArray() {
return [...this.heap];
}
/**
* Creates an iterator for the priority queue
*
* @returns Iterator that yields elements in priority order
*/
*[Symbol.iterator]() {
const copy = new PriorityQueue({ ...this.config, comparator: this.comparator });
for (const node of this.heap) {
copy.insert(node.data, node.priority);
}
while (!copy.isEmpty()) {
yield copy.extract();
}
}
/**
* Bubbles up an element to maintain heap property
*
* Mathematical Process:
* - Compare element with parent
* - If heap property is violated, swap with parent
* - Continue until heap property is satisfied or element reaches root
*
* @param index - Index of element to bubble up
*/
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.hasHigherPriority(index, parentIndex)) {
this.swap(index, parentIndex);
index = parentIndex;
} else {
break;
}
}
}
/**
* Bubbles down an element to maintain heap property
*
* Mathematical Process:
* - Compare element with its children
* - If heap property is violated, swap with highest priority child
* - Continue until heap property is satisfied or element reaches leaf
*
* @param index - Index of element to bubble down
*/
bubbleDown(index) {
while (true) {
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
let highestPriorityIndex = index;
if (leftChild < this.heap.length && this.hasHigherPriority(leftChild, highestPriorityIndex)) {
highestPriorityIndex = leftChild;
}
if (rightChild < this.heap.length && this.hasHigherPriority(rightChild, highestPriorityIndex)) {
highestPriorityIndex = rightChild;
}
if (highestPriorityIndex === index) {
break;
}
this.swap(index, highestPriorityIndex);
index = highestPriorityIndex;
}
}
/**
* Checks if element at index1 has higher priority than element at index2
*
* @param index1 - First element index
* @param index2 - Second element index
* @returns True if index1 has higher priority
*/
hasHigherPriority(index1, index2) {
const node1 = this.heap[index1];
const node2 = this.heap[index2];
if (this.comparator) {
return this.comparator(node1.data, node2.data) < 0;
}
if (this.config.maxHeap) {
return node1.priority > node2.priority;
} else {
return node1.priority < node2.priority;
}
}
/**
* Swaps two elements in the heap
*
* @param index1 - First element index
* @param index2 - Second element index
*/
swap(index1, index2) {
[this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
}
/**
* Resizes the heap array when capacity is exceeded
*/
resize() {
const newCapacity = Math.min(this.heap.length * this.config.growthFactor, this.config.maxCapacity);
if (newCapacity > this.heap.length) {
const newHeap = new Array(newCapacity);
for (let i = 0; i < this.heap.length; i++) {
newHeap[i] = this.heap[i];
}
this.heap = newHeap;
this.emitEvent("resize", void 0, void 0, { newCapacity });
}
}
/**
* Updates the average insertion time
*
* @param time - Time taken for insertion
*/
updateAverageInsertTime(time) {
this.stats.averageInsertTime = (this.stats.averageInsertTime * (this.stats.insertCount - 1) + time) / this.stats.insertCount;
}
/**
* Updates the average extraction time
*
* @param time - Time taken for extraction
*/
updateAverageExtractTime(time) {
this.stats.averageExtractTime = (this.stats.averageExtractTime * (this.stats.extractCount - 1) + time) / this.stats.extractCount;
}
/**
* Emits an event if event handler is configured
*
* @param type - Event type
* @param data - Event data
* @param priority - Event priority
* @param metadata - Additional metadata
*/
emitEvent(type, data, priority, metadata) {
if (this.eventHandler) {
const event = {
type,
timestamp: performance.now(),
data,
priority,
metadata
};
this.eventHandler(event);
}
}
}
class LRUCache {
cache = /* @__PURE__ */ new Map();
head = null;
tail = null;
config;
stats;
eventHandler;
onEvict;
onAccess;
onSet;
onDelete;
cleanupTimer;
performanceMetrics;
/**
* Creates a new LRU Cache instance
*
* @param options - Configuration options for the LRU cache
*/
constructor(options) {
this.config = {
maxSize: options.maxSize,
ttl: options.ttl ?? 0,
enableCleanup: options.enableCleanup ?? true,
cleanupInterval: options.cleanupInterval ?? 6e4,
// 1 minute
enableStats: options.enableStats ?? true
};
this.eventHandler = options.onEvent;
this.onEvict = options.onEvict;
this.onAccess = options.onAccess;
this.onSet = options.onSet;
this.onDelete = options.onDelete;
this.stats = {
hits: 0,
misses: 0,
sets: 0,
deletes: 0,
evictions: 0,
size: 0,
maxSize: this.config.maxSize,
hitRate: 0,
averageAccessTime: 0
};
this.performanceMetrics = {
averageGetTime: 0,
averageSetTime: 0,
averageDeleteTime: 0,
estimatedMemoryUsage: 0,
cleanupCount: 0,
totalCleanupTime: 0
};
if (this.config.enableCleanup && this.config.ttl > 0) {
this.startCleanupTimer();
}
}
/**
* Gets a value from the cache
*
* Mathematical Process:
* 1. Look up node in hash map: O(1)
* 2. Check if expired (if TTL enabled)
* 3. Move node to head of doubly-linked list: O(1)
* 4. Update access statistics
*
* @param key - The key to look up
* @returns The cached value, or undefined if not found or expired
*/
get(key) {
const startTime = performance.now();
const node = this.cache.get(key);
if (!node) {
this.stats.misses++;
this.updateHitRate();
this.emitEvent("get", key, void 0, { hit: false });
return void 0;
}
if (this.isExpired(node)) {
this.delete(key);
this.stats.misses++;
this.updateHitRate();
this.emitEvent("get", key, void 0, { hit: false, expired: true });
return void 0;
}
this.moveToHead(node);
node.lastAccessed = Date.now();
node.accessCount++;
this.stats.hits++;
this.updateHitRate();
this.updateAverageGetTime(performance.now() - startTime);
this.emitEvent("get", key, node.value, { hit: true });
if (this.config.enableStats) {
this.updateMostAccessedKey(key, node.accessCount);
}
return node.value;
}
/**
* Sets a value in the cache
*
* Mathematical Process:
* 1. Check if key exists in hash map: O(1)
* 2. If exists, update value and move to head: O(1)
* 3. If not exists, create new node and add to head: O(1)
* 4. If cache is full, evict tail node: O(1)
* 5. Update statistics
*
* @param key - The key to store
* @param value - The value to store
* @returns True if the operation was successful
*/
set(key, value) {
const startTime = performance.now();
try {
const existingNode = this.cache.get(key);
if (existingNode) {
existingNode.value = value;
existingNode.lastAccessed = Date.now();
existingNode.accessCount++;
this.moveToHead(existingNode);
this.emitEvent("set", key, value, { updated: true });
} else {
const newNode = {
key,
value,
prev: null,
next: null,
lastAccessed: Date.now(),
accessCount: 1
};
this.cache.set(key, newNode);
this.stats.size++;
this.stats.sets++;
this.addToHead(newNode);
if (this.cache.size > this.config.maxSize) {
this.evict();
}
this.emitEvent("set", key, value, { updated: false });
}
this.updateAverageSetTime(performance.now() - startTime);
this.updateMemoryUsage();
return true;
} catch (error) {
this.emitEvent("set", key, value, { error: error instanceof Error ? error.message : "Unknown error" });
return false;
}
}
/**
* Deletes a value from the cache
*
* Mathematical Process:
* 1. Look up node in hash map: O(1)
* 2. Remove from doubly-linked list: O(1)
* 3. Remove from hash map: O(1)
* 4. Update statistics
*
* @param key - The key to delete
* @returns True if the key existed and was deleted
*/
delete(key) {
const startTime = performance.now();
const node = this.cache.get(key);
if (!node) {
return false;
}
this.removeNode(node);
this.cache.delete(key);
this.stats.size--;
this.stats.deletes++;
this.updateAverageDeleteTime(performance.now() - startTime);
this.updateMemoryUsage();
this.emitEvent("delete", key, node.value);
return true;
}
/**
* Checks if a key exists in the cache
*
* @param key - The key to check
* @returns True if the key exists and is not expired
*/
has(key) {
const node = this.cache.get(key);
if (!node) {
return false;
}
if (this.isExpired(node)) {
this.delete(key);
return false;
}
return true;
}
/**
* Clears all entries from the cache
*/
clear() {
this.cache.clear();
this.head = null;
this.tail = null;
this.stats.size = 0;
this.emitEvent("clear");
}
/**
* Gets the current size of the cache
*
* @returns Number of items in the cache
*/
size() {
return this.stats.size;
}
/**
* Gets the maximum size of the cache
*
* @returns Maximum number of items the cache can hold
*/
maxSize() {
return this.config.maxSize;
}
/**
* Gets cache statistics
*
* @returns Current cache statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Gets performance metrics
*
* @returns Current performance metrics
*/
getPerformanceMetrics() {
return { ...this.performanceMetrics };
}
/**
* Creates a snapshot of the cache state
*
* @returns Cache snapshot for debugging and analysis
*/
snapshot() {
const entries = [];
let current = this.head;
while (current) {
entries.push({
key: current.key,
value: current.value,
createdAt: current.lastAccessed,
lastAccessed: current.lastAccessed,
accessCount: current.accessCount,
ttl: this.config.ttl,
isExpired: this.isExpired(current)
});
current = current.next;
}
return {
entries,
stats: this.getStats(),
config: { ...this.config },
timestamp: Date.now()
};
}
/**
* Batch sets multiple key-value pairs
*
* @param entries - Array of key-value pairs to set
* @returns Batch operation result
*/
batchSet(entries) {
const processed = [];
const failed = [];
for (const { key, value } of entries) {
try {
if (this.set(key, value)) {
processed.push({ key, value });
} else {
failed.push({ key, value, error: "Set operation failed" });
}
} catch (error) {
failed.push({
key,
value,
error: error instanceof Error ? error.message : "Unknown error"
});
}
}
return {
processed,
failed,
total: entries.length,
successRate: processed.length / entries.length
};
}
/**
* Creates an iterator for the cache entries
*
* @returns Iterator that yields entries in LRU order (most recent first)
*/
*[Symbol.iterator]() {
let current = this.head;
while (current) {
if (!this.isExpired(current)) {
yield { key: current.key, value: current.value };
}
current = current.next;
}
}
/**
* Moves a node to the head of the doubly-linked list
*
* @param node - The node to move
*/
moveToHead(node) {
this.removeNode(node);
this.addToHead(node);
}
/**
* Adds a node to the head of the doubly-linked list
*
* @param node - The node to add
*/
addToHead(node) {
node.prev = null;
node.next = this.head;
if (this.head) {
this.head.prev = node;
}
this.head = node;
if (!this.tail) {
this.tail = node;
}
}
/**
* Removes a node from the doubly-linked list
*
* @param node - The node to remove
*/
removeNode(node) {
if (node.prev) {
node.prev.next = node.next;
} else {
this.head = node.next;
}
if (node.next) {
node.next.prev = node.prev;
} else {
this.tail = node.prev;
}
}
/**
* Evicts the least recently used item (tail of the list)
*/
evict() {
if (!this.tail) {
return;
}
const evictedKey = this.tail.key;
const evictedValue = this.tail.value;
this.removeNode(this.tail);
this.cache.delete(evictedKey);
this.stats.size--;
this.stats.evictions++;
this.emitEvent("evict", evictedKey, evictedValue);
}
/**
* Checks if a node has expired based on TTL
*
* @param node - The node to check
* @returns True if the node has expired
*/
isExpired(node) {
if (this.config.ttl <= 0) {
return false;
}
return Date.now() - node.lastAccessed > this.config.ttl;
}
/**
* Starts the cleanup timer for expired entries
*/
startCleanupTimer() {
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.config.cleanupInterval);
}
/**
* Cleans up expired entries
*/
cleanup() {
const startTime = performance.now();
const expiredKeys = [];
for (const [key, node] of this.cache) {
if (this.isExpired(node)) {
expiredKeys.push(key);
}
}
for (const key of expiredKeys) {
this.delete(key);
this.emitEvent("expire", key);
}
this.performanceMetrics.cleanupCount++;
this.performanceMetrics.totalCleanupTime += performance.now() - startTime;
this.emitEvent("cleanup", void 0, void 0, { expiredCount: expiredKeys.length });
}
/**
* Updates the hit rate statistic
*/
updateHitRate() {
const total = this.stats.hits + this.stats.misses;
this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
}
/**
* Updates the most accessed key statistic
*
* @param key - The key that was accessed
* @param accessCount - The access count for the key
*/
updateMostAccessedKey(key, accessCount) {
if (!this.stats.mostAccessedKey) {
this.stats.mostAccessedKey = key;
return;
}
const mostAccessedNode = this.cache.get(this.stats.mostAccessedKey);
if (!mostAccessedNode || accessCount > mostAccessedNode.accessCount) {
this.stats.mostAccessedKey = key;
}
}
/**
* Updates the average get time
*
* @param time - Time taken for the get operation
*/
updateAverageGetTime(time) {
const totalGets = this.stats.hits + this.stats.misses;
this.performanceMetrics.averageGetTime = (this.performanceMetrics.averageGetTime * (totalGets - 1) + time) / totalGets;
}
/**
* Updates the average set time
*
* @param time - Time taken for the set operation
*/
updateAverageSetTime(time) {
this.performanceMetrics.averageSetTime = (this.performanceMetrics.averageSetTime * (this.stats.sets - 1) + time) / this.stats.sets;
}
/**
* Updates the average delete time
*
* @param time - Time taken for the delete operation
*/
updateAverageDeleteTime(time) {
this.performanceMetrics.averageDeleteTime = (this.performanceMetrics.averageDeleteTime * (this.stats.deletes - 1) + time) / this.stats.deletes;
}
/**
* Updates the estimated memory usage
*/
updateMemoryUsage() {
let estimatedSize = 0;
for (const [key, node] of this.cache) {
estimatedSize += JSON.stringify(key).length * 2;
estimatedSize += JSON.stringify(node.value).length * 2;
estimatedSize += 64;
}
this.performanceMetrics.estimatedMemoryUsage = estimatedSize;
}
/**
* Emits an event if event handler is configured
*
* @param type - Event type
* @param key - Event key
* @param value - Event value
* @param metadata - Additional metadata
*/
emitEvent(type, key, value, metadata) {
const event = {
type,
timestamp: Date.now(),
key,
value,
metadata
};
if (this.eventHandler) {
this.eventHandler(event);
}
if (key !== void 0 && value !== void 0) {
switch (type) {
case "set":
if (this.onSet) {
this.onSet(key, value);
}
break;
case "get":
if (this.onAccess) {
this.onAccess(key, value);
}
break;
case "delete":
if (this.onDelete) {
this.onDelete(key, value);
}
break;
case "evict":
if (this.onEvict) {
this.onEvict(key, value);
}
break;
}
}
}
/**
* Destroys the cache and cleans up resources
*/
destroy() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.clear();
}
}
var TrieEventType = /* @__PURE__ */ ((TrieEventType2) => {
TrieEventType2["WORD_INSERTED"] = "word_inserted";
TrieEventType2["WORD_DELETED"] = "word_deleted";
TrieEventType2["WORD_SEARCHED"] = "word_searched";
TrieEventType2["PREFIX_SEARCHED"] = "prefix_searched";
TrieEventType2["AUTCOMPLETE_PERFORMED"] = "autocomplete_performed";
TrieEventType2["NODE_CREATED"] = "node_created";
TrieEventType2["NODE_DELETED"] = "node_deleted";
TrieEventType2["COMPRESSION_PERFORMED"] = "compression_performed";
TrieEventType2["CLEAR_PERFORMED"] = "clear_performed";
return TrieEventType2;
})(TrieEventType || {});
const DEFAULT_TRIE_CONFIG = {
caseSensitive: false,
allowEmptyStrings: false,
maxDepth: 1e3,
useCompression: false,
trackFrequency: true,
enableWildcards: true,
wildcardChar: "*",
enableFuzzyMatching: false,
maxEditDistance: 2
};
const DEFAULT_TRIE_OPTIONS = {
config: DEFAULT_TRIE_CONFIG,
enableStats: true,
enableDebug: false,
initialWords: []
};
class Trie {
root;
config;
eventHandlers;
enableStats;
enableDebug;
stats;
constructor(options = {}) {
const opts = { ...DEFAULT_TRIE_OPTIONS, ...options };
this.config = { ...DEFAULT_TRIE_CONFIG, ...opts.config };
this.eventHandlers = opts.eventHandlers || [];
this.enableStats = opts.enableStats ?? true;
this.enableDebug = opts.enableDebug ?? false;
this.root = this.createNode("", null);
this.root.isEndOfWord = false;
this.stats = {
totalWords: 0,
totalNodes: 1,
// Root node
averageWordLength: 0,
maxDepth: 0,
totalSearches: 0,
totalInserts: 0,
totalDeletes: 0,
averageSearchTime: 0,
memoryUsage: 0
};
if (opts.initialWords && opts.initialWords.length > 0) {
this.insertBatch(opts.initialWords);
}
}
/**
* Insert a word into the trie
*
* @param word The word to insert
* @param data Optional data associated with the word
* @returns True if insertion was successful
*/
insert(word, data) {
const startTime = performance.now();
try {
const normalizedWord = this.normalizeWord(word);
if (!this.config.allowEmptyStrings && normalizedWord.length === 0) {
return false;
}
if (normalizedWord.length > this.config.maxDepth) {
return false;
}
let currentNode = this.root;
for (let i = 0; i < normalizedWord.length; i++) {
const char = normalizedWord[i];
if (!currentNode.children.has(char)) {
const newNode = this.createNode(char, currentNode);
currentNode.children.set(char, newNode);
this.emitEvent(TrieEventType.NODE_CREATED, { node: newNode, char });
}
currentNode = currentNode.children.get(char);
}
currentNode.isEndOfWord = true;
currentNode.data = data;
currentNode.frequency++;
this.stats.totalWords++;
this.stats.totalNodes = this.countNodes();
this.stats.averageWordLength = this.calculateAverageWordLength();
this.stats.maxDepth = Math.max(this.stats.maxDepth, normalizedWord.length);
this.stats.totalInserts++;
this.emitEvent(TrieEventType.WORD_INSERTED, { word: normalizedWord, data });
return true;
} catch (error) {
return false;
} finally {
if (this.enableStats) {
const executionTime = performance.now() - startTime;
this.stats.averageSearchTime = (this.stats.averageSearchTime * (this.stats.totalInserts - 1) + executionTime) / this.stats.totalInserts;
}
}
}
/**
* Search for a word in the trie
*
* @param word The word to search for
* @returns Search result
*/
search(word) {
const startTime = performance.now();
let nodesTraversed = 0;
try {
const normalizedWord = this.normalizeWord(word);
if (!this.config.allowEmptyStrings && normalizedWord.length === 0) {
return {
found: false,
word: null,
data: null,
frequency: 0,
executionTime: performance.now() - startTime,
nodesTraversed: 0
};
}
let currentNode = this.root;
nodesTraversed++;
for (let i = 0; i < normalizedWord.length; i++) {
const char = normalizedWord[i];
if (!currentNode.children.has(char)) {
return {
found: false,
word: null,
data: null,
frequency: 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
}
currentNode = currentNode.children.get(char);
nodesTraversed++;
}
const found = currentNode.isEndOfWord;
if (this.enableStats) {
this.stats.totalSearches++;
const executionTime = performance.now() - startTime;
this.stats.averageSearchTime = (this.stats.averageSearchTime * (this.stats.totalSearches - 1) + executionTime) / this.stats.totalSearches;
}
this.emitEvent(TrieEventType.WORD_SEARCHED, { word: normalizedWord, found });
return {
found,
word: found ? normalizedWord : null,
data: found ? currentNode.data : null,
frequency: found ? currentNode.frequency : 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
} catch (error) {
return {
found: false,
word: null,
data: null,
frequency: 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
}
}
/**
* Delete a word from the trie
*
* @param word The word to delete
* @returns True if deletion was successful
*/
delete(word) {
const startTime = performance.now();
try {
const normalizedWord = this.normalizeWord(word);
if (!this.config.allowEmptyStrings && normalizedWord.length === 0) {
return false;
}
const result = this.deleteRecursive(this.root, normalizedWord, 0);
if (result) {
this.stats.totalWords--;
this.stats.totalNodes = this.countNodes();
this.stats.averageWordLength = this.calculateAverageWordLength();
this.stats.totalDeletes++;
this.emitEvent(TrieEventType.WORD_DELETED, { word: normalizedWord });
}
return result;
} catch (error) {
return false;
} finally {
if (this.enableStats) {
const executionTime = performance.now() - startTime;
this.stats.averageSearchTime = (this.stats.averageSearchTime * (this.stats.totalDeletes - 1) + executionTime) / this.stats.totalDeletes;
}
}
}
/**
* Find all words with a given prefix
*
* @param prefix The prefix to search for
* @returns Prefix search result
*/
findWordsWithPrefix(prefix) {
const startTime = performance.now();
let nodesTraversed = 0;
try {
const normalizedPrefix = this.normalizeWord(prefix);
const words = [];
const data = [];
let currentNode = this.root;
nodesTraversed++;
for (let i = 0; i < normalizedPrefix.length; i++) {
const char = normalizedPrefix[i];
if (!currentNode.children.has(char)) {
return {
words: [],
data: [],
count: 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
}
currentNode = currentNode.children.get(char);
nodesTraversed++;
}
this.collectWords(currentNode, normalizedPrefix, words, data, nodesTraversed);
this.emitEvent(TrieEventType.PREFIX_SEARCHED, { prefix: normalizedPrefix, count: words.length });
return {
words,
data,
count: words.length,
executionTime: performance.now() - startTime,
nodesTraversed
};
} catch (error) {
return {
words: [],
data: [],
count: 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
}
}
/**
* Get autocomplete suggestions for a prefix
*
* @param prefix The prefix to autocomplete
* @param maxSuggestions Maximum number of suggestions
* @returns Autocomplete result
*/
autocomplete(prefix, maxSuggestions = 10) {
const startTime = performance.now();
let nodesTraversed = 0;
try {
const normalizedPrefix = this.normalizeWord(prefix);
const suggestions = [];
let currentNode = this.root;
nodesTraversed++;
for (let i = 0; i < normalizedPrefix.length; i++) {
const char = normalizedPrefix[i];
if (!currentNode.children.has(char)) {
return {
suggestions: [],
count: 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
}
currentNode = currentNode.children.get(char);
nodesTraversed++;
}
this.collectAutocompleteSuggestions(currentNode, normalizedPrefix, suggestions, maxSuggestions, nodesTraversed);
suggestions.sort((a, b) => b.score - a.score);
this.emitEvent(TrieEventType.AUTCOMPLETE_PERFORMED, { prefix: normalizedPrefix, count: suggestions.length });
return {
suggestions: suggestions.slice(0, maxSuggestions),
count: suggestions.length,
executionTime: performance.now() - startTime,
nodesTraversed
};
} catch (error) {
return {
suggestions: [],
count: 0,
executionTime: performance.now() - startTime,
nodesTraversed
};
}
}
/**
* Find fuzzy matches for a word
*
* @param word The word to find fuzzy matches for
* @param maxDistance Maximum edit distance
* @returns Array of fuzzy match results
*/
findFuzzyMatches(word, maxDistance) {
if (!this.config.enableFuzzyMatching) {
return [];
}
const normalizedWord = this.normalizeWord(word);
const maxEditDistance = maxDistance || this.config.maxEditDistance;
const matches = [];
this.findFuzzyMatchesRecursive(this.root, "", normalizedWord, maxEditDistance, matches);
matches.sort((a, b) => b.similarity - a.similarity);
return matches;
}
/**
* Find wildcard matches for a pattern
*
* @param pattern The pattern to match (with wildcards)
* @returns Array of wildcard match results
*/
findWildcardMatches(pattern) {
if (!this.config.enableWildcards) {
return [];
}
const normalizedPattern = this.normalizeWord(pattern);
const matches = [];
this.findWildcardMatchesRecursive(this.root, "", normalizedPattern, 0, matches);
return matches;
}
/**
* Get all words in the trie
*
* @returns Array of all words
*/
getAllWords() {
const words = [];
const data = [];
this.collectWords(this.root, "", words, data, 0);
return words;
}
/**
* Get the size of the trie (number of words)
*
* @returns Number of words in the trie
*/
size() {
return this.stats.totalWords;
}
/**
* Check if the trie is empty
*
* @returns True if the trie is empty
*/
isEmpty() {
return this.stats.totalWords === 0;
}
/**
* Clear all words from the trie
*/
clear() {
this.root.children.clear();
this.root.isEndOfWord = false;
this.root.data = null;
this.root.frequency = 0;
this.stats = {
totalWords: 0,
totalNodes: 1,
averageWordLength: 0,
maxDepth: 0,
totalSearches: 0,
totalInserts: 0,
totalDeletes: 0,
averageSearchTime: 0,
memoryUsage: 0
};
this.emitEvent(TrieEventType.CLEAR_PERFORMED, {});
}
/**
* Insert multiple words in batch
*
* @param words Array of words to insert
* @returns Batch operation result
*/
insertBatch(words) {
const startTime = performance.now();
const results = [];
const errors = [];
let successful = 0;
let failed = 0;
for (const word of words) {
try {
const result = this.insert(word);
results.push(result);
if (result) {
successful++;
} else {
failed++;
errors.push(`Failed to insert word: ${word}`);
}
} catch (error) {
results.push(false);
failed++;
errors.push(`Error inserting word ${word}: ${error}`);
}
}
return {
successful,
failed,
errors,
executionTime: performance.now() - startTime,
results
};
}
/**
* Traverse the trie with custom options
*
* @param options Traversal options
* @returns Traversal result
*/
traverse(options = {}) {
const startTime = performance.now();
const opts = {
includeIntermediate: false,
maxDepth: this.config.maxDepth,
sorted: true,
...options
};
const nodes = [];
const words = [];
const nodesVisited = { value: 0 };
this.traverseRecursive(this.root, "", nodes, words, nodesVisited, opts);
return {
nodes,
words,
nodesVisited: nodesVisited.value,
executionTime: performance.now() - startTime
};
}
/**
* Serialize the trie to a JSON format
*
* @returns Serialized trie data
*/
serialize() {
const data = this.serializeNode(this.root);
return {
version: "1.0",
config: this.config,
data,
metadata: {
totalWords: this.stats.totalWords,
totalNodes: this.stats.totalNodes,
maxDepth: this.stats.maxDepth,
createdAt: Date.now()
}
};
}
/**
* Deserialize a trie from JSON format
*
* @param serialized Serialized trie data
* @returns True if deserialization was successful
*/
deserialize(serialized) {
try {
this.clear();
this.config = serialized.config;
this.deserializeNode(this.root, serialized.data);
this.stats.totalWords = serialized.metadata.totalWords;
this.stats.totalNodes = serialized.metadata.totalNodes;
this.stats.maxDepth = serialized.metadata.maxDepth;
return true;
} catch (error) {
return false;
}
}
/**
* Add event handler
*/
addEventHandler(handler) {
this.eventHandlers.push(handler);
}
/**
* Remove event handler
*/
removeEventHandler(handler) {
const index = this.eventHandlers.indexOf(handler);
if (index > -1) {
this.eventHandlers.splice(index, 1);
}
}
/**
* Get current statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Get performance metrics
*/
getPerformanceMetrics() {
const performanceScore = Math.min(
100,
Math.max(
0,
Math.max(0, 1 - this.stats.averageSearchTime / 10) * 40 + Math.max(0, 1 - this.stats.totalNodes / 1e4) * 30 + Math.max(0, 1 - this.stats.memoryUsage / 1e6) * 30
)
);
return {
memoryUsage: this.stats.memoryUsage,
averageSearchTime: this.stats.averageSearchTime,
averageInsertTime: this.stats.averageSearchTime,
// Using same value for now
averageDeleteTime: this.stats.averageSearchTime,
// Using same value for now
performanceScore,
compressionRatio: 0
// TODO: Implement compression ratio calculation
};
}
/**
* Update configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
// Private helper methods
/**
* Create a new trie node
*/
createNode(char, parent) {
return {
char,
isEndOfWord: false,
children: /* @__PURE__ */ new Map(),
parent,
data: null,
frequency: 0,
depth: parent ? parent.depth + 1 : 0
};
}
/**
* Normalize a word based on configuration
*/
normalizeWord(word) {
if (!this.config.caseSensitive) {
return word.toLowerCase();
}
return word;
}
/**
* Recursive delete helper
*/
deleteRecursive(node, word, index) {
if (index === word.length) {
if (!node.isEndOfWord) {
return false;
}
node.isEndOfWord = false;
node.data = null;
node.frequency = 0;
return node.children.size === 0 && node !== this.root;
}
const char = word[index];
const child = node.children.get(char);
if (!child) {
return false;
}
const shouldDeleteChild = this.deleteRecursive(child, word, index + 1);
if (shouldDeleteChild) {
node.children.delete(char);
this.emitEvent(TrieEventType.NODE_DELETED, { node: child, char });
}
return !node.isEndOfWord && node.children.size === 0 && node !== this.root;
}
/**
* Collect all words from a node
*/
collectWords(node, prefix, words, data, nodesTraversed) {
if (node.isEndOfWord) {
words.push(prefix);
data.push(node.data);
}
const children = Array.from(node.children.ent