UNPKG

@entropy-tamer/reynard-algorithms

Version:

Algorithm primitives and data structures for Reynard applications

1,796 lines 927 kB
"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