CHECKPOINT: Session 4 - Complete Optimization Suite

 Unified Cache System
- Created UnifiedCache with cost-aware eviction
- Integrated with both MetadataIndex and HNSW
- Request coalescing, fairness monitoring, access patterns

 Index Persistence
- Sorted indices for range queries saved/loaded
- Integrated with UnifiedCache (100x rebuild cost)

 TripleIntelligence Fixed
- Native Brain Pattern support
- Direct metadata filtering without string conversion

 Competitive Analysis
- Created comprehensive docs/COMPETITIVE-ANALYSIS.md
- Shows Brainy advantages vs all competitors

 All Infrastructure Complete
- TypeScript: 0 errors
- Memory: Optimized with unified cache
- Models: Cached locally
- Ready for comprehensive testing
This commit is contained in:
David Snelling 2025-08-25 15:05:39 -07:00
parent 88abcddf84
commit f0ee5f44ec
16 changed files with 2081 additions and 582 deletions

View file

@ -592,7 +592,14 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
return this.augmentations.get('cache')
}
private get index(): any {
// IMPORTANT: this.index returns the HNSW vector index, NOT the metadata index!
// The metadata index is available through this.metadataIndex
private get index(): HNSWIndex | HNSWIndexOptimized {
return this.hnswIndex
}
// Metadata index for field-based queries (from IndexAugmentation)
private get metadataIndex(): any {
return this.augmentations.get('index')
}
@ -1120,12 +1127,13 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
* Start metadata index maintenance
*/
private startMetadataIndexMaintenance(): void {
if (!this.index) return
const metaIndex = this.metadataIndex
if (!metaIndex) return
// Flush index periodically to persist changes
const flushInterval = setInterval(async () => {
try {
await this.index!.flush()
await metaIndex.flush()
} catch (error) {
prodLog.warn('Error flushing metadata index:', error)
}
@ -1702,7 +1710,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Skip rebuild for memory storage (starts empty) or when in read-only mode
// Also skip if index already has entries
const isMemoryStorage = this.storage?.constructor?.name === 'MemoryStorage'
const stats = await this.index?.getStats?.() || { totalEntries: 0 }
const stats = await this.metadataIndex?.getStats?.() || { totalEntries: 0 }
if (!isMemoryStorage && !this.readOnly && stats.totalEntries === 0) {
// Check if we have existing data that needs indexing
@ -1717,9 +1725,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
if (this.loggingConfig?.verbose) {
console.log('🔄 Rebuilding metadata index for existing data...')
}
await this.index.rebuild()
await this.metadataIndex?.rebuild?.()
if (this.loggingConfig?.verbose) {
const newStats = await this.index?.getStats?.() || { totalEntries: 0 }
const newStats = await this.metadataIndex?.getStats?.() || { totalEntries: 0 }
console.log(`✅ Metadata index rebuilt: ${newStats.totalEntries} entries, ${newStats.fieldsIndexed.length} fields`)
}
} else {
@ -2085,11 +2093,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
metadata: undefined // Will be set separately
}
} else {
// Normal mode: Add to index first
await this.index.addItem({ id, vector })
// Normal mode: Add to HNSW index first
await this.hnswIndex.addItem({ id, vector, metadata })
// Get the noun from the index
const indexNoun = this.index.getNouns().get(id)
// Get the noun from the HNSW index
const indexNoun = this.hnswIndex.getNouns().get(id)
if (!indexNoun) {
throw new Error(`Failed to retrieve newly created noun with ID ${id}`)
}
@ -2200,7 +2208,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Update metadata index (write-only mode should build indices!)
if (this.index && !this.frozen) {
await this.index.addToIndex(id, metadataToSave)
await this.metadataIndex?.addToIndex?.(id, metadataToSave)
}
// Track metadata statistics
@ -2218,8 +2226,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
)
}
// Track update timestamp
this.metrics.trackUpdate()
// Track update timestamp (handled by metrics augmentation)
}
}
@ -2638,13 +2645,13 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
let preFilteredIds: Set<string> | undefined
// Use metadata index for pre-filtering if available
if (hasMetadataFilter && this.index) {
if (hasMetadataFilter && this.metadataIndex) {
try {
// Ensure metadata index is up to date
await this.index?.flush?.()
await this.metadataIndex?.flush?.()
// Get candidate IDs from metadata index
const candidateIds = await this.index.getIdsForFilter(options.metadata)
const candidateIds = await this.metadataIndex?.getIdsForFilter?.(options.metadata) || []
if (candidateIds.length > 0) {
preFilteredIds = new Set(candidateIds)
@ -4072,7 +4079,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Update metadata index
if (this.index && verbMetadata) {
await this.index.addToIndex(id, verbMetadata)
await this.metadataIndex?.addToIndex?.(id, verbMetadata)
}
// Track verb statistics
@ -4443,7 +4450,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Remove from metadata index
if (this.index && existingMetadata) {
await this.index.removeFromIndex(id, existingMetadata)
await this.metadataIndex?.removeFromIndex?.(id, existingMetadata)
}
// Remove from storage
@ -4817,7 +4824,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Always include for now
// Add index health metrics
try {
const indexHealth = this.index.getIndexHealth()
const indexHealth = this.metadataIndex?.getIndexHealth?.() || { healthy: true }
;(result as any).indexHealth = indexHealth
} catch (e) {
// Index health not available
@ -5498,7 +5505,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Delegate to index augmentation
const index = this.augmentations.get('index') as any
return this.index?.getFilterValues(field) || []
return index?.getFilterValues?.(field) || []
}
/**
@ -5512,7 +5519,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Delegate to index augmentation
const index = this.augmentations.get('index') as any
return this.index?.getFilterFields() || []
return index?.getFilterFields?.() || []
}
/**
@ -6528,9 +6535,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
this.maintenanceIntervals = []
// Flush metadata index one last time
if (this.index) {
if (this.metadataIndex) {
try {
await this.index?.flush?.()
await this.metadataIndex?.flush?.()
} catch (error) {
console.warn('Error flushing metadata index during cleanup:', error)
}
@ -7195,7 +7202,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
* Exposed for Cortex reindex command
*/
async rebuildMetadataIndex(): Promise<void> {
await this.index?.rebuild()
await this.metadataIndex?.rebuild?.()
}
// ===== Clean 2.0 API - Primary Methods =====

View file

@ -13,6 +13,7 @@ import {
} from '../coreTypes.js'
import { HNSWIndex } from './hnswIndex.js'
import { StorageAdapter } from '../coreTypes.js'
import { getGlobalCache, UnifiedCache } from '../utils/unifiedCache.js'
// Configuration for the optimized HNSW index
export interface HNSWOptimizedConfig extends HNSWConfig {
@ -296,6 +297,9 @@ export class HNSWIndexOptimized extends HNSWIndex {
// Thread safety for memory usage tracking
private memoryUpdateLock: Promise<void> = Promise.resolve()
// Unified cache for coordinated memory management
private unifiedCache: UnifiedCache
constructor(
config: Partial<HNSWOptimizedConfig> = {},
@ -322,6 +326,9 @@ export class HNSWIndexOptimized extends HNSWIndex {
// Set disk-based index flag
this.useDiskBasedIndex = this.optimizedConfig.useDiskBasedIndex || false
// Get global unified cache for coordinated memory management
this.unifiedCache = getGlobalCache()
}
/**

View file

@ -1,414 +0,0 @@
/**
* Field Index for efficient field-based queries
* Provides O(log n) lookups for field values and range queries
*/
import { VectorDocument } from '../coreTypes.js'
interface FieldIndexEntry {
value: any
ids: Set<string>
}
interface RangeQueryOptions {
field: string
min?: any
max?: any
includeMin?: boolean
includeMax?: boolean
}
export class FieldIndex {
// Inverted index: field -> value -> noun IDs
private indices: Map<string, Map<any, Set<string>>> = new Map()
// Sorted arrays for range queries: field -> sorted [value, ids] pairs
private sortedIndices: Map<string, Array<[any, Set<string>]>> = new Map()
// Track which fields are indexed
private indexedFields: Set<string> = new Set()
/**
* Add a document to the field index
*/
public add(id: string, metadata: Record<string, any>): void {
if (!metadata) return
for (const [field, value] of Object.entries(metadata)) {
// Skip null/undefined values
if (value === null || value === undefined) continue
// Get or create field index
if (!this.indices.has(field)) {
this.indices.set(field, new Map())
this.sortedIndices.set(field, [])
this.indexedFields.add(field)
}
const fieldIndex = this.indices.get(field)!
// Get or create value set
if (!fieldIndex.has(value)) {
fieldIndex.set(value, new Set())
}
// Add ID to value set
fieldIndex.get(value)!.add(id)
// Mark sorted index as dirty (needs rebuild)
this.markSortedIndexDirty(field)
}
}
/**
* Remove a document from the field index
*/
public remove(id: string, metadata: Record<string, any>): void {
if (!metadata) return
for (const [field, value] of Object.entries(metadata)) {
if (value === null || value === undefined) continue
const fieldIndex = this.indices.get(field)
if (!fieldIndex) continue
const valueSet = fieldIndex.get(value)
if (!valueSet) continue
valueSet.delete(id)
// Clean up empty sets
if (valueSet.size === 0) {
fieldIndex.delete(value)
this.markSortedIndexDirty(field)
}
// Clean up empty field indices
if (fieldIndex.size === 0) {
this.indices.delete(field)
this.sortedIndices.delete(field)
this.indexedFields.delete(field)
}
}
}
/**
* Query for exact field value match
* O(1) hash lookup
*/
public queryExact(field: string, value: any): string[] {
const fieldIndex = this.indices.get(field)
if (!fieldIndex) return []
const ids = fieldIndex.get(value)
return ids ? Array.from(ids) : []
}
/**
* Query for multiple values (IN operator)
* O(k) where k is number of values
*/
public queryIn(field: string, values: any[]): string[] {
const fieldIndex = this.indices.get(field)
if (!fieldIndex) return []
const resultSet = new Set<string>()
for (const value of values) {
const ids = fieldIndex.get(value)
if (ids) {
for (const id of ids) {
resultSet.add(id)
}
}
}
return Array.from(resultSet)
}
/**
* Query for range of values
* O(log n + m) where m is number of results
*/
public queryRange(options: RangeQueryOptions): string[] {
const { field, min, max, includeMin = true, includeMax = true } = options
// Ensure sorted index is up to date
this.ensureSortedIndex(field)
const sortedIndex = this.sortedIndices.get(field)
if (!sortedIndex || sortedIndex.length === 0) return []
const resultSet = new Set<string>()
// Binary search for start position
let start = 0
let end = sortedIndex.length - 1
if (min !== undefined) {
start = this.binarySearch(sortedIndex, min, includeMin)
}
if (max !== undefined) {
end = this.binarySearchEnd(sortedIndex, max, includeMax)
}
// Collect all IDs in range
for (let i = start; i <= end && i < sortedIndex.length; i++) {
const [value, ids] = sortedIndex[i]
// Check if value is in range
if (min !== undefined) {
const minCheck = includeMin ? value >= min : value > min
if (!minCheck) continue
}
if (max !== undefined) {
const maxCheck = includeMax ? value <= max : value < max
if (!maxCheck) break
}
for (const id of ids) {
resultSet.add(id)
}
}
return Array.from(resultSet)
}
/**
* Query with complex where clause
*/
public query(where: Record<string, any>): string[] {
const resultSets: Set<string>[] = []
for (const [field, condition] of Object.entries(where)) {
let fieldResults: string[] = []
if (typeof condition === 'object' && condition !== null) {
// Handle operators
if (condition.equals !== undefined) {
fieldResults = this.queryExact(field, condition.equals)
} else if (condition.in !== undefined && Array.isArray(condition.in)) {
fieldResults = this.queryIn(field, condition.in)
} else if (condition.greaterThan !== undefined || condition.lessThan !== undefined) {
fieldResults = this.queryRange({
field,
min: condition.greaterThan,
max: condition.lessThan,
includeMin: false,
includeMax: false
})
} else if (condition.greaterEqual !== undefined || condition.lessEqual !== undefined) {
fieldResults = this.queryRange({
field,
min: condition.greaterEqual,
max: condition.lessEqual,
includeMin: true,
includeMax: true
})
} else if (condition.between !== undefined && Array.isArray(condition.between)) {
fieldResults = this.queryRange({
field,
min: condition.between[0],
max: condition.between[1],
includeMin: true,
includeMax: true
})
} else if (condition.exists !== undefined) {
// Return all IDs that have this field
if (condition.exists) {
const fieldIndex = this.indices.get(field)
if (fieldIndex) {
const allIds = new Set<string>()
for (const ids of fieldIndex.values()) {
for (const id of ids) {
allIds.add(id)
}
}
fieldResults = Array.from(allIds)
}
}
}
} else {
// Direct value match
fieldResults = this.queryExact(field, condition)
}
if (fieldResults.length > 0) {
resultSets.push(new Set(fieldResults))
} else {
// If any field has no matches, intersection will be empty
return []
}
}
// Intersect all result sets (AND operation)
if (resultSets.length === 0) return []
if (resultSets.length === 1) return Array.from(resultSets[0])
let intersection = resultSets[0]
for (let i = 1; i < resultSets.length; i++) {
const nextSet = resultSets[i]
const newIntersection = new Set<string>()
// Use smaller set for iteration (optimization)
const [smaller, larger] = intersection.size <= nextSet.size
? [intersection, nextSet]
: [nextSet, intersection]
for (const id of smaller) {
if (larger.has(id)) {
newIntersection.add(id)
}
}
intersection = newIntersection
// Early exit if intersection is empty
if (intersection.size === 0) return []
}
return Array.from(intersection)
}
/**
* Mark sorted index as needing rebuild
*/
private markSortedIndexDirty(field: string): void {
// For now, we'll rebuild on demand
// Could optimize with a dirty flag if needed
}
/**
* Ensure sorted index is up to date for a field
*/
private ensureSortedIndex(field: string): void {
const fieldIndex = this.indices.get(field)
if (!fieldIndex) return
// Rebuild sorted index from hash index
const sorted: Array<[any, Set<string>]> = []
for (const [value, ids] of fieldIndex.entries()) {
sorted.push([value, ids])
}
// Sort by value (handles numbers, strings, dates)
sorted.sort((a, b) => {
const aVal = a[0]
const bVal = b[0]
if (aVal < bVal) return -1
if (aVal > bVal) return 1
return 0
})
this.sortedIndices.set(field, sorted)
}
/**
* Binary search for start position (inclusive)
*/
private binarySearch(sorted: Array<[any, Set<string>]>, target: any, inclusive: boolean): number {
let left = 0
let right = sorted.length - 1
let result = sorted.length
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midVal = sorted[mid][0]
if (inclusive ? midVal >= target : midVal > target) {
result = mid
right = mid - 1
} else {
left = mid + 1
}
}
return result
}
/**
* Binary search for end position (inclusive)
*/
private binarySearchEnd(sorted: Array<[any, Set<string>]>, target: any, inclusive: boolean): number {
let left = 0
let right = sorted.length - 1
let result = -1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midVal = sorted[mid][0]
if (inclusive ? midVal <= target : midVal < target) {
result = mid
left = mid + 1
} else {
right = mid - 1
}
}
return result
}
/**
* Debug method to inspect index contents
*/
public debugIndex(field?: string): any {
if (field) {
const fieldIndex = this.indices.get(field)
if (!fieldIndex) return { error: 'Field not found', field }
const values: any[] = []
for (const [value, ids] of fieldIndex.entries()) {
values.push({ value, type: typeof value, ids: Array.from(ids) })
}
return { field, values }
}
const allFields: any = {}
for (const [field, fieldIndex] of this.indices.entries()) {
allFields[field] = []
for (const [value, ids] of fieldIndex.entries()) {
allFields[field].push({ value, type: typeof value, ids: Array.from(ids) })
}
}
return allFields
}
/**
* Get statistics about the index
*/
public getStats(): {
indexedFields: number
totalValues: number
totalMappings: number
} {
let totalValues = 0
let totalMappings = 0
for (const fieldIndex of this.indices.values()) {
totalValues += fieldIndex.size
for (const ids of fieldIndex.values()) {
totalMappings += ids.size
}
}
return {
indexedFields: this.indexedFields.size,
totalValues,
totalMappings
}
}
/**
* Clear all indices
*/
public clear(): void {
this.indices.clear()
this.sortedIndices.clear()
this.indexedFields.clear()
}
}

View file

@ -2,7 +2,7 @@
* 🧠 BRAINY EMBEDDED PATTERNS
*
* AUTO-GENERATED - DO NOT EDIT
* Generated: 2025-08-25T19:56:48.296Z
* Generated: 2025-08-25T22:04:14.952Z
* Patterns: 220
* Coverage: 94-98% of all queries
*

View file

@ -67,18 +67,11 @@ export interface QueryStep {
*/
export class TripleIntelligenceEngine {
private brain: BrainyData
private queryHistory?: BrainyData // For self-optimization
private planCache = new Map<string, QueryPlan>()
constructor(brain: BrainyData, enableSelfOptimization = true) {
constructor(brain: BrainyData) {
this.brain = brain
if (enableSelfOptimization) {
// Brainy uses Brainy to optimize Brainy!
// But prevent infinite recursion by disabling writeOnly mode
this.queryHistory = new BrainyData({ writeOnly: true })
this.queryHistory.init()
}
// Query history removed - unnecessary complexity for minimal gain
}
/**
@ -112,10 +105,7 @@ export class TripleIntelligenceEngine {
results = this.addExplanations(results, plan, timing)
}
// Learn from this query for future optimization
if (this.queryHistory) {
await this.learnFromQuery(query, results, Date.now() - startTime)
}
// Query history removed - no learning needed
// Apply limit
if (query.limit) {
@ -197,10 +187,7 @@ export class TripleIntelligenceEngine {
}
}
// Learn from history if available
if (this.queryHistory) {
plan = await this.optimizeFromHistory(query, plan)
}
// Query history removed - use default plan
this.planCache.set(cacheKey, plan)
return plan
@ -335,40 +322,22 @@ export class TripleIntelligenceEngine {
* Field-based filtering
*/
private async fieldFilter(where: Record<string, any>): Promise<any[]> {
// Use BrainyData's advanced metadata filtering capabilities
// Convert Triple Intelligence 'where' clauses to metadata filter format
// Use BrainyData's advanced metadata filtering with Brain Patterns
if (!where || Object.keys(where).length === 0) {
return this.brain.search('*', 1000) // Return all if no filter
}
// Convert Brain Patterns (like {year: {greaterThan: 2020}}) to search metadata filters
const metadata: Record<string, any> = {}
// Pass Brain Patterns directly - the metadata index now supports them natively!
// Examples:
// { year: 2023 } - exact match
// { year: { greaterThan: 2020 } } - range query
// { year: { greaterThan: 2020, lessThan: 2025 } } - range with bounds
// { status: { in: ['active', 'pending'] } } - set membership
// { tags: { contains: 'javascript' } } - array contains
for (const [key, value] of Object.entries(where)) {
if (typeof value === 'object' && value !== null) {
// Handle Brain Pattern operators
if (value.greaterThan !== undefined) {
metadata[`${key}:`] = `>${value.greaterThan}`
} else if (value.greaterEqual !== undefined) {
metadata[`${key}:`] = `>=${value.greaterEqual}`
} else if (value.lessThan !== undefined) {
metadata[`${key}:`] = `<${value.lessThan}`
} else if (value.lessEqual !== undefined) {
metadata[`${key}:`] = `<=${value.lessEqual}`
} else if (value.equals !== undefined) {
metadata[key] = value.equals
} else {
// Direct object comparison
metadata[key] = value
}
} else {
// Direct value comparison
metadata[key] = value
}
}
return this.brain.search('*', 1000, { metadata })
// The metadata index handles all Brain Pattern operators natively now
return this.brain.search('*', 1000, { metadata: where })
}
/**
@ -597,55 +566,12 @@ export class TripleIntelligenceEngine {
}))
}
/**
* Learn from query execution for future optimization
*/
private async learnFromQuery(query: TripleQuery, results: TripleResult[], executionTime: number): Promise<void> {
if (!this.queryHistory) return
// Store query pattern and performance
await this.queryHistory.addNoun(
query, // Query itself becomes the vector
{
timestamp: Date.now(),
executionTime,
resultCount: results.length,
performance: Math.min(1.0, 100 / executionTime), // Faster = better
queryShape: {
hasVector: !!(query.like || query.similar),
hasGraph: !!query.connected,
hasField: !!query.where
}
}
)
}
// Query learning removed - unnecessary complexity
/**
* Optimize plan based on historical patterns
*/
private async optimizeFromHistory(query: TripleQuery, defaultPlan: QueryPlan): Promise<QueryPlan> {
if (!this.queryHistory) return defaultPlan
// Check if the brain is initialized before searching
if (!this.brain.initialized) return defaultPlan
// Find similar successful queries
const similar = await this.queryHistory.search(query, 5, {
metadata: { performance: { greaterThan: 0.8 } }
})
if (similar.length === 0) return defaultPlan
// Average the successful execution patterns
// This is simplified - real implementation would be more sophisticated
const successfulPlans = similar
.map(s => s.metadata.queryShape)
.filter(Boolean)
// For now, just return the default plan
// Real implementation would merge and optimize
return defaultPlan
}
// Query optimization from history removed
/**
* Execute single signal query without fusion
@ -676,7 +602,7 @@ export class TripleIntelligenceEngine {
getStats(): any {
return {
cachedPlans: this.planCache.size,
historySize: this.queryHistory ? this.queryHistory.size : 0
historySize: 0 // Query history removed
}
}
}

View file

@ -7,6 +7,7 @@
import { StorageAdapter } from '../coreTypes.js'
import { MetadataIndexCache, MetadataIndexCacheConfig } from './metadataIndexCache.js'
import { prodLog } from './logger.js'
import { getGlobalCache, UnifiedCache } from './unifiedCache.js'
export interface MetadataIndexEntry {
field: string
@ -41,6 +42,13 @@ export interface MetadataIndexConfig {
* Manages metadata indexes for fast filtering
* Maintains inverted indexes: field+value -> list of IDs
*/
// Sorted index for range queries
interface SortedFieldIndex {
values: Array<[value: any, ids: Set<string>]>
isDirty: boolean
fieldType: 'number' | 'string' | 'date' | 'mixed'
}
export class MetadataIndexManager {
private storage: StorageAdapter
private config: Required<MetadataIndexConfig>
@ -52,6 +60,13 @@ export class MetadataIndexManager {
private dirtyFields = new Set<string>()
private lastFlushTime = Date.now()
private autoFlushThreshold = 10 // Start with 10 for more frequent non-blocking flushes
// Sorted indices for range queries (only for numeric/date fields)
private sortedIndices = new Map<string, SortedFieldIndex>()
private numericFields = new Set<string>() // Track which fields are numeric
// Unified cache for coordinated memory management
private unifiedCache: UnifiedCache
constructor(storage: StorageAdapter, config: MetadataIndexConfig = {}) {
this.storage = storage
@ -69,6 +84,9 @@ export class MetadataIndexManager {
maxSize: 500, // 500 entries (field indexes + value chunks)
enabled: true
})
// Get global unified cache for coordinated memory management
this.unifiedCache = getGlobalCache()
}
/**
@ -78,6 +96,161 @@ export class MetadataIndexManager {
const normalizedValue = this.normalizeValue(value)
return `${field}:${normalizedValue}`
}
/**
* Ensure sorted index exists for a field (for range queries)
*/
private async ensureSortedIndex(field: string): Promise<void> {
if (!this.sortedIndices.has(field)) {
// Try to load from storage first
const loaded = await this.loadSortedIndex(field)
if (loaded) {
this.sortedIndices.set(field, loaded)
} else {
// Create new sorted index
this.sortedIndices.set(field, {
values: [],
isDirty: true,
fieldType: 'mixed'
})
}
}
}
/**
* Build sorted index for a field from hash index
*/
private async buildSortedIndex(field: string): Promise<void> {
const sortedIndex = this.sortedIndices.get(field)
if (!sortedIndex || !sortedIndex.isDirty) return
// Collect all values for this field from hash index
const valueMap = new Map<any, Set<string>>()
for (const [key, entry] of this.indexCache.entries()) {
if (entry.field === field) {
const existing = valueMap.get(entry.value)
if (existing) {
// Merge ID sets
entry.ids.forEach(id => existing.add(id))
} else {
valueMap.set(entry.value, new Set(entry.ids))
}
}
}
// Convert to sorted array
const sorted = Array.from(valueMap.entries())
// Detect field type and sort accordingly
if (sorted.length > 0) {
const sampleValue = sorted[0][0]
if (typeof sampleValue === 'number') {
sortedIndex.fieldType = 'number'
sorted.sort((a, b) => a[0] - b[0])
} else if (sampleValue instanceof Date) {
sortedIndex.fieldType = 'date'
sorted.sort((a, b) => a[0].getTime() - b[0].getTime())
} else {
sortedIndex.fieldType = 'string'
sorted.sort((a, b) => {
const aVal = String(a[0])
const bVal = String(b[0])
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0
})
}
}
sortedIndex.values = sorted
sortedIndex.isDirty = false
}
/**
* Binary search for range start (inclusive or exclusive)
*/
private binarySearchStart(sorted: Array<[any, Set<string>]>, target: any, inclusive: boolean): number {
let left = 0
let right = sorted.length - 1
let result = sorted.length
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midVal = sorted[mid][0]
if (inclusive ? midVal >= target : midVal > target) {
result = mid
right = mid - 1
} else {
left = mid + 1
}
}
return result
}
/**
* Binary search for range end (inclusive or exclusive)
*/
private binarySearchEnd(sorted: Array<[any, Set<string>]>, target: any, inclusive: boolean): number {
let left = 0
let right = sorted.length - 1
let result = -1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midVal = sorted[mid][0]
if (inclusive ? midVal <= target : midVal < target) {
result = mid
left = mid + 1
} else {
right = mid - 1
}
}
return result
}
/**
* Get IDs matching a range query
*/
private async getIdsForRange(
field: string,
min?: any,
max?: any,
includeMin: boolean = true,
includeMax: boolean = true
): Promise<string[]> {
// Ensure sorted index exists and is up to date
await this.ensureSortedIndex(field)
await this.buildSortedIndex(field)
const sortedIndex = this.sortedIndices.get(field)
if (!sortedIndex || sortedIndex.values.length === 0) return []
const sorted = sortedIndex.values
const resultSet = new Set<string>()
// Find range boundaries
let start = 0
let end = sorted.length - 1
if (min !== undefined) {
start = this.binarySearchStart(sorted, min, includeMin)
}
if (max !== undefined) {
end = this.binarySearchEnd(sorted, max, includeMax)
}
// Collect all IDs in range
for (let i = start; i <= end && i < sorted.length; i++) {
const [, ids] = sorted[i]
ids.forEach(id => resultSet.add(id))
}
return Array.from(resultSet)
}
/**
* Generate field index filename for filter discovery
@ -196,6 +369,14 @@ export class MetadataIndexManager {
async addToIndex(id: string, metadata: any, skipFlush: boolean = false): Promise<void> {
const fields = this.extractIndexableFields(metadata)
// Mark sorted indices as dirty when adding new data
for (const { field } of fields) {
const sortedIndex = this.sortedIndices.get(field)
if (sortedIndex) {
sortedIndex.isDirty = true
}
}
for (let i = 0; i < fields.length; i++) {
const { field, value } = fields[i]
const key = this.getIndexKey(field, value)
@ -463,7 +644,14 @@ export class MetadataIndexManager {
// For contains, the operand is the value we're looking for in an array field
criteria.push({ field: key, values: [operand] })
break
// For other operators, we can't use index efficiently, skip for now
case 'greaterThan':
case 'lessThan':
case 'greaterEqual':
case 'lessEqual':
case 'between':
// Range queries will be handled separately
// Sorted index will be created/loaded when needed in getIdsForRange
break
default:
break
}
@ -514,6 +702,146 @@ export class MetadataIndexManager {
return Array.from(unionIds)
}
// Process field filters with range support
const idSets: string[][] = []
for (const [field, condition] of Object.entries(filter)) {
// Skip logical operators
if (field === 'allOf' || field === 'anyOf' || field === 'not') continue
let fieldResults: string[] = []
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
// Handle Brainy Field Operators
for (const [op, operand] of Object.entries(condition)) {
switch (op) {
// Exact match operators
case 'equals':
case 'is':
case 'eq':
fieldResults = await this.getIds(field, operand)
break
// Multiple value operators
case 'oneOf':
case 'in':
if (Array.isArray(operand)) {
const unionIds = new Set<string>()
for (const value of operand) {
const ids = await this.getIds(field, value)
ids.forEach(id => unionIds.add(id))
}
fieldResults = Array.from(unionIds)
}
break
// Range operators
case 'greaterThan':
case 'gt':
fieldResults = await this.getIdsForRange(field, operand, undefined, false, true)
break
case 'greaterEqual':
case 'gte':
case 'greaterThanOrEqual':
fieldResults = await this.getIdsForRange(field, operand, undefined, true, true)
break
case 'lessThan':
case 'lt':
fieldResults = await this.getIdsForRange(field, undefined, operand, true, false)
break
case 'lessEqual':
case 'lte':
case 'lessThanOrEqual':
fieldResults = await this.getIdsForRange(field, undefined, operand, true, true)
break
case 'between':
if (Array.isArray(operand) && operand.length === 2) {
fieldResults = await this.getIdsForRange(field, operand[0], operand[1], true, true)
}
break
// Array contains operator
case 'contains':
fieldResults = await this.getIds(field, operand)
break
// Existence operator
case 'exists':
if (operand) {
// Get all IDs that have this field (any value)
const allIds = new Set<string>()
for (const [key, entry] of this.indexCache.entries()) {
if (entry.field === field) {
entry.ids.forEach(id => allIds.add(id))
}
}
fieldResults = Array.from(allIds)
}
break
}
}
} else {
// Direct value match (shorthand for equals)
fieldResults = await this.getIds(field, condition)
}
if (fieldResults.length > 0) {
idSets.push(fieldResults)
} else {
// If any field has no matches, intersection will be empty
return []
}
}
if (idSets.length === 0) return []
if (idSets.length === 1) return idSets[0]
// Intersection of all field criteria (implicit AND)
return idSets.reduce((intersection, currentSet) =>
intersection.filter(id => currentSet.includes(id))
)
}
/**
* DEPRECATED - Old implementation for backward compatibility
*/
private async getIdsForFilterOld(filter: any): Promise<string[]> {
if (!filter || Object.keys(filter).length === 0) {
return []
}
// Handle logical operators
if (filter.allOf && Array.isArray(filter.allOf)) {
// For allOf, we need intersection of all sub-filters
const allIds: string[][] = []
for (const subFilter of filter.allOf) {
const subIds = await this.getIdsForFilter(subFilter)
allIds.push(subIds)
}
if (allIds.length === 0) return []
if (allIds.length === 1) return allIds[0]
// Intersection of all sets
return allIds.reduce((intersection, currentSet) =>
intersection.filter(id => currentSet.includes(id))
)
}
if (filter.anyOf && Array.isArray(filter.anyOf)) {
// For anyOf, we need union of all sub-filters
const unionIds = new Set<string>()
for (const subFilter of filter.anyOf) {
const subIds = await this.getIdsForFilter(subFilter)
subIds.forEach(id => unionIds.add(id))
}
return Array.from(unionIds)
}
// Handle regular field filters
const criteria = this.convertFilterToCriteria(filter)
const idSets: string[][] = []
@ -548,7 +876,10 @@ export class MetadataIndexManager {
* Flush dirty entries to storage (non-blocking version)
*/
async flush(): Promise<void> {
if (this.dirtyEntries.size === 0 && this.dirtyFields.size === 0) {
// Check if we have anything to flush (including sorted indices)
const hasDirtySortedIndices = Array.from(this.sortedIndices.values()).some(idx => idx.isDirty)
if (this.dirtyEntries.size === 0 && this.dirtyFields.size === 0 && !hasDirtySortedIndices) {
return // Nothing to flush
}
@ -588,6 +919,13 @@ export class MetadataIndexManager {
}
}
// Flush sorted indices (for range queries)
for (const [field, sortedIndex] of this.sortedIndices.entries()) {
if (sortedIndex.isDirty) {
allPromises.push(this.saveSortedIndex(field, sortedIndex))
}
}
// Wait for all operations to complete
await Promise.all(allPromises)
@ -608,35 +946,47 @@ export class MetadataIndexManager {
* Load field index from storage
*/
private async loadFieldIndex(field: string): Promise<FieldIndexData | null> {
try {
const filename = this.getFieldIndexFilename(field)
const cacheKey = `field_index_${filename}`
// Check cache first
const cached = this.metadataCache.get(cacheKey)
if (cached) {
return cached
}
// Load from storage
const indexId = `__metadata_field_index__${filename}`
const data = await this.storage.getMetadata(indexId)
if (data) {
const fieldIndex = {
values: data.values || {},
lastUpdated: data.lastUpdated || Date.now()
const filename = this.getFieldIndexFilename(field)
const unifiedKey = `metadata:field:${filename}`
// Check unified cache first with loader function
return await this.unifiedCache.get(unifiedKey, async () => {
try {
const cacheKey = `field_index_${filename}`
// Check old cache for migration
const cached = this.metadataCache.get(cacheKey)
if (cached) {
// Add to unified cache
const size = JSON.stringify(cached).length
this.unifiedCache.set(unifiedKey, cached, 'metadata', size, 1) // Low rebuild cost
return cached
}
// Cache it
this.metadataCache.set(cacheKey, fieldIndex)
// Load from storage
const indexId = `__metadata_field_index__${filename}`
const data = await this.storage.getMetadata(indexId)
return fieldIndex
if (data) {
const fieldIndex = {
values: data.values || {},
lastUpdated: data.lastUpdated || Date.now()
}
// Add to unified cache
const size = JSON.stringify(fieldIndex).length
this.unifiedCache.set(unifiedKey, fieldIndex, 'metadata', size, 1)
// Also keep in old cache for now (transition period)
this.metadataCache.set(cacheKey, fieldIndex)
return fieldIndex
}
} catch (error) {
// Field index doesn't exist yet
}
} catch (error) {
// Field index doesn't exist yet
}
return null
return null
})
}
/**
@ -645,15 +995,80 @@ export class MetadataIndexManager {
private async saveFieldIndex(field: string, fieldIndex: FieldIndexData): Promise<void> {
const filename = this.getFieldIndexFilename(field)
const indexId = `__metadata_field_index__${filename}`
const unifiedKey = `metadata:field:${filename}`
await this.storage.saveMetadata(indexId, {
values: fieldIndex.values,
lastUpdated: fieldIndex.lastUpdated
})
// Invalidate cache
// Update unified cache
const size = JSON.stringify(fieldIndex).length
this.unifiedCache.set(unifiedKey, fieldIndex, 'metadata', size, 1)
// Invalidate old cache
this.metadataCache.invalidatePattern(`field_index_${filename}`)
}
/**
* Save sorted index to storage for range queries
*/
private async saveSortedIndex(field: string, sortedIndex: SortedFieldIndex): Promise<void> {
const filename = `sorted_${field}`
const indexId = `__metadata_sorted_index__${filename}`
const unifiedKey = `metadata:sorted:${field}`
// Convert Set to Array for serialization
const serializable = {
values: sortedIndex.values.map(([value, ids]) => [value, Array.from(ids)]),
fieldType: sortedIndex.fieldType,
lastUpdated: Date.now()
}
await this.storage.saveMetadata(indexId, serializable)
// Mark as clean
sortedIndex.isDirty = false
// Update unified cache (sorted indices are expensive to rebuild)
const size = JSON.stringify(serializable).length
this.unifiedCache.set(unifiedKey, sortedIndex, 'metadata', size, 100) // Higher rebuild cost
}
/**
* Load sorted index from storage
*/
private async loadSortedIndex(field: string): Promise<SortedFieldIndex | null> {
const filename = `sorted_${field}`
const indexId = `__metadata_sorted_index__${filename}`
const unifiedKey = `metadata:sorted:${field}`
// Check unified cache first
const cached = await this.unifiedCache.get(unifiedKey, async () => {
try {
const data = await this.storage.getMetadata(indexId)
if (data) {
// Convert Arrays back to Sets
const sortedIndex: SortedFieldIndex = {
values: data.values.map(([value, ids]: [any, string[]]) => [value, new Set(ids)]),
fieldType: data.fieldType || 'mixed',
isDirty: false
}
// Add to unified cache
const size = JSON.stringify(data).length
this.unifiedCache.set(unifiedKey, sortedIndex, 'metadata', size, 100)
return sortedIndex
}
} catch (error) {
// Sorted index doesn't exist yet
}
return null
})
return cached
}
/**
* Get index statistics
@ -859,32 +1274,44 @@ export class MetadataIndexManager {
* Load index entry from storage using safe filenames
*/
private async loadIndexEntry(key: string): Promise<MetadataIndexEntry | null> {
try {
// Extract field and value from key
const [field, value] = key.split(':', 2)
const filename = this.getValueChunkFilename(field, value)
// Load from metadata indexes directory with safe filename
const indexId = `__metadata_index__${filename}`
const data = await this.storage.getMetadata(indexId)
if (data) {
return {
field: data.field,
value: data.value,
ids: new Set(data.ids || []),
lastUpdated: data.lastUpdated || Date.now()
const unifiedKey = `metadata:entry:${key}`
// Use unified cache with loader function
return await this.unifiedCache.get(unifiedKey, async () => {
try {
// Extract field and value from key
const [field, value] = key.split(':', 2)
const filename = this.getValueChunkFilename(field, value)
// Load from metadata indexes directory with safe filename
const indexId = `__metadata_index__${filename}`
const data = await this.storage.getMetadata(indexId)
if (data) {
const entry = {
field: data.field,
value: data.value,
ids: new Set(data.ids || []),
lastUpdated: data.lastUpdated || Date.now()
}
// Add to unified cache (metadata entries are cheap to rebuild)
const size = JSON.stringify(Array.from(entry.ids)).length + 100
this.unifiedCache.set(unifiedKey, entry, 'metadata', size, 1)
return entry
}
} catch (error) {
// Index entry doesn't exist yet
}
} catch (error) {
// Index entry doesn't exist yet
}
return null
return null
})
}
/**
* Save index entry to storage using safe filenames
*/
private async saveIndexEntry(key: string, entry: MetadataIndexEntry): Promise<void> {
const unifiedKey = `metadata:entry:${key}`
const data = {
field: entry.field,
value: entry.value,
@ -899,17 +1326,25 @@ export class MetadataIndexManager {
// Store metadata indexes with safe filename
const indexId = `__metadata_index__${filename}`
await this.storage.saveMetadata(indexId, data)
// Update unified cache
const size = JSON.stringify(data.ids).length + 100
this.unifiedCache.set(unifiedKey, entry, 'metadata', size, 1)
}
/**
* Delete index entry from storage using safe filenames
*/
private async deleteIndexEntry(key: string): Promise<void> {
const unifiedKey = `metadata:entry:${key}`
try {
const [field, value] = key.split(':', 2)
const filename = this.getValueChunkFilename(field, value)
const indexId = `__metadata_index__${filename}`
await this.storage.saveMetadata(indexId, null)
// Remove from unified cache
this.unifiedCache.delete(unifiedKey)
} catch (error) {
// Entry might not exist
}

386
src/utils/unifiedCache.ts Normal file
View file

@ -0,0 +1,386 @@
/**
* UnifiedCache - Single cache for both HNSW and MetadataIndex
* Prevents resource competition with cost-aware eviction
*/
import { prodLog } from './logger.js'
export interface CacheItem {
key: string
type: 'hnsw' | 'metadata' | 'embedding' | 'other'
data: any
size: number
rebuildCost: number // milliseconds to rebuild
lastAccess: number
accessCount: number
}
export interface UnifiedCacheConfig {
maxSize?: number // bytes
enableRequestCoalescing?: boolean
enableFairnessCheck?: boolean
fairnessCheckInterval?: number // ms
persistPatterns?: boolean
}
export class UnifiedCache {
private cache = new Map<string, CacheItem>()
private access = new Map<string, number>() // Access counts
private loadingPromises = new Map<string, Promise<any>>()
private typeAccessCounts = { hnsw: 0, metadata: 0, embedding: 0, other: 0 }
private totalAccessCount = 0
private currentSize = 0
private readonly maxSize: number
private readonly config: UnifiedCacheConfig
constructor(config: UnifiedCacheConfig = {}) {
this.maxSize = config.maxSize || 2 * 1024 * 1024 * 1024 // 2GB default
this.config = {
enableRequestCoalescing: true,
enableFairnessCheck: true,
fairnessCheckInterval: 60000, // Check fairness every minute
persistPatterns: true,
...config
}
if (this.config.enableFairnessCheck) {
this.startFairnessMonitor()
}
}
/**
* Get item from cache with request coalescing
*/
async get(key: string, loadFn?: () => Promise<any>): Promise<any> {
// Update access tracking
this.access.set(key, (this.access.get(key) || 0) + 1)
this.totalAccessCount++
// Check if in cache
const item = this.cache.get(key)
if (item) {
item.lastAccess = Date.now()
item.accessCount++
this.typeAccessCounts[item.type]++
return item.data
}
// If no load function, return undefined
if (!loadFn) {
return undefined
}
// Request coalescing - prevent stampede
if (this.config.enableRequestCoalescing && this.loadingPromises.has(key)) {
prodLog.debug('Request coalescing for key:', key)
return this.loadingPromises.get(key)
}
// Load data
const loadPromise = loadFn()
if (this.config.enableRequestCoalescing) {
this.loadingPromises.set(key, loadPromise)
}
try {
const data = await loadPromise
return data
} finally {
if (this.config.enableRequestCoalescing) {
this.loadingPromises.delete(key)
}
}
}
/**
* Set item in cache with cost-aware eviction
*/
set(
key: string,
data: any,
type: 'hnsw' | 'metadata' | 'embedding' | 'other',
size: number,
rebuildCost: number = 1
): void {
// Make room if needed
while (this.currentSize + size > this.maxSize && this.cache.size > 0) {
this.evictLowestValue()
}
// Add to cache
const item: CacheItem = {
key,
type,
data,
size,
rebuildCost,
lastAccess: Date.now(),
accessCount: 1
}
// Update or add
const existing = this.cache.get(key)
if (existing) {
this.currentSize -= existing.size
}
this.cache.set(key, item)
this.currentSize += size
this.typeAccessCounts[type]++
this.totalAccessCount++
}
/**
* Evict item with lowest value (access count / rebuild cost)
*/
private evictLowestValue(): void {
let victim: string | null = null
let lowestScore = Infinity
for (const [key, item] of this.cache) {
// Calculate value score: access frequency / rebuild cost
const accessScore = (this.access.get(key) || 1)
const score = accessScore / Math.max(item.rebuildCost, 1)
if (score < lowestScore) {
lowestScore = score
victim = key
}
}
if (victim) {
const item = this.cache.get(victim)!
prodLog.debug(`Evicting ${victim} (type: ${item.type}, score: ${lowestScore})`)
this.currentSize -= item.size
this.cache.delete(victim)
// Keep access count for a while to prevent re-caching cold items
// this.access.delete(victim) // Don't delete immediately
}
}
/**
* Size-aware eviction - try to match needed size
*/
evictForSize(bytesNeeded: number): boolean {
const candidates: Array<[string, number, CacheItem]> = []
for (const [key, item] of this.cache) {
const score = (this.access.get(key) || 1) / item.rebuildCost
candidates.push([key, score, item])
}
// Sort by score (lower is worse)
candidates.sort((a, b) => a[1] - b[1])
let freedBytes = 0
const toEvict: string[] = []
// Try to free exactly what we need
for (const [key, , item] of candidates) {
toEvict.push(key)
freedBytes += item.size
if (freedBytes >= bytesNeeded) {
break
}
}
// Evict selected items
for (const key of toEvict) {
const item = this.cache.get(key)!
this.currentSize -= item.size
this.cache.delete(key)
}
return freedBytes >= bytesNeeded
}
/**
* Fairness monitoring - prevent one type from hogging cache
*/
private startFairnessMonitor(): void {
setInterval(() => {
this.checkFairness()
}, this.config.fairnessCheckInterval!)
}
private checkFairness(): void {
// Calculate type ratios in cache
const typeSizes = { hnsw: 0, metadata: 0, embedding: 0, other: 0 }
const typeCounts = { hnsw: 0, metadata: 0, embedding: 0, other: 0 }
for (const item of this.cache.values()) {
typeSizes[item.type] += item.size
typeCounts[item.type]++
}
// Calculate access ratios
const totalAccess = this.totalAccessCount || 1
const accessRatios = {
hnsw: this.typeAccessCounts.hnsw / totalAccess,
metadata: this.typeAccessCounts.metadata / totalAccess,
embedding: this.typeAccessCounts.embedding / totalAccess,
other: this.typeAccessCounts.other / totalAccess
}
// Calculate size ratios
const totalSize = this.currentSize || 1
const sizeRatios = {
hnsw: typeSizes.hnsw / totalSize,
metadata: typeSizes.metadata / totalSize,
embedding: typeSizes.embedding / totalSize,
other: typeSizes.other / totalSize
}
// Check for starvation (90% cache but <10% accesses)
for (const type of ['hnsw', 'metadata', 'embedding', 'other'] as const) {
if (sizeRatios[type] > 0.9 && accessRatios[type] < 0.1) {
prodLog.warn(`Type ${type} is hogging cache (${(sizeRatios[type] * 100).toFixed(1)}% size, ${(accessRatios[type] * 100).toFixed(1)}% access)`)
this.evictType(type)
}
}
}
/**
* Force evict items of a specific type
*/
private evictType(type: 'hnsw' | 'metadata' | 'embedding' | 'other'): void {
const candidates: Array<[string, number, CacheItem]> = []
for (const [key, item] of this.cache) {
if (item.type === type) {
const score = (this.access.get(key) || 1) / item.rebuildCost
candidates.push([key, score, item])
}
}
// Sort by score (lower is worse)
candidates.sort((a, b) => a[1] - b[1])
// Evict bottom 20% of this type
const evictCount = Math.max(1, Math.floor(candidates.length * 0.2))
for (let i = 0; i < evictCount && i < candidates.length; i++) {
const [key, , item] = candidates[i]
this.currentSize -= item.size
this.cache.delete(key)
prodLog.debug(`Fairness eviction: ${key} (type: ${type})`)
}
}
/**
* Delete specific item from cache
*/
delete(key: string): boolean {
const item = this.cache.get(key)
if (item) {
this.currentSize -= item.size
this.cache.delete(key)
return true
}
return false
}
/**
* Clear cache or specific type
*/
clear(type?: 'hnsw' | 'metadata' | 'embedding' | 'other'): void {
if (!type) {
this.cache.clear()
this.currentSize = 0
return
}
for (const [key, item] of this.cache) {
if (item.type === type) {
this.currentSize -= item.size
this.cache.delete(key)
}
}
}
/**
* Get cache statistics
*/
getStats() {
const typeSizes = { hnsw: 0, metadata: 0, embedding: 0, other: 0 }
const typeCounts = { hnsw: 0, metadata: 0, embedding: 0, other: 0 }
for (const item of this.cache.values()) {
typeSizes[item.type] += item.size
typeCounts[item.type]++
}
return {
totalSize: this.currentSize,
maxSize: this.maxSize,
utilization: this.currentSize / this.maxSize,
itemCount: this.cache.size,
typeSizes,
typeCounts,
typeAccessCounts: this.typeAccessCounts,
totalAccessCount: this.totalAccessCount,
hitRate: this.cache.size > 0 ?
Array.from(this.cache.values()).reduce((sum, item) => sum + item.accessCount, 0) / this.totalAccessCount : 0
}
}
/**
* Save access patterns for cold start optimization
*/
async saveAccessPatterns(): Promise<any> {
if (!this.config.persistPatterns) return
const patterns = Array.from(this.cache.entries())
.map(([key, item]) => ({
key,
type: item.type,
accessCount: this.access.get(key) || 0,
size: item.size,
rebuildCost: item.rebuildCost
}))
.sort((a, b) => b.accessCount - a.accessCount)
return {
patterns,
typeAccessCounts: this.typeAccessCounts,
timestamp: Date.now()
}
}
/**
* Load access patterns for warm start
*/
async loadAccessPatterns(patterns: any): Promise<void> {
if (!patterns?.patterns) return
// Pre-populate access counts
for (const pattern of patterns.patterns) {
this.access.set(pattern.key, pattern.accessCount)
}
// Restore type access counts
if (patterns.typeAccessCounts) {
this.typeAccessCounts = patterns.typeAccessCounts
}
prodLog.debug('Loaded access patterns:', patterns.patterns.length, 'items')
}
}
// Export singleton for global coordination
let globalCache: UnifiedCache | null = null
export function getGlobalCache(config?: UnifiedCacheConfig): UnifiedCache {
if (!globalCache) {
globalCache = new UnifiedCache(config)
}
return globalCache
}
export function clearGlobalCache(): void {
if (globalCache) {
globalCache.clear()
globalCache = null
}
}