2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* OPFS (Origin Private File System) Storage Adapter
|
|
|
|
|
* Provides persistent storage for the vector database using the Origin Private File System API
|
|
|
|
|
*/
|
|
|
|
|
|
2025-08-03 10:47:55 -07:00
|
|
|
import {GraphVerb, HNSWNoun, HNSWVerb, StatisticsData} from '../../coreTypes.js'
|
|
|
|
|
import {BaseStorage, NOUNS_DIR, VERBS_DIR, METADATA_DIR, NOUN_METADATA_DIR, VERB_METADATA_DIR, INDEX_DIR, STATISTICS_KEY} from '../baseStorage.js'
|
2025-07-21 12:46:41 -07:00
|
|
|
import '../../types/fileSystemTypes.js'
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
// Type alias for HNSWNode
|
|
|
|
|
type HNSWNode = HNSWNoun
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-03 10:47:55 -07:00
|
|
|
* Type alias for HNSWVerb to make the code more readable
|
2025-07-25 09:44:10 -07:00
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
type Edge = HNSWVerb
|
2025-07-25 09:44:10 -07:00
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Helper function to safely get a file from a FileSystemHandle
|
|
|
|
|
* This is needed because TypeScript doesn't recognize that a FileSystemHandle
|
|
|
|
|
* can be a FileSystemFileHandle which has the getFile method
|
|
|
|
|
*/
|
|
|
|
|
async function safeGetFile(handle: FileSystemHandle): Promise<File> {
|
|
|
|
|
// Type cast to any to avoid TypeScript error
|
|
|
|
|
return (handle as any).getFile()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Type aliases for better readability
|
2025-07-25 09:44:10 -07:00
|
|
|
type HNSWNoun_internal = HNSWNoun
|
|
|
|
|
type Verb = GraphVerb
|
2025-07-21 12:46:41 -07:00
|
|
|
|
|
|
|
|
// Root directory name for OPFS storage
|
|
|
|
|
const ROOT_DIR = 'opfs-vector-db'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* OPFS storage adapter for browser environments
|
|
|
|
|
* Uses the Origin Private File System API to store data persistently
|
|
|
|
|
*/
|
|
|
|
|
export class OPFSStorage extends BaseStorage {
|
|
|
|
|
private rootDir: FileSystemDirectoryHandle | null = null
|
|
|
|
|
private nounsDir: FileSystemDirectoryHandle | null = null
|
|
|
|
|
private verbsDir: FileSystemDirectoryHandle | null = null
|
|
|
|
|
private metadataDir: FileSystemDirectoryHandle | null = null
|
2025-08-03 10:47:55 -07:00
|
|
|
private nounMetadataDir: FileSystemDirectoryHandle | null = null
|
|
|
|
|
private verbMetadataDir: FileSystemDirectoryHandle | null = null
|
2025-07-21 12:46:41 -07:00
|
|
|
private indexDir: FileSystemDirectoryHandle | null = null
|
|
|
|
|
private isAvailable = false
|
|
|
|
|
private isPersistentRequested = false
|
|
|
|
|
private isPersistentGranted = false
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
private statistics: StatisticsData | null = null
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
private activeLocks: Set<string> = new Set()
|
|
|
|
|
private lockPrefix = 'opfs-lock-'
|
2025-07-21 12:46:41 -07:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super()
|
|
|
|
|
// Check if OPFS is available
|
|
|
|
|
this.isAvailable =
|
|
|
|
|
typeof navigator !== 'undefined' &&
|
|
|
|
|
'storage' in navigator &&
|
|
|
|
|
'getDirectory' in navigator.storage
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize the storage adapter
|
|
|
|
|
*/
|
|
|
|
|
public async init(): Promise<void> {
|
|
|
|
|
if (this.isInitialized) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.isAvailable) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'Origin Private File System is not available in this environment'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Get the root directory
|
|
|
|
|
const root = await navigator.storage.getDirectory()
|
|
|
|
|
|
|
|
|
|
// Create or get our app's root directory
|
|
|
|
|
this.rootDir = await root.getDirectoryHandle(ROOT_DIR, {create: true})
|
|
|
|
|
|
|
|
|
|
// Create or get nouns directory
|
|
|
|
|
this.nounsDir = await this.rootDir.getDirectoryHandle(NOUNS_DIR, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create or get verbs directory
|
|
|
|
|
this.verbsDir = await this.rootDir.getDirectoryHandle(VERBS_DIR, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create or get metadata directory
|
|
|
|
|
this.metadataDir = await this.rootDir.getDirectoryHandle(METADATA_DIR, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
2025-08-03 10:47:55 -07:00
|
|
|
// Create or get noun metadata directory
|
|
|
|
|
this.nounMetadataDir = await this.rootDir.getDirectoryHandle(NOUN_METADATA_DIR, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create or get verb metadata directory
|
|
|
|
|
this.verbMetadataDir = await this.rootDir.getDirectoryHandle(VERB_METADATA_DIR, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
// Create or get index directory
|
|
|
|
|
this.indexDir = await this.rootDir.getDirectoryHandle(INDEX_DIR, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.isInitialized = true
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to initialize OPFS storage:', error)
|
|
|
|
|
throw new Error(`Failed to initialize OPFS storage: ${error}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if OPFS is available in the current environment
|
|
|
|
|
*/
|
|
|
|
|
public isOPFSAvailable(): boolean {
|
|
|
|
|
return this.isAvailable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Request persistent storage permission from the user
|
|
|
|
|
* @returns Promise that resolves to true if permission was granted, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
public async requestPersistentStorage(): Promise<boolean> {
|
|
|
|
|
if (!this.isAvailable) {
|
|
|
|
|
console.warn('Cannot request persistent storage: OPFS is not available')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Check if persistence is already granted
|
|
|
|
|
this.isPersistentGranted = await navigator.storage.persisted()
|
|
|
|
|
|
|
|
|
|
if (!this.isPersistentGranted) {
|
|
|
|
|
// Request permission for persistent storage
|
|
|
|
|
this.isPersistentGranted = await navigator.storage.persist()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.isPersistentRequested = true
|
|
|
|
|
return this.isPersistentGranted
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to request persistent storage:', error)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if persistent storage is granted
|
|
|
|
|
* @returns Promise that resolves to true if persistent storage is granted, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
public async isPersistent(): Promise<boolean> {
|
|
|
|
|
if (!this.isAvailable) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.isPersistentGranted = await navigator.storage.persisted()
|
|
|
|
|
return this.isPersistentGranted
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to check persistent storage status:', error)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-25 09:44:10 -07:00
|
|
|
* Save a noun to storage
|
2025-07-21 12:46:41 -07:00
|
|
|
*/
|
2025-07-25 09:44:10 -07:00
|
|
|
protected async saveNoun_internal(noun: HNSWNoun_internal): Promise<void> {
|
2025-07-21 12:46:41 -07:00
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Convert connections Map to a serializable format
|
2025-07-25 09:44:10 -07:00
|
|
|
const serializableNoun = {
|
|
|
|
|
...noun,
|
|
|
|
|
connections: this.mapToObject(noun.connections, (set) =>
|
2025-07-21 12:46:41 -07:00
|
|
|
Array.from(set as Set<string>)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create or get the file for this noun
|
2025-07-25 09:44:10 -07:00
|
|
|
const fileHandle = await this.nounsDir!.getFileHandle(noun.id, {
|
2025-07-21 12:46:41 -07:00
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Write the noun data to the file
|
|
|
|
|
const writable = await fileHandle.createWritable()
|
2025-07-25 09:44:10 -07:00
|
|
|
await writable.write(JSON.stringify(serializableNoun))
|
2025-07-21 12:46:41 -07:00
|
|
|
await writable.close()
|
|
|
|
|
} catch (error) {
|
2025-07-25 09:44:10 -07:00
|
|
|
console.error(`Failed to save noun ${noun.id}:`, error)
|
|
|
|
|
throw new Error(`Failed to save noun ${noun.id}: ${error}`)
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-25 09:44:10 -07:00
|
|
|
* Get a noun from storage
|
2025-07-21 12:46:41 -07:00
|
|
|
*/
|
2025-07-25 09:44:10 -07:00
|
|
|
protected async getNoun_internal(id: string): Promise<HNSWNoun_internal | null> {
|
2025-07-21 12:46:41 -07:00
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
2025-07-25 09:44:10 -07:00
|
|
|
// Get the file handle for this noun
|
2025-07-21 12:46:41 -07:00
|
|
|
const fileHandle = await this.nounsDir!.getFileHandle(id)
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
// Read the noun data from the file
|
2025-07-21 12:46:41 -07:00
|
|
|
const file = await fileHandle.getFile()
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
const data = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
// Convert serialized connections back to Map<number, Set<string>>
|
|
|
|
|
const connections = new Map<number, Set<string>>()
|
2025-07-25 09:44:10 -07:00
|
|
|
for (const [level, nounIds] of Object.entries(data.connections)) {
|
|
|
|
|
connections.set(Number(level), new Set(nounIds as string[]))
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
vector: data.vector,
|
2025-08-04 20:00:38 -07:00
|
|
|
connections,
|
|
|
|
|
level: data.level || 0
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-07-25 09:44:10 -07:00
|
|
|
// Noun not found or other error
|
2025-07-21 12:46:41 -07:00
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-25 09:44:10 -07:00
|
|
|
* Get all nouns from storage
|
2025-07-21 12:46:41 -07:00
|
|
|
*/
|
2025-07-25 09:44:10 -07:00
|
|
|
protected async getAllNouns_internal(): Promise<HNSWNoun_internal[]> {
|
2025-07-21 12:46:41 -07:00
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
const allNouns: HNSWNoun_internal[] = []
|
2025-07-21 12:46:41 -07:00
|
|
|
try {
|
|
|
|
|
// Iterate through all files in the nouns directory
|
|
|
|
|
for await (const [name, handle] of this.nounsDir!.entries()) {
|
|
|
|
|
if (handle.kind === 'file') {
|
|
|
|
|
try {
|
2025-07-25 09:44:10 -07:00
|
|
|
// Read the noun data from the file
|
2025-07-21 12:46:41 -07:00
|
|
|
const file = await safeGetFile(handle)
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
const data = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
// Convert serialized connections back to Map<number, Set<string>>
|
|
|
|
|
const connections = new Map<number, Set<string>>()
|
2025-07-25 09:44:10 -07:00
|
|
|
for (const [level, nounIds] of Object.entries(data.connections)) {
|
|
|
|
|
connections.set(Number(level), new Set(nounIds as string[]))
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
allNouns.push({
|
2025-07-21 12:46:41 -07:00
|
|
|
id: data.id,
|
|
|
|
|
vector: data.vector,
|
2025-08-04 20:00:38 -07:00
|
|
|
connections,
|
|
|
|
|
level: data.level || 0
|
2025-07-21 12:46:41 -07:00
|
|
|
})
|
|
|
|
|
} catch (error) {
|
2025-07-25 09:44:10 -07:00
|
|
|
console.error(`Error reading noun file ${name}:`, error)
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error reading nouns directory:', error)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
return allNouns
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get nouns by noun type (internal implementation)
|
|
|
|
|
* @param nounType The noun type to filter by
|
|
|
|
|
* @returns Promise that resolves to an array of nouns of the specified noun type
|
|
|
|
|
*/
|
|
|
|
|
protected async getNounsByNounType_internal(nounType: string): Promise<HNSWNoun[]> {
|
|
|
|
|
return this.getNodesByNounType(nounType)
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get nodes by noun type
|
|
|
|
|
* @param nounType The noun type to filter by
|
|
|
|
|
* @returns Promise that resolves to an array of nodes of the specified noun type
|
|
|
|
|
*/
|
|
|
|
|
protected async getNodesByNounType(nounType: string): Promise<HNSWNode[]> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
const nodes: HNSWNode[] = []
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Iterate through all files in the nouns directory
|
|
|
|
|
for await (const [name, handle] of this.nounsDir!.entries()) {
|
|
|
|
|
if (handle.kind === 'file') {
|
|
|
|
|
try {
|
|
|
|
|
// Read the node data from the file
|
|
|
|
|
const file = await safeGetFile(handle)
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
const data = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
// Get the metadata to check the noun type
|
|
|
|
|
const metadata = await this.getMetadata(data.id)
|
|
|
|
|
|
|
|
|
|
// Include the node if its noun type matches the requested type
|
|
|
|
|
if (metadata && metadata.noun === nounType) {
|
|
|
|
|
// Convert serialized connections back to Map<number, Set<string>>
|
|
|
|
|
const connections = new Map<number, Set<string>>()
|
|
|
|
|
for (const [level, nodeIds] of Object.entries(data.connections)) {
|
|
|
|
|
connections.set(Number(level), new Set(nodeIds as string[]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nodes.push({
|
|
|
|
|
id: data.id,
|
|
|
|
|
vector: data.vector,
|
2025-08-04 20:00:38 -07:00
|
|
|
connections,
|
|
|
|
|
level: data.level || 0
|
2025-07-21 12:46:41 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error reading node file ${name}:`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error reading nouns directory:', error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nodes
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
/**
|
|
|
|
|
* Delete a noun from storage (internal implementation)
|
|
|
|
|
*/
|
|
|
|
|
protected async deleteNoun_internal(id: string): Promise<void> {
|
|
|
|
|
return this.deleteNode(id)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Delete a node from storage
|
|
|
|
|
*/
|
|
|
|
|
protected async deleteNode(id: string): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.nounsDir!.removeEntry(id)
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// Ignore NotFoundError, which means the file doesn't exist
|
|
|
|
|
if (error.name !== 'NotFoundError') {
|
|
|
|
|
console.error(`Error deleting node ${id}:`, error)
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
/**
|
|
|
|
|
* Save a verb to storage (internal implementation)
|
|
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
protected async saveVerb_internal(verb: HNSWVerb): Promise<void> {
|
2025-07-25 09:44:10 -07:00
|
|
|
return this.saveEdge(verb)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Save an edge to storage
|
|
|
|
|
*/
|
|
|
|
|
protected async saveEdge(edge: Edge): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Convert connections Map to a serializable format
|
|
|
|
|
const serializableEdge = {
|
|
|
|
|
...edge,
|
|
|
|
|
connections: this.mapToObject(edge.connections, (set) =>
|
|
|
|
|
Array.from(set as Set<string>)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create or get the file for this verb
|
|
|
|
|
const fileHandle = await this.verbsDir!.getFileHandle(edge.id, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Write the verb data to the file
|
|
|
|
|
const writable = await fileHandle.createWritable()
|
|
|
|
|
await writable.write(JSON.stringify(serializableEdge))
|
|
|
|
|
await writable.close()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Failed to save edge ${edge.id}:`, error)
|
|
|
|
|
throw new Error(`Failed to save edge ${edge.id}: ${error}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
/**
|
|
|
|
|
* Get a verb from storage (internal implementation)
|
|
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
protected async getVerb_internal(id: string): Promise<HNSWVerb | null> {
|
2025-07-25 09:44:10 -07:00
|
|
|
return this.getEdge(id)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Get an edge from storage
|
|
|
|
|
*/
|
|
|
|
|
protected async getEdge(id: string): Promise<Edge | null> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Get the file handle for this edge
|
|
|
|
|
const fileHandle = await this.verbsDir!.getFileHandle(id)
|
|
|
|
|
|
|
|
|
|
// Read the edge data from the file
|
|
|
|
|
const file = await fileHandle.getFile()
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
const data = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
// Convert serialized connections back to Map<number, Set<string>>
|
|
|
|
|
const connections = new Map<number, Set<string>>()
|
|
|
|
|
for (const [level, nodeIds] of Object.entries(data.connections)) {
|
|
|
|
|
connections.set(Number(level), new Set(nodeIds as string[]))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
// Create default timestamp if not present
|
|
|
|
|
const defaultTimestamp = {
|
|
|
|
|
seconds: Math.floor(Date.now() / 1000),
|
|
|
|
|
nanoseconds: (Date.now() % 1000) * 1000000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create default createdBy if not present
|
|
|
|
|
const defaultCreatedBy = {
|
|
|
|
|
augmentation: 'unknown',
|
|
|
|
|
version: '1.0'
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
vector: data.vector,
|
2025-08-03 10:47:55 -07:00
|
|
|
connections
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Edge not found or other error
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
/**
|
|
|
|
|
* Get all verbs from storage (internal implementation)
|
|
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
protected async getAllVerbs_internal(): Promise<HNSWVerb[]> {
|
2025-07-25 09:44:10 -07:00
|
|
|
return this.getAllEdges()
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Get all edges from storage
|
|
|
|
|
*/
|
|
|
|
|
protected async getAllEdges(): Promise<Edge[]> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
const allEdges: Edge[] = []
|
|
|
|
|
try {
|
|
|
|
|
// Iterate through all files in the verbs directory
|
|
|
|
|
for await (const [name, handle] of this.verbsDir!.entries()) {
|
|
|
|
|
if (handle.kind === 'file') {
|
|
|
|
|
try {
|
|
|
|
|
// Read the edge data from the file
|
|
|
|
|
const file = await safeGetFile(handle)
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
const data = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
// Convert serialized connections back to Map<number, Set<string>>
|
|
|
|
|
const connections = new Map<number, Set<string>>()
|
|
|
|
|
for (const [level, nodeIds] of Object.entries(data.connections)) {
|
|
|
|
|
connections.set(Number(level), new Set(nodeIds as string[]))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
// Create default timestamp if not present
|
|
|
|
|
const defaultTimestamp = {
|
|
|
|
|
seconds: Math.floor(Date.now() / 1000),
|
|
|
|
|
nanoseconds: (Date.now() % 1000) * 1000000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create default createdBy if not present
|
|
|
|
|
const defaultCreatedBy = {
|
|
|
|
|
augmentation: 'unknown',
|
|
|
|
|
version: '1.0'
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
allEdges.push({
|
|
|
|
|
id: data.id,
|
|
|
|
|
vector: data.vector,
|
2025-08-03 10:47:55 -07:00
|
|
|
connections
|
2025-07-21 12:46:41 -07:00
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error reading edge file ${name}:`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error reading verbs directory:', error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return allEdges
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 09:44:10 -07:00
|
|
|
/**
|
|
|
|
|
* Get verbs by source (internal implementation)
|
|
|
|
|
*/
|
|
|
|
|
protected async getVerbsBySource_internal(sourceId: string): Promise<GraphVerb[]> {
|
2025-08-03 10:47:55 -07:00
|
|
|
// This method is deprecated and would require loading metadata for each edge
|
|
|
|
|
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
|
|
|
console.warn('getVerbsBySource_internal is deprecated and not efficiently supported in new storage pattern')
|
|
|
|
|
return []
|
2025-07-25 09:44:10 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Get edges by source
|
|
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
protected async getEdgesBySource(sourceId: string): Promise<GraphVerb[]> {
|
|
|
|
|
// This method is deprecated and would require loading metadata for each edge
|
|
|
|
|
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
|
|
|
console.warn('getEdgesBySource is deprecated and not efficiently supported in new storage pattern')
|
|
|
|
|
return []
|
2025-07-25 09:44:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get verbs by target (internal implementation)
|
|
|
|
|
*/
|
|
|
|
|
protected async getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]> {
|
2025-08-03 10:47:55 -07:00
|
|
|
// This method is deprecated and would require loading metadata for each edge
|
|
|
|
|
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
|
|
|
console.warn('getVerbsByTarget_internal is deprecated and not efficiently supported in new storage pattern')
|
|
|
|
|
return []
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get edges by target
|
|
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
protected async getEdgesByTarget(targetId: string): Promise<GraphVerb[]> {
|
|
|
|
|
// This method is deprecated and would require loading metadata for each edge
|
|
|
|
|
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
|
|
|
console.warn('getEdgesByTarget is deprecated and not efficiently supported in new storage pattern')
|
|
|
|
|
return []
|
2025-07-25 09:44:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get verbs by type (internal implementation)
|
|
|
|
|
*/
|
|
|
|
|
protected async getVerbsByType_internal(type: string): Promise<GraphVerb[]> {
|
2025-08-03 10:47:55 -07:00
|
|
|
// This method is deprecated and would require loading metadata for each edge
|
|
|
|
|
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
|
|
|
console.warn('getVerbsByType_internal is deprecated and not efficiently supported in new storage pattern')
|
|
|
|
|
return []
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get edges by type
|
|
|
|
|
*/
|
2025-08-03 10:47:55 -07:00
|
|
|
protected async getEdgesByType(type: string): Promise<GraphVerb[]> {
|
|
|
|
|
// This method is deprecated and would require loading metadata for each edge
|
|
|
|
|
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
|
|
|
console.warn('getEdgesByType is deprecated and not efficiently supported in new storage pattern')
|
|
|
|
|
return []
|
2025-07-25 09:44:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a verb from storage (internal implementation)
|
|
|
|
|
*/
|
|
|
|
|
protected async deleteVerb_internal(id: string): Promise<void> {
|
|
|
|
|
return this.deleteEdge(id)
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete an edge from storage
|
|
|
|
|
*/
|
|
|
|
|
protected async deleteEdge(id: string): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.verbsDir!.removeEntry(id)
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// Ignore NotFoundError, which means the file doesn't exist
|
|
|
|
|
if (error.name !== 'NotFoundError') {
|
|
|
|
|
console.error(`Error deleting edge ${id}:`, error)
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save metadata to storage
|
|
|
|
|
*/
|
|
|
|
|
public async saveMetadata(id: string, metadata: any): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Create or get the file for this metadata
|
|
|
|
|
const fileHandle = await this.metadataDir!.getFileHandle(id, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Write the metadata to the file
|
|
|
|
|
const writable = await fileHandle.createWritable()
|
|
|
|
|
await writable.write(JSON.stringify(metadata))
|
|
|
|
|
await writable.close()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Failed to save metadata ${id}:`, error)
|
|
|
|
|
throw new Error(`Failed to save metadata ${id}: ${error}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get metadata from storage
|
|
|
|
|
*/
|
|
|
|
|
public async getMetadata(id: string): Promise<any | null> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Get the file handle for this metadata
|
|
|
|
|
const fileHandle = await this.metadataDir!.getFileHandle(id)
|
|
|
|
|
|
|
|
|
|
// Read the metadata from the file
|
|
|
|
|
const file = await fileHandle.getFile()
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
return JSON.parse(text)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Metadata not found or other error
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-03 10:47:55 -07:00
|
|
|
/**
|
|
|
|
|
* Save verb metadata to storage
|
|
|
|
|
*/
|
|
|
|
|
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
const fileName = `${id}.json`
|
|
|
|
|
const fileHandle = await (this.verbMetadataDir as FileSystemDirectoryHandle).getFileHandle(fileName, { create: true })
|
|
|
|
|
const writable = await (fileHandle as FileSystemFileHandle).createWritable()
|
|
|
|
|
await writable.write(JSON.stringify(metadata, null, 2))
|
|
|
|
|
await writable.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get verb metadata from storage
|
|
|
|
|
*/
|
|
|
|
|
public async getVerbMetadata(id: string): Promise<any | null> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
const fileName = `${id}.json`
|
|
|
|
|
try {
|
|
|
|
|
const fileHandle = await (this.verbMetadataDir as FileSystemDirectoryHandle).getFileHandle(fileName)
|
|
|
|
|
const file = await safeGetFile(fileHandle)
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
return JSON.parse(text)
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.name !== 'NotFoundError') {
|
|
|
|
|
console.error(`Error reading verb metadata ${id}:`, error)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save noun metadata to storage
|
|
|
|
|
*/
|
|
|
|
|
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
const fileName = `${id}.json`
|
|
|
|
|
const fileHandle = await (this.nounMetadataDir as FileSystemDirectoryHandle).getFileHandle(fileName, { create: true })
|
|
|
|
|
const writable = await fileHandle.createWritable()
|
|
|
|
|
await writable.write(JSON.stringify(metadata, null, 2))
|
|
|
|
|
await writable.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get noun metadata from storage
|
|
|
|
|
*/
|
|
|
|
|
public async getNounMetadata(id: string): Promise<any | null> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
const fileName = `${id}.json`
|
|
|
|
|
try {
|
|
|
|
|
const fileHandle = await (this.nounMetadataDir as FileSystemDirectoryHandle).getFileHandle(fileName)
|
|
|
|
|
const file = await safeGetFile(fileHandle)
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
return JSON.parse(text)
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.name !== 'NotFoundError') {
|
|
|
|
|
console.error(`Error reading noun metadata ${id}:`, error)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
/**
|
|
|
|
|
* Clear all data from storage
|
|
|
|
|
*/
|
|
|
|
|
public async clear(): Promise<void> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
// Helper function to remove all files in a directory
|
|
|
|
|
const removeDirectoryContents = async (
|
|
|
|
|
dirHandle: FileSystemDirectoryHandle
|
|
|
|
|
): Promise<void> => {
|
|
|
|
|
try {
|
|
|
|
|
for await (const [name, handle] of dirHandle.entries()) {
|
2025-07-30 11:25:36 -07:00
|
|
|
// Use recursive option to handle directories that may contain files
|
|
|
|
|
await dirHandle.removeEntry(name, { recursive: true })
|
2025-07-21 12:46:41 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error removing directory contents:`, error)
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Remove all files in the nouns directory
|
|
|
|
|
await removeDirectoryContents(this.nounsDir!)
|
|
|
|
|
|
|
|
|
|
// Remove all files in the verbs directory
|
|
|
|
|
await removeDirectoryContents(this.verbsDir!)
|
|
|
|
|
|
|
|
|
|
// Remove all files in the metadata directory
|
|
|
|
|
await removeDirectoryContents(this.metadataDir!)
|
|
|
|
|
|
2025-08-03 10:47:55 -07:00
|
|
|
// Remove all files in the noun metadata directory
|
|
|
|
|
await removeDirectoryContents(this.nounMetadataDir!)
|
|
|
|
|
|
|
|
|
|
// Remove all files in the verb metadata directory
|
|
|
|
|
await removeDirectoryContents(this.verbMetadataDir!)
|
|
|
|
|
|
2025-07-21 12:46:41 -07:00
|
|
|
// Remove all files in the index directory
|
|
|
|
|
await removeDirectoryContents(this.indexDir!)
|
2025-08-04 20:00:38 -07:00
|
|
|
|
|
|
|
|
// Clear the statistics cache
|
|
|
|
|
this.statisticsCache = null
|
|
|
|
|
this.statisticsModified = false
|
2025-07-21 12:46:41 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error clearing storage:', error)
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get information about storage usage and capacity
|
|
|
|
|
*/
|
|
|
|
|
public async getStorageStatus(): Promise<{
|
|
|
|
|
type: string
|
|
|
|
|
used: number
|
|
|
|
|
quota: number | null
|
|
|
|
|
details?: Record<string, any>
|
|
|
|
|
}> {
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Calculate the total size of all files in the storage directories
|
|
|
|
|
let totalSize = 0
|
|
|
|
|
|
|
|
|
|
// Helper function to calculate directory size
|
|
|
|
|
const calculateDirSize = async (
|
|
|
|
|
dirHandle: FileSystemDirectoryHandle
|
|
|
|
|
): Promise<number> => {
|
|
|
|
|
let size = 0
|
|
|
|
|
try {
|
|
|
|
|
for await (const [name, handle] of dirHandle.entries()) {
|
|
|
|
|
if (handle.kind === 'file') {
|
|
|
|
|
const file = await (handle as FileSystemFileHandle).getFile()
|
|
|
|
|
size += file.size
|
|
|
|
|
} else if (handle.kind === 'directory') {
|
|
|
|
|
size += await calculateDirSize(
|
|
|
|
|
handle as FileSystemDirectoryHandle
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(`Error calculating size for directory:`, error)
|
|
|
|
|
}
|
|
|
|
|
return size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to count files in a directory
|
|
|
|
|
const countFilesInDirectory = async (
|
|
|
|
|
dirHandle: FileSystemDirectoryHandle
|
|
|
|
|
): Promise<number> => {
|
|
|
|
|
let count = 0
|
|
|
|
|
try {
|
|
|
|
|
for await (const [name, handle] of dirHandle.entries()) {
|
|
|
|
|
if (handle.kind === 'file') {
|
|
|
|
|
count++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(`Error counting files in directory:`, error)
|
|
|
|
|
}
|
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate size for each directory
|
|
|
|
|
if (this.nounsDir) {
|
|
|
|
|
totalSize += await calculateDirSize(this.nounsDir)
|
|
|
|
|
}
|
|
|
|
|
if (this.verbsDir) {
|
|
|
|
|
totalSize += await calculateDirSize(this.verbsDir)
|
|
|
|
|
}
|
|
|
|
|
if (this.metadataDir) {
|
|
|
|
|
totalSize += await calculateDirSize(this.metadataDir)
|
|
|
|
|
}
|
|
|
|
|
if (this.indexDir) {
|
|
|
|
|
totalSize += await calculateDirSize(this.indexDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get storage quota information using the Storage API
|
|
|
|
|
let quota = null
|
|
|
|
|
let details: Record<string, any> = {
|
|
|
|
|
isPersistent: await this.isPersistent(),
|
|
|
|
|
nounTypes: {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (navigator.storage && navigator.storage.estimate) {
|
|
|
|
|
const estimate = await navigator.storage.estimate()
|
|
|
|
|
quota = estimate.quota || null
|
|
|
|
|
details = {
|
|
|
|
|
...details,
|
|
|
|
|
usage: estimate.usage,
|
|
|
|
|
quota: estimate.quota,
|
|
|
|
|
freePercentage: estimate.quota
|
|
|
|
|
? ((estimate.quota - (estimate.usage || 0)) / estimate.quota) *
|
|
|
|
|
100
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Unable to get storage estimate:', error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Count files in each directory
|
|
|
|
|
if (this.nounsDir) {
|
|
|
|
|
details.nounsCount = await countFilesInDirectory(this.nounsDir)
|
|
|
|
|
}
|
|
|
|
|
if (this.verbsDir) {
|
|
|
|
|
details.verbsCount = await countFilesInDirectory(this.verbsDir)
|
|
|
|
|
}
|
|
|
|
|
if (this.metadataDir) {
|
|
|
|
|
details.metadataCount = await countFilesInDirectory(this.metadataDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Count nouns by type using metadata
|
|
|
|
|
const nounTypeCounts: Record<string, number> = {}
|
|
|
|
|
if (this.metadataDir) {
|
|
|
|
|
for await (const [name, handle] of this.metadataDir.entries()) {
|
|
|
|
|
if (handle.kind === 'file') {
|
|
|
|
|
try {
|
|
|
|
|
const file = await safeGetFile(handle)
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
const metadata = JSON.parse(text)
|
|
|
|
|
if (metadata.noun) {
|
|
|
|
|
nounTypeCounts[metadata.noun] = (nounTypeCounts[metadata.noun] || 0) + 1
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error reading metadata file ${name}:`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
details.nounTypes = nounTypeCounts
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: 'opfs',
|
|
|
|
|
used: totalSize,
|
|
|
|
|
quota,
|
|
|
|
|
details
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to get storage status:', error)
|
|
|
|
|
return {
|
|
|
|
|
type: 'opfs',
|
|
|
|
|
used: 0,
|
|
|
|
|
quota: null,
|
|
|
|
|
details: {error: String(error)}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
/**
|
|
|
|
|
* Get the statistics key for a specific date
|
|
|
|
|
* @param date The date to get the key for
|
|
|
|
|
* @returns The statistics key for the specified date
|
|
|
|
|
*/
|
|
|
|
|
private getStatisticsKeyForDate(date: Date): string {
|
|
|
|
|
const year = date.getUTCFullYear()
|
|
|
|
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
|
|
|
|
const day = String(date.getUTCDate()).padStart(2, '0')
|
|
|
|
|
return `statistics_${year}${month}${day}.json`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the current statistics key
|
|
|
|
|
* @returns The current statistics key
|
|
|
|
|
*/
|
|
|
|
|
private getCurrentStatisticsKey(): string {
|
|
|
|
|
return this.getStatisticsKeyForDate(new Date())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the legacy statistics key (for backward compatibility)
|
|
|
|
|
* @returns The legacy statistics key
|
|
|
|
|
*/
|
|
|
|
|
private getLegacyStatisticsKey(): string {
|
|
|
|
|
return 'statistics.json'
|
|
|
|
|
}
|
|
|
|
|
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
/**
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
* Acquire a browser-based lock for coordinating operations across multiple tabs
|
|
|
|
|
* @param lockKey The key to lock on
|
|
|
|
|
* @param ttl Time to live for the lock in milliseconds (default: 30 seconds)
|
|
|
|
|
* @returns Promise that resolves to true if lock was acquired, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
private async acquireLock(lockKey: string, ttl: number = 30000): Promise<boolean> {
|
|
|
|
|
if (typeof localStorage === 'undefined') {
|
|
|
|
|
console.warn('localStorage not available, proceeding without lock')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lockStorageKey = `${this.lockPrefix}${lockKey}`
|
|
|
|
|
const lockValue = `${Date.now()}_${Math.random()}_${window.location.href}`
|
|
|
|
|
const expiresAt = Date.now() + ttl
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Check if lock already exists and is still valid
|
|
|
|
|
const existingLock = localStorage.getItem(lockStorageKey)
|
|
|
|
|
if (existingLock) {
|
|
|
|
|
try {
|
|
|
|
|
const lockInfo = JSON.parse(existingLock)
|
|
|
|
|
if (lockInfo.expiresAt > Date.now()) {
|
|
|
|
|
// Lock exists and is still valid
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Invalid lock data, we can proceed to create a new lock
|
|
|
|
|
console.warn(`Invalid lock data for ${lockStorageKey}:`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to create the lock
|
|
|
|
|
const lockInfo = {
|
|
|
|
|
lockValue,
|
|
|
|
|
expiresAt,
|
|
|
|
|
tabId: window.location.href,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localStorage.setItem(lockStorageKey, JSON.stringify(lockInfo))
|
|
|
|
|
|
|
|
|
|
// Add to active locks for cleanup
|
|
|
|
|
this.activeLocks.add(lockKey)
|
|
|
|
|
|
|
|
|
|
// Schedule automatic cleanup when lock expires
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.releaseLock(lockKey, lockValue).catch(error => {
|
|
|
|
|
console.warn(`Failed to auto-release expired lock ${lockKey}:`, error)
|
|
|
|
|
})
|
|
|
|
|
}, ttl)
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(`Failed to acquire lock ${lockKey}:`, error)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Release a browser-based lock
|
|
|
|
|
* @param lockKey The key to unlock
|
|
|
|
|
* @param lockValue The value used when acquiring the lock (for verification)
|
|
|
|
|
* @returns Promise that resolves when lock is released
|
|
|
|
|
*/
|
|
|
|
|
private async releaseLock(lockKey: string, lockValue?: string): Promise<void> {
|
|
|
|
|
if (typeof localStorage === 'undefined') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lockStorageKey = `${this.lockPrefix}${lockKey}`
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// If lockValue is provided, verify it matches before releasing
|
|
|
|
|
if (lockValue) {
|
|
|
|
|
const existingLock = localStorage.getItem(lockStorageKey)
|
|
|
|
|
if (existingLock) {
|
|
|
|
|
try {
|
|
|
|
|
const lockInfo = JSON.parse(existingLock)
|
|
|
|
|
if (lockInfo.lockValue !== lockValue) {
|
|
|
|
|
// Lock was acquired by someone else, don't release it
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Invalid lock data, remove it
|
|
|
|
|
localStorage.removeItem(lockStorageKey)
|
|
|
|
|
this.activeLocks.delete(lockKey)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove the lock
|
|
|
|
|
localStorage.removeItem(lockStorageKey)
|
|
|
|
|
|
|
|
|
|
// Remove from active locks
|
|
|
|
|
this.activeLocks.delete(lockKey)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(`Failed to release lock ${lockKey}:`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clean up expired locks from localStorage
|
|
|
|
|
*/
|
|
|
|
|
private async cleanupExpiredLocks(): Promise<void> {
|
|
|
|
|
if (typeof localStorage === 'undefined') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
const keysToRemove: string[] = []
|
|
|
|
|
|
|
|
|
|
// Iterate through localStorage to find expired locks
|
|
|
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
|
|
|
const key = localStorage.key(i)
|
|
|
|
|
if (key && key.startsWith(this.lockPrefix)) {
|
|
|
|
|
try {
|
|
|
|
|
const lockData = localStorage.getItem(key)
|
|
|
|
|
if (lockData) {
|
|
|
|
|
const lockInfo = JSON.parse(lockData)
|
|
|
|
|
if (lockInfo.expiresAt <= now) {
|
|
|
|
|
keysToRemove.push(key)
|
|
|
|
|
const lockKey = key.replace(this.lockPrefix, '')
|
|
|
|
|
this.activeLocks.delete(lockKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Invalid lock data, mark for removal
|
|
|
|
|
keysToRemove.push(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove expired locks
|
|
|
|
|
keysToRemove.forEach(key => {
|
|
|
|
|
localStorage.removeItem(key)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (keysToRemove.length > 0) {
|
|
|
|
|
console.log(`Cleaned up ${keysToRemove.length} expired locks`)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to cleanup expired locks:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save statistics data to storage with browser-based locking
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
* @param statistics The statistics data to save
|
|
|
|
|
*/
|
|
|
|
|
protected async saveStatisticsData(statistics: StatisticsData): Promise<void> {
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
const lockKey = 'statistics'
|
|
|
|
|
const lockAcquired = await this.acquireLock(lockKey, 10000) // 10 second timeout
|
|
|
|
|
|
|
|
|
|
if (!lockAcquired) {
|
|
|
|
|
console.warn('Failed to acquire lock for statistics update, proceeding without lock')
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
}
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
try {
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
// Get existing statistics to merge with new data
|
|
|
|
|
const existingStats = await this.getStatisticsData()
|
|
|
|
|
|
|
|
|
|
let mergedStats: StatisticsData
|
|
|
|
|
if (existingStats) {
|
|
|
|
|
// Merge statistics data
|
|
|
|
|
mergedStats = {
|
|
|
|
|
nounCount: {
|
|
|
|
|
...existingStats.nounCount,
|
|
|
|
|
...statistics.nounCount
|
|
|
|
|
},
|
|
|
|
|
verbCount: {
|
|
|
|
|
...existingStats.verbCount,
|
|
|
|
|
...statistics.verbCount
|
|
|
|
|
},
|
|
|
|
|
metadataCount: {
|
|
|
|
|
...existingStats.metadataCount,
|
|
|
|
|
...statistics.metadataCount
|
|
|
|
|
},
|
|
|
|
|
hnswIndexSize: Math.max(statistics.hnswIndexSize || 0, existingStats.hnswIndexSize || 0),
|
|
|
|
|
lastUpdated: new Date().toISOString()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No existing statistics, use new ones
|
|
|
|
|
mergedStats = {
|
|
|
|
|
...statistics,
|
|
|
|
|
lastUpdated: new Date().toISOString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a deep copy to avoid reference issues
|
|
|
|
|
this.statistics = {
|
|
|
|
|
nounCount: {...mergedStats.nounCount},
|
|
|
|
|
verbCount: {...mergedStats.verbCount},
|
|
|
|
|
metadataCount: {...mergedStats.metadataCount},
|
|
|
|
|
hnswIndexSize: mergedStats.hnswIndexSize,
|
|
|
|
|
lastUpdated: mergedStats.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
// Ensure the root directory is initialized
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
// Get or create the index directory
|
|
|
|
|
if (!this.indexDir) {
|
|
|
|
|
throw new Error('Index directory not initialized')
|
|
|
|
|
}
|
|
|
|
|
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
// Get the current statistics key
|
|
|
|
|
const currentKey = this.getCurrentStatisticsKey()
|
|
|
|
|
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
// Create a file for the statistics data
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
const fileHandle = await this.indexDir.getFileHandle(currentKey, {
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create a writable stream
|
|
|
|
|
const writable = await fileHandle.createWritable()
|
|
|
|
|
|
|
|
|
|
// Write the statistics data to the file
|
|
|
|
|
await writable.write(JSON.stringify(this.statistics, null, 2))
|
|
|
|
|
|
|
|
|
|
// Close the stream
|
|
|
|
|
await writable.close()
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
|
|
|
|
|
// Also update the legacy key for backward compatibility, but less frequently
|
|
|
|
|
if (Math.random() < 0.1) {
|
|
|
|
|
const legacyKey = this.getLegacyStatisticsKey()
|
|
|
|
|
const legacyFileHandle = await this.indexDir.getFileHandle(legacyKey, {
|
|
|
|
|
create: true
|
|
|
|
|
})
|
|
|
|
|
const legacyWritable = await legacyFileHandle.createWritable()
|
|
|
|
|
await legacyWritable.write(JSON.stringify(this.statistics, null, 2))
|
|
|
|
|
await legacyWritable.close()
|
|
|
|
|
}
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to save statistics data:', error)
|
|
|
|
|
throw new Error(`Failed to save statistics data: ${error}`)
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
} finally {
|
|
|
|
|
if (lockAcquired) {
|
|
|
|
|
await this.releaseLock(lockKey)
|
|
|
|
|
}
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get statistics data from storage
|
|
|
|
|
* @returns Promise that resolves to the statistics data or null if not found
|
|
|
|
|
*/
|
|
|
|
|
protected async getStatisticsData(): Promise<StatisticsData | null> {
|
|
|
|
|
// If we have cached statistics, return a deep copy
|
|
|
|
|
if (this.statistics) {
|
|
|
|
|
return {
|
|
|
|
|
nounCount: {...this.statistics.nounCount},
|
|
|
|
|
verbCount: {...this.statistics.verbCount},
|
|
|
|
|
metadataCount: {...this.statistics.metadataCount},
|
|
|
|
|
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: this.statistics.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Ensure the root directory is initialized
|
|
|
|
|
await this.ensureInitialized()
|
|
|
|
|
|
|
|
|
|
if (!this.indexDir) {
|
|
|
|
|
throw new Error('Index directory not initialized')
|
|
|
|
|
}
|
|
|
|
|
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
// First try to get statistics from today's file
|
|
|
|
|
const currentKey = this.getCurrentStatisticsKey()
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
try {
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
const fileHandle = await this.indexDir.getFileHandle(currentKey, {
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
create: false
|
|
|
|
|
})
|
|
|
|
|
const file = await fileHandle.getFile()
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
this.statistics = JSON.parse(text)
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
if (this.statistics) {
|
|
|
|
|
return {
|
|
|
|
|
nounCount: {...this.statistics.nounCount},
|
|
|
|
|
verbCount: {...this.statistics.verbCount},
|
|
|
|
|
metadataCount: {...this.statistics.metadataCount},
|
|
|
|
|
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: this.statistics.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
// If today's file doesn't exist, try yesterday's file
|
|
|
|
|
const yesterday = new Date()
|
|
|
|
|
yesterday.setDate(yesterday.getDate() - 1)
|
|
|
|
|
const yesterdayKey = this.getStatisticsKeyForDate(yesterday)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const fileHandle = await this.indexDir.getFileHandle(yesterdayKey, {
|
|
|
|
|
create: false
|
|
|
|
|
})
|
|
|
|
|
const file = await fileHandle.getFile()
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
this.statistics = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
if (this.statistics) {
|
|
|
|
|
return {
|
|
|
|
|
nounCount: {...this.statistics.nounCount},
|
|
|
|
|
verbCount: {...this.statistics.verbCount},
|
|
|
|
|
metadataCount: {...this.statistics.metadataCount},
|
|
|
|
|
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: this.statistics.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// If yesterday's file doesn't exist, try the legacy file
|
|
|
|
|
const legacyKey = this.getLegacyStatisticsKey()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const fileHandle = await this.indexDir.getFileHandle(legacyKey, {
|
|
|
|
|
create: false
|
|
|
|
|
})
|
|
|
|
|
const file = await fileHandle.getFile()
|
|
|
|
|
const text = await file.text()
|
|
|
|
|
this.statistics = JSON.parse(text)
|
|
|
|
|
|
|
|
|
|
if (this.statistics) {
|
|
|
|
|
return {
|
|
|
|
|
nounCount: {...this.statistics.nounCount},
|
|
|
|
|
verbCount: {...this.statistics.verbCount},
|
|
|
|
|
metadataCount: {...this.statistics.metadataCount},
|
|
|
|
|
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: this.statistics.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// If the legacy file doesn't exist either, return null
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
}
|
**feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.
**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00
|
|
|
|
|
|
|
|
// If we get here and statistics is null, return default statistics
|
|
|
|
|
return this.statistics ? this.statistics : null
|
**feat(core, storage, tests): add service-level statistics tracking and storage adapter enhancements**
- **Core**: Enhanced `getStatistics` function to support `service` and `service[]` filters, enabling statistics breakdown by service. Modified return structure to include `serviceBreakdown` for detailed insights.
- **Storage**: Implemented a new `BaseStorageAdapter` abstract class to centralize statistics-related functionality, such as incrementing/decrementing counters and updating HNSW index size. Refactored all storage adapters (`FileSystemStorage`, `S3CompatibleStorage`, `MemoryStorage`, `OPFSStorage`) to extend `BaseStorageAdapter`, ensuring consistent statistics tracking.
- **Tests**: Added new test cases in `statistics.test.ts` to validate service-level statistics tracking, breakdown accuracy, and multi-service filtering.
**Purpose**: Improve insight into data trends by tracking service-specific usage in statistics. Enhance maintainability and consistency through storage adapter centralization and robust testing.
2025-07-24 11:35:52 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to get statistics data:', error)
|
|
|
|
|
throw new Error(`Failed to get statistics data: ${error}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
**docs: add detailed concurrency analysis and implementation documentation**
- Introduced `CONCURRENCY_ANALYSIS.md` to outline identified concurrency issues, including statistics handling, index synchronization, and storage contention.
- Added `CONCURRENCY_IMPLEMENTATION_SUMMARY.md` to summarize concurrency improvements, such as distributed locking and change log mechanisms.
- Created `STORAGE_CONCURRENCY_ANALYSIS.md` to evaluate concurrency risks and applied solutions for different storage adapters (`S3CompatibleStorage`, `FileSystemStorage`, `OPFSStorage`, and `MemoryStorage`).
- Updated codebase with changes related to concurrency, including distributed locking, atomic updates, event-driven synchronization, and change log support.
- Refactored tests to verify behavior of new concurrency mechanisms, including robust error handling and cleanup functions.
**Purpose**: Provides comprehensive documentation and implementation details to ensure robust concurrency handling in multi-instance, high-throughput environments.
2025-07-30 11:01:24 -07:00
|
|
|
}
|