@soulcraft/brainy 0.40.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +605 -194
- package/dist/augmentations/conduitAugmentations.js +1158 -0
- package/dist/augmentations/conduitAugmentations.js.map +1 -0
- package/dist/augmentations/memoryAugmentations.d.ts +2 -0
- package/dist/augmentations/memoryAugmentations.d.ts.map +1 -1
- package/dist/augmentations/memoryAugmentations.js +270 -0
- package/dist/augmentations/memoryAugmentations.js.map +1 -0
- package/dist/augmentations/serverSearchAugmentations.js +531 -0
- package/dist/augmentations/serverSearchAugmentations.js.map +1 -0
- package/dist/browserFramework.d.ts +15 -0
- package/dist/demo.d.ts +106 -0
- package/dist/examples/basicUsage.js +118 -0
- package/dist/examples/basicUsage.js.map +1 -0
- package/dist/hnsw/distributedSearch.js +452 -0
- package/dist/hnsw/distributedSearch.js.map +1 -0
- package/dist/hnsw/hnswIndex.js +602 -0
- package/dist/hnsw/hnswIndex.js.map +1 -0
- package/dist/hnsw/hnswIndexOptimized.js +471 -0
- package/dist/hnsw/hnswIndexOptimized.js.map +1 -0
- package/dist/hnsw/optimizedHNSWIndex.js +313 -0
- package/dist/hnsw/optimizedHNSWIndex.js.map +1 -0
- package/dist/hnsw/partitionedHNSWIndex.js +304 -0
- package/dist/hnsw/partitionedHNSWIndex.js.map +1 -0
- package/dist/hnsw/scaledHNSWSystem.js +559 -0
- package/dist/hnsw/scaledHNSWSystem.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +81 -0
- package/dist/mcp/brainyMCPAdapter.js +142 -0
- package/dist/mcp/brainyMCPAdapter.js.map +1 -0
- package/dist/mcp/brainyMCPService.js +248 -0
- package/dist/mcp/brainyMCPService.js.map +1 -0
- package/dist/mcp/index.js +17 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcpAugmentationToolset.js +180 -0
- package/dist/mcp/mcpAugmentationToolset.js.map +1 -0
- package/dist/storage/adapters/baseStorageAdapter.js +349 -0
- package/dist/storage/adapters/baseStorageAdapter.js.map +1 -0
- package/dist/storage/adapters/batchS3Operations.js +287 -0
- package/dist/storage/adapters/batchS3Operations.js.map +1 -0
- package/dist/storage/adapters/fileSystemStorage.js +846 -0
- package/dist/storage/adapters/fileSystemStorage.js.map +1 -0
- package/dist/storage/adapters/memoryStorage.js +532 -0
- package/dist/storage/adapters/memoryStorage.js.map +1 -0
- package/dist/storage/adapters/opfsStorage.d.ts.map +1 -1
- package/dist/storage/adapters/opfsStorage.js +1118 -0
- package/dist/storage/adapters/opfsStorage.js.map +1 -0
- package/dist/storage/adapters/optimizedS3Search.d.ts +79 -0
- package/dist/storage/adapters/optimizedS3Search.d.ts.map +1 -0
- package/dist/storage/adapters/optimizedS3Search.js +248 -0
- package/dist/storage/adapters/optimizedS3Search.js.map +1 -0
- package/dist/storage/adapters/s3CompatibleStorage.d.ts +21 -0
- package/dist/storage/adapters/s3CompatibleStorage.d.ts.map +1 -1
- package/dist/storage/adapters/s3CompatibleStorage.js +2026 -0
- package/dist/storage/adapters/s3CompatibleStorage.js.map +1 -0
- package/dist/storage/baseStorage.d.ts +1 -0
- package/dist/storage/baseStorage.d.ts.map +1 -1
- package/dist/storage/baseStorage.js +603 -0
- package/dist/storage/baseStorage.js.map +1 -0
- package/dist/storage/cacheManager.js +1306 -0
- package/dist/storage/cacheManager.js.map +1 -0
- package/dist/storage/enhancedCacheManager.js +520 -0
- package/dist/storage/enhancedCacheManager.js.map +1 -0
- package/dist/storage/readOnlyOptimizations.js +425 -0
- package/dist/storage/readOnlyOptimizations.js.map +1 -0
- package/dist/storage/storageFactory.d.ts +0 -1
- package/dist/storage/storageFactory.d.ts.map +1 -1
- package/dist/storage/storageFactory.js +227 -0
- package/dist/storage/storageFactory.js.map +1 -0
- package/dist/types/augmentations.js +16 -0
- package/dist/types/augmentations.js.map +1 -0
- package/dist/types/brainyDataInterface.js +8 -0
- package/dist/types/brainyDataInterface.js.map +1 -0
- package/dist/types/distributedTypes.js +6 -0
- package/dist/types/distributedTypes.js.map +1 -0
- package/dist/types/fileSystemTypes.js +8 -0
- package/dist/types/fileSystemTypes.js.map +1 -0
- package/dist/types/graphTypes.js +247 -0
- package/dist/types/graphTypes.js.map +1 -0
- package/dist/types/mcpTypes.js +22 -0
- package/dist/types/mcpTypes.js.map +1 -0
- package/dist/types/paginationTypes.js +5 -0
- package/dist/types/paginationTypes.js.map +1 -0
- package/dist/types/pipelineTypes.js +7 -0
- package/dist/types/pipelineTypes.js.map +1 -0
- package/dist/types/tensorflowTypes.js +6 -0
- package/dist/types/tensorflowTypes.js.map +1 -0
- package/dist/unified.js +52 -128048
- package/dist/utils/autoConfiguration.js +341 -0
- package/dist/utils/autoConfiguration.js.map +1 -0
- package/dist/utils/cacheAutoConfig.js +261 -0
- package/dist/utils/cacheAutoConfig.js.map +1 -0
- package/dist/utils/crypto.js +45 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/distance.js +239 -0
- package/dist/utils/distance.js.map +1 -0
- package/dist/utils/embedding.d.ts.map +1 -1
- package/dist/utils/embedding.js +702 -0
- package/dist/utils/embedding.js.map +1 -0
- package/dist/utils/environment.js +75 -0
- package/dist/utils/environment.js.map +1 -0
- package/dist/utils/fieldNameTracking.js +90 -0
- package/dist/utils/fieldNameTracking.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/jsonProcessing.js +179 -0
- package/dist/utils/jsonProcessing.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -92
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +129 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/operationUtils.js +126 -0
- package/dist/utils/operationUtils.js.map +1 -0
- package/dist/utils/robustModelLoader.d.ts +14 -0
- package/dist/utils/robustModelLoader.d.ts.map +1 -1
- package/dist/utils/robustModelLoader.js +537 -0
- package/dist/utils/robustModelLoader.js.map +1 -0
- package/dist/utils/searchCache.js +248 -0
- package/dist/utils/searchCache.js.map +1 -0
- package/dist/utils/statistics.js +25 -0
- package/dist/utils/statistics.js.map +1 -0
- package/dist/utils/statisticsCollector.js +224 -0
- package/dist/utils/statisticsCollector.js.map +1 -0
- package/dist/utils/textEncoding.js +309 -0
- package/dist/utils/textEncoding.js.map +1 -0
- package/dist/utils/typeUtils.js +40 -0
- package/dist/utils/typeUtils.js.map +1 -0
- package/dist/utils/version.d.ts +15 -3
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +24 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/utils/workerUtils.js +458 -0
- package/dist/utils/workerUtils.js.map +1 -0
- package/package.json +23 -15
- package/dist/brainy.js +0 -90220
- package/dist/brainy.min.js +0 -12511
- package/dist/patched-platform-node.d.ts +0 -17
- package/dist/statistics/statisticsManager.d.ts +0 -121
- package/dist/storage/fileSystemStorage.d.ts +0 -73
- package/dist/storage/fileSystemStorage.d.ts.map +0 -1
- package/dist/storage/opfsStorage.d.ts +0 -236
- package/dist/storage/opfsStorage.d.ts.map +0 -1
- package/dist/storage/s3CompatibleStorage.d.ts +0 -157
- package/dist/storage/s3CompatibleStorage.d.ts.map +0 -1
- package/dist/testing/prettyReporter.d.ts +0 -23
- package/dist/testing/prettySummaryReporter.d.ts +0 -22
- package/dist/unified.min.js +0 -16153
- package/dist/utils/environmentDetection.d.ts +0 -47
- package/dist/utils/environmentDetection.d.ts.map +0 -1
- package/dist/utils/tensorflowUtils.d.ts +0 -17
- package/dist/utils/tensorflowUtils.d.ts.map +0 -1
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPFS (Origin Private File System) Storage Adapter
|
|
3
|
+
* Provides persistent storage for the vector database using the Origin Private File System API
|
|
4
|
+
*/
|
|
5
|
+
import { BaseStorage, NOUNS_DIR, VERBS_DIR, METADATA_DIR, NOUN_METADATA_DIR, VERB_METADATA_DIR, INDEX_DIR } from '../baseStorage.js';
|
|
6
|
+
import '../../types/fileSystemTypes.js';
|
|
7
|
+
/**
|
|
8
|
+
* Helper function to safely get a file from a FileSystemHandle
|
|
9
|
+
* This is needed because TypeScript doesn't recognize that a FileSystemHandle
|
|
10
|
+
* can be a FileSystemFileHandle which has the getFile method
|
|
11
|
+
*/
|
|
12
|
+
async function safeGetFile(handle) {
|
|
13
|
+
// Type cast to any to avoid TypeScript error
|
|
14
|
+
return handle.getFile();
|
|
15
|
+
}
|
|
16
|
+
// Root directory name for OPFS storage
|
|
17
|
+
const ROOT_DIR = 'opfs-vector-db';
|
|
18
|
+
/**
|
|
19
|
+
* OPFS storage adapter for browser environments
|
|
20
|
+
* Uses the Origin Private File System API to store data persistently
|
|
21
|
+
*/
|
|
22
|
+
export class OPFSStorage extends BaseStorage {
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
this.rootDir = null;
|
|
26
|
+
this.nounsDir = null;
|
|
27
|
+
this.verbsDir = null;
|
|
28
|
+
this.metadataDir = null;
|
|
29
|
+
this.nounMetadataDir = null;
|
|
30
|
+
this.verbMetadataDir = null;
|
|
31
|
+
this.indexDir = null;
|
|
32
|
+
this.isAvailable = false;
|
|
33
|
+
this.isPersistentRequested = false;
|
|
34
|
+
this.isPersistentGranted = false;
|
|
35
|
+
this.statistics = null;
|
|
36
|
+
this.activeLocks = new Set();
|
|
37
|
+
this.lockPrefix = 'opfs-lock-';
|
|
38
|
+
// Check if OPFS is available
|
|
39
|
+
this.isAvailable =
|
|
40
|
+
typeof navigator !== 'undefined' &&
|
|
41
|
+
'storage' in navigator &&
|
|
42
|
+
'getDirectory' in navigator.storage;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Initialize the storage adapter
|
|
46
|
+
*/
|
|
47
|
+
async init() {
|
|
48
|
+
if (this.isInitialized) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!this.isAvailable) {
|
|
52
|
+
throw new Error('Origin Private File System is not available in this environment');
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
// Get the root directory
|
|
56
|
+
const root = await navigator.storage.getDirectory();
|
|
57
|
+
// Create or get our app's root directory
|
|
58
|
+
this.rootDir = await root.getDirectoryHandle(ROOT_DIR, { create: true });
|
|
59
|
+
// Create or get nouns directory
|
|
60
|
+
this.nounsDir = await this.rootDir.getDirectoryHandle(NOUNS_DIR, {
|
|
61
|
+
create: true
|
|
62
|
+
});
|
|
63
|
+
// Create or get verbs directory
|
|
64
|
+
this.verbsDir = await this.rootDir.getDirectoryHandle(VERBS_DIR, {
|
|
65
|
+
create: true
|
|
66
|
+
});
|
|
67
|
+
// Create or get metadata directory
|
|
68
|
+
this.metadataDir = await this.rootDir.getDirectoryHandle(METADATA_DIR, {
|
|
69
|
+
create: true
|
|
70
|
+
});
|
|
71
|
+
// Create or get noun metadata directory
|
|
72
|
+
this.nounMetadataDir = await this.rootDir.getDirectoryHandle(NOUN_METADATA_DIR, {
|
|
73
|
+
create: true
|
|
74
|
+
});
|
|
75
|
+
// Create or get verb metadata directory
|
|
76
|
+
this.verbMetadataDir = await this.rootDir.getDirectoryHandle(VERB_METADATA_DIR, {
|
|
77
|
+
create: true
|
|
78
|
+
});
|
|
79
|
+
// Create or get index directory
|
|
80
|
+
this.indexDir = await this.rootDir.getDirectoryHandle(INDEX_DIR, {
|
|
81
|
+
create: true
|
|
82
|
+
});
|
|
83
|
+
this.isInitialized = true;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('Failed to initialize OPFS storage:', error);
|
|
87
|
+
throw new Error(`Failed to initialize OPFS storage: ${error}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if OPFS is available in the current environment
|
|
92
|
+
*/
|
|
93
|
+
isOPFSAvailable() {
|
|
94
|
+
return this.isAvailable;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Request persistent storage permission from the user
|
|
98
|
+
* @returns Promise that resolves to true if permission was granted, false otherwise
|
|
99
|
+
*/
|
|
100
|
+
async requestPersistentStorage() {
|
|
101
|
+
if (!this.isAvailable) {
|
|
102
|
+
console.warn('Cannot request persistent storage: OPFS is not available');
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
// Check if persistence is already granted
|
|
107
|
+
this.isPersistentGranted = await navigator.storage.persisted();
|
|
108
|
+
if (!this.isPersistentGranted) {
|
|
109
|
+
// Request permission for persistent storage
|
|
110
|
+
this.isPersistentGranted = await navigator.storage.persist();
|
|
111
|
+
}
|
|
112
|
+
this.isPersistentRequested = true;
|
|
113
|
+
return this.isPersistentGranted;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.warn('Failed to request persistent storage:', error);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if persistent storage is granted
|
|
122
|
+
* @returns Promise that resolves to true if persistent storage is granted, false otherwise
|
|
123
|
+
*/
|
|
124
|
+
async isPersistent() {
|
|
125
|
+
if (!this.isAvailable) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
this.isPersistentGranted = await navigator.storage.persisted();
|
|
130
|
+
return this.isPersistentGranted;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.warn('Failed to check persistent storage status:', error);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Save a noun to storage
|
|
139
|
+
*/
|
|
140
|
+
async saveNoun_internal(noun) {
|
|
141
|
+
await this.ensureInitialized();
|
|
142
|
+
try {
|
|
143
|
+
// Convert connections Map to a serializable format
|
|
144
|
+
const serializableNoun = {
|
|
145
|
+
...noun,
|
|
146
|
+
connections: this.mapToObject(noun.connections, (set) => Array.from(set))
|
|
147
|
+
};
|
|
148
|
+
// Create or get the file for this noun
|
|
149
|
+
const fileHandle = await this.nounsDir.getFileHandle(noun.id, {
|
|
150
|
+
create: true
|
|
151
|
+
});
|
|
152
|
+
// Write the noun data to the file
|
|
153
|
+
const writable = await fileHandle.createWritable();
|
|
154
|
+
await writable.write(JSON.stringify(serializableNoun));
|
|
155
|
+
await writable.close();
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error(`Failed to save noun ${noun.id}:`, error);
|
|
159
|
+
throw new Error(`Failed to save noun ${noun.id}: ${error}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get a noun from storage
|
|
164
|
+
*/
|
|
165
|
+
async getNoun_internal(id) {
|
|
166
|
+
await this.ensureInitialized();
|
|
167
|
+
try {
|
|
168
|
+
// Get the file handle for this noun
|
|
169
|
+
const fileHandle = await this.nounsDir.getFileHandle(id);
|
|
170
|
+
// Read the noun data from the file
|
|
171
|
+
const file = await fileHandle.getFile();
|
|
172
|
+
const text = await file.text();
|
|
173
|
+
const data = JSON.parse(text);
|
|
174
|
+
// Convert serialized connections back to Map<number, Set<string>>
|
|
175
|
+
const connections = new Map();
|
|
176
|
+
for (const [level, nounIds] of Object.entries(data.connections)) {
|
|
177
|
+
connections.set(Number(level), new Set(nounIds));
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
id: data.id,
|
|
181
|
+
vector: data.vector,
|
|
182
|
+
connections,
|
|
183
|
+
level: data.level || 0
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// Noun not found or other error
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get all nouns from storage
|
|
193
|
+
*/
|
|
194
|
+
async getAllNouns_internal() {
|
|
195
|
+
await this.ensureInitialized();
|
|
196
|
+
const allNouns = [];
|
|
197
|
+
try {
|
|
198
|
+
// Iterate through all files in the nouns directory
|
|
199
|
+
for await (const [name, handle] of this.nounsDir.entries()) {
|
|
200
|
+
if (handle.kind === 'file') {
|
|
201
|
+
try {
|
|
202
|
+
// Read the noun data from the file
|
|
203
|
+
const file = await safeGetFile(handle);
|
|
204
|
+
const text = await file.text();
|
|
205
|
+
const data = JSON.parse(text);
|
|
206
|
+
// Convert serialized connections back to Map<number, Set<string>>
|
|
207
|
+
const connections = new Map();
|
|
208
|
+
for (const [level, nounIds] of Object.entries(data.connections)) {
|
|
209
|
+
connections.set(Number(level), new Set(nounIds));
|
|
210
|
+
}
|
|
211
|
+
allNouns.push({
|
|
212
|
+
id: data.id,
|
|
213
|
+
vector: data.vector,
|
|
214
|
+
connections,
|
|
215
|
+
level: data.level || 0
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error(`Error reading noun file ${name}:`, error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error('Error reading nouns directory:', error);
|
|
226
|
+
}
|
|
227
|
+
return allNouns;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get nouns by noun type (internal implementation)
|
|
231
|
+
* @param nounType The noun type to filter by
|
|
232
|
+
* @returns Promise that resolves to an array of nouns of the specified noun type
|
|
233
|
+
*/
|
|
234
|
+
async getNounsByNounType_internal(nounType) {
|
|
235
|
+
return this.getNodesByNounType(nounType);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get nodes by noun type
|
|
239
|
+
* @param nounType The noun type to filter by
|
|
240
|
+
* @returns Promise that resolves to an array of nodes of the specified noun type
|
|
241
|
+
*/
|
|
242
|
+
async getNodesByNounType(nounType) {
|
|
243
|
+
await this.ensureInitialized();
|
|
244
|
+
const nodes = [];
|
|
245
|
+
try {
|
|
246
|
+
// Iterate through all files in the nouns directory
|
|
247
|
+
for await (const [name, handle] of this.nounsDir.entries()) {
|
|
248
|
+
if (handle.kind === 'file') {
|
|
249
|
+
try {
|
|
250
|
+
// Read the node data from the file
|
|
251
|
+
const file = await safeGetFile(handle);
|
|
252
|
+
const text = await file.text();
|
|
253
|
+
const data = JSON.parse(text);
|
|
254
|
+
// Get the metadata to check the noun type
|
|
255
|
+
const metadata = await this.getMetadata(data.id);
|
|
256
|
+
// Include the node if its noun type matches the requested type
|
|
257
|
+
if (metadata && metadata.noun === nounType) {
|
|
258
|
+
// Convert serialized connections back to Map<number, Set<string>>
|
|
259
|
+
const connections = new Map();
|
|
260
|
+
for (const [level, nodeIds] of Object.entries(data.connections)) {
|
|
261
|
+
connections.set(Number(level), new Set(nodeIds));
|
|
262
|
+
}
|
|
263
|
+
nodes.push({
|
|
264
|
+
id: data.id,
|
|
265
|
+
vector: data.vector,
|
|
266
|
+
connections,
|
|
267
|
+
level: data.level || 0
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error(`Error reading node file ${name}:`, error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
console.error('Error reading nouns directory:', error);
|
|
279
|
+
}
|
|
280
|
+
return nodes;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Delete a noun from storage (internal implementation)
|
|
284
|
+
*/
|
|
285
|
+
async deleteNoun_internal(id) {
|
|
286
|
+
return this.deleteNode(id);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Delete a node from storage
|
|
290
|
+
*/
|
|
291
|
+
async deleteNode(id) {
|
|
292
|
+
await this.ensureInitialized();
|
|
293
|
+
try {
|
|
294
|
+
await this.nounsDir.removeEntry(id);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
// Ignore NotFoundError, which means the file doesn't exist
|
|
298
|
+
if (error.name !== 'NotFoundError') {
|
|
299
|
+
console.error(`Error deleting node ${id}:`, error);
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Save a verb to storage (internal implementation)
|
|
306
|
+
*/
|
|
307
|
+
async saveVerb_internal(verb) {
|
|
308
|
+
return this.saveEdge(verb);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Save an edge to storage
|
|
312
|
+
*/
|
|
313
|
+
async saveEdge(edge) {
|
|
314
|
+
await this.ensureInitialized();
|
|
315
|
+
try {
|
|
316
|
+
// Convert connections Map to a serializable format
|
|
317
|
+
const serializableEdge = {
|
|
318
|
+
...edge,
|
|
319
|
+
connections: this.mapToObject(edge.connections, (set) => Array.from(set))
|
|
320
|
+
};
|
|
321
|
+
// Create or get the file for this verb
|
|
322
|
+
const fileHandle = await this.verbsDir.getFileHandle(edge.id, {
|
|
323
|
+
create: true
|
|
324
|
+
});
|
|
325
|
+
// Write the verb data to the file
|
|
326
|
+
const writable = await fileHandle.createWritable();
|
|
327
|
+
await writable.write(JSON.stringify(serializableEdge));
|
|
328
|
+
await writable.close();
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
console.error(`Failed to save edge ${edge.id}:`, error);
|
|
332
|
+
throw new Error(`Failed to save edge ${edge.id}: ${error}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get a verb from storage (internal implementation)
|
|
337
|
+
*/
|
|
338
|
+
async getVerb_internal(id) {
|
|
339
|
+
return this.getEdge(id);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get an edge from storage
|
|
343
|
+
*/
|
|
344
|
+
async getEdge(id) {
|
|
345
|
+
await this.ensureInitialized();
|
|
346
|
+
try {
|
|
347
|
+
// Get the file handle for this edge
|
|
348
|
+
const fileHandle = await this.verbsDir.getFileHandle(id);
|
|
349
|
+
// Read the edge data from the file
|
|
350
|
+
const file = await fileHandle.getFile();
|
|
351
|
+
const text = await file.text();
|
|
352
|
+
const data = JSON.parse(text);
|
|
353
|
+
// Convert serialized connections back to Map<number, Set<string>>
|
|
354
|
+
const connections = new Map();
|
|
355
|
+
for (const [level, nodeIds] of Object.entries(data.connections)) {
|
|
356
|
+
connections.set(Number(level), new Set(nodeIds));
|
|
357
|
+
}
|
|
358
|
+
// Create default timestamp if not present
|
|
359
|
+
const defaultTimestamp = {
|
|
360
|
+
seconds: Math.floor(Date.now() / 1000),
|
|
361
|
+
nanoseconds: (Date.now() % 1000) * 1000000
|
|
362
|
+
};
|
|
363
|
+
// Create default createdBy if not present
|
|
364
|
+
const defaultCreatedBy = {
|
|
365
|
+
augmentation: 'unknown',
|
|
366
|
+
version: '1.0'
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
id: data.id,
|
|
370
|
+
vector: data.vector,
|
|
371
|
+
connections
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
// Edge not found or other error
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get all verbs from storage (internal implementation)
|
|
381
|
+
*/
|
|
382
|
+
async getAllVerbs_internal() {
|
|
383
|
+
return this.getAllEdges();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get all edges from storage
|
|
387
|
+
*/
|
|
388
|
+
async getAllEdges() {
|
|
389
|
+
await this.ensureInitialized();
|
|
390
|
+
const allEdges = [];
|
|
391
|
+
try {
|
|
392
|
+
// Iterate through all files in the verbs directory
|
|
393
|
+
for await (const [name, handle] of this.verbsDir.entries()) {
|
|
394
|
+
if (handle.kind === 'file') {
|
|
395
|
+
try {
|
|
396
|
+
// Read the edge data from the file
|
|
397
|
+
const file = await safeGetFile(handle);
|
|
398
|
+
const text = await file.text();
|
|
399
|
+
const data = JSON.parse(text);
|
|
400
|
+
// Convert serialized connections back to Map<number, Set<string>>
|
|
401
|
+
const connections = new Map();
|
|
402
|
+
for (const [level, nodeIds] of Object.entries(data.connections)) {
|
|
403
|
+
connections.set(Number(level), new Set(nodeIds));
|
|
404
|
+
}
|
|
405
|
+
// Create default timestamp if not present
|
|
406
|
+
const defaultTimestamp = {
|
|
407
|
+
seconds: Math.floor(Date.now() / 1000),
|
|
408
|
+
nanoseconds: (Date.now() % 1000) * 1000000
|
|
409
|
+
};
|
|
410
|
+
// Create default createdBy if not present
|
|
411
|
+
const defaultCreatedBy = {
|
|
412
|
+
augmentation: 'unknown',
|
|
413
|
+
version: '1.0'
|
|
414
|
+
};
|
|
415
|
+
allEdges.push({
|
|
416
|
+
id: data.id,
|
|
417
|
+
vector: data.vector,
|
|
418
|
+
connections
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
console.error(`Error reading edge file ${name}:`, error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
console.error('Error reading verbs directory:', error);
|
|
429
|
+
}
|
|
430
|
+
return allEdges;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get verbs by source (internal implementation)
|
|
434
|
+
*/
|
|
435
|
+
async getVerbsBySource_internal(sourceId) {
|
|
436
|
+
// This method is deprecated and would require loading metadata for each edge
|
|
437
|
+
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
438
|
+
console.warn('getVerbsBySource_internal is deprecated and not efficiently supported in new storage pattern');
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get edges by source
|
|
443
|
+
*/
|
|
444
|
+
async getEdgesBySource(sourceId) {
|
|
445
|
+
// This method is deprecated and would require loading metadata for each edge
|
|
446
|
+
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
447
|
+
console.warn('getEdgesBySource is deprecated and not efficiently supported in new storage pattern');
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Get verbs by target (internal implementation)
|
|
452
|
+
*/
|
|
453
|
+
async getVerbsByTarget_internal(targetId) {
|
|
454
|
+
// This method is deprecated and would require loading metadata for each edge
|
|
455
|
+
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
456
|
+
console.warn('getVerbsByTarget_internal is deprecated and not efficiently supported in new storage pattern');
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Get edges by target
|
|
461
|
+
*/
|
|
462
|
+
async getEdgesByTarget(targetId) {
|
|
463
|
+
// This method is deprecated and would require loading metadata for each edge
|
|
464
|
+
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
465
|
+
console.warn('getEdgesByTarget is deprecated and not efficiently supported in new storage pattern');
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get verbs by type (internal implementation)
|
|
470
|
+
*/
|
|
471
|
+
async getVerbsByType_internal(type) {
|
|
472
|
+
// This method is deprecated and would require loading metadata for each edge
|
|
473
|
+
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
474
|
+
console.warn('getVerbsByType_internal is deprecated and not efficiently supported in new storage pattern');
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get edges by type
|
|
479
|
+
*/
|
|
480
|
+
async getEdgesByType(type) {
|
|
481
|
+
// This method is deprecated and would require loading metadata for each edge
|
|
482
|
+
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
|
483
|
+
console.warn('getEdgesByType is deprecated and not efficiently supported in new storage pattern');
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Delete a verb from storage (internal implementation)
|
|
488
|
+
*/
|
|
489
|
+
async deleteVerb_internal(id) {
|
|
490
|
+
return this.deleteEdge(id);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Delete an edge from storage
|
|
494
|
+
*/
|
|
495
|
+
async deleteEdge(id) {
|
|
496
|
+
await this.ensureInitialized();
|
|
497
|
+
try {
|
|
498
|
+
await this.verbsDir.removeEntry(id);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
// Ignore NotFoundError, which means the file doesn't exist
|
|
502
|
+
if (error.name !== 'NotFoundError') {
|
|
503
|
+
console.error(`Error deleting edge ${id}:`, error);
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Save metadata to storage
|
|
510
|
+
*/
|
|
511
|
+
async saveMetadata(id, metadata) {
|
|
512
|
+
await this.ensureInitialized();
|
|
513
|
+
try {
|
|
514
|
+
// Create or get the file for this metadata
|
|
515
|
+
const fileHandle = await this.metadataDir.getFileHandle(id, {
|
|
516
|
+
create: true
|
|
517
|
+
});
|
|
518
|
+
// Write the metadata to the file
|
|
519
|
+
const writable = await fileHandle.createWritable();
|
|
520
|
+
await writable.write(JSON.stringify(metadata));
|
|
521
|
+
await writable.close();
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
console.error(`Failed to save metadata ${id}:`, error);
|
|
525
|
+
throw new Error(`Failed to save metadata ${id}: ${error}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Get metadata from storage
|
|
530
|
+
*/
|
|
531
|
+
async getMetadata(id) {
|
|
532
|
+
await this.ensureInitialized();
|
|
533
|
+
try {
|
|
534
|
+
// Get the file handle for this metadata
|
|
535
|
+
const fileHandle = await this.metadataDir.getFileHandle(id);
|
|
536
|
+
// Read the metadata from the file
|
|
537
|
+
const file = await fileHandle.getFile();
|
|
538
|
+
const text = await file.text();
|
|
539
|
+
return JSON.parse(text);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
// Metadata not found or other error
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Save verb metadata to storage
|
|
548
|
+
*/
|
|
549
|
+
async saveVerbMetadata(id, metadata) {
|
|
550
|
+
await this.ensureInitialized();
|
|
551
|
+
const fileName = `${id}.json`;
|
|
552
|
+
const fileHandle = await this.verbMetadataDir.getFileHandle(fileName, { create: true });
|
|
553
|
+
const writable = await fileHandle.createWritable();
|
|
554
|
+
await writable.write(JSON.stringify(metadata, null, 2));
|
|
555
|
+
await writable.close();
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Get verb metadata from storage
|
|
559
|
+
*/
|
|
560
|
+
async getVerbMetadata(id) {
|
|
561
|
+
await this.ensureInitialized();
|
|
562
|
+
const fileName = `${id}.json`;
|
|
563
|
+
try {
|
|
564
|
+
const fileHandle = await this.verbMetadataDir.getFileHandle(fileName);
|
|
565
|
+
const file = await safeGetFile(fileHandle);
|
|
566
|
+
const text = await file.text();
|
|
567
|
+
return JSON.parse(text);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
if (error.name !== 'NotFoundError') {
|
|
571
|
+
console.error(`Error reading verb metadata ${id}:`, error);
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Save noun metadata to storage
|
|
578
|
+
*/
|
|
579
|
+
async saveNounMetadata(id, metadata) {
|
|
580
|
+
await this.ensureInitialized();
|
|
581
|
+
const fileName = `${id}.json`;
|
|
582
|
+
const fileHandle = await this.nounMetadataDir.getFileHandle(fileName, { create: true });
|
|
583
|
+
const writable = await fileHandle.createWritable();
|
|
584
|
+
await writable.write(JSON.stringify(metadata, null, 2));
|
|
585
|
+
await writable.close();
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get noun metadata from storage
|
|
589
|
+
*/
|
|
590
|
+
async getNounMetadata(id) {
|
|
591
|
+
await this.ensureInitialized();
|
|
592
|
+
const fileName = `${id}.json`;
|
|
593
|
+
try {
|
|
594
|
+
const fileHandle = await this.nounMetadataDir.getFileHandle(fileName);
|
|
595
|
+
const file = await safeGetFile(fileHandle);
|
|
596
|
+
const text = await file.text();
|
|
597
|
+
return JSON.parse(text);
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
if (error.name !== 'NotFoundError') {
|
|
601
|
+
console.error(`Error reading noun metadata ${id}:`, error);
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Clear all data from storage
|
|
608
|
+
*/
|
|
609
|
+
async clear() {
|
|
610
|
+
await this.ensureInitialized();
|
|
611
|
+
// Helper function to remove all files in a directory
|
|
612
|
+
const removeDirectoryContents = async (dirHandle) => {
|
|
613
|
+
try {
|
|
614
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
615
|
+
// Use recursive option to handle directories that may contain files
|
|
616
|
+
await dirHandle.removeEntry(name, { recursive: true });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
console.error(`Error removing directory contents:`, error);
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
try {
|
|
625
|
+
// Remove all files in the nouns directory
|
|
626
|
+
await removeDirectoryContents(this.nounsDir);
|
|
627
|
+
// Remove all files in the verbs directory
|
|
628
|
+
await removeDirectoryContents(this.verbsDir);
|
|
629
|
+
// Remove all files in the metadata directory
|
|
630
|
+
await removeDirectoryContents(this.metadataDir);
|
|
631
|
+
// Remove all files in the noun metadata directory
|
|
632
|
+
await removeDirectoryContents(this.nounMetadataDir);
|
|
633
|
+
// Remove all files in the verb metadata directory
|
|
634
|
+
await removeDirectoryContents(this.verbMetadataDir);
|
|
635
|
+
// Remove all files in the index directory
|
|
636
|
+
await removeDirectoryContents(this.indexDir);
|
|
637
|
+
// Clear the statistics cache
|
|
638
|
+
this.statisticsCache = null;
|
|
639
|
+
this.statisticsModified = false;
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
console.error('Error clearing storage:', error);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get information about storage usage and capacity
|
|
648
|
+
*/
|
|
649
|
+
async getStorageStatus() {
|
|
650
|
+
await this.ensureInitialized();
|
|
651
|
+
try {
|
|
652
|
+
// Calculate the total size of all files in the storage directories
|
|
653
|
+
let totalSize = 0;
|
|
654
|
+
// Helper function to calculate directory size
|
|
655
|
+
const calculateDirSize = async (dirHandle) => {
|
|
656
|
+
let size = 0;
|
|
657
|
+
try {
|
|
658
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
659
|
+
if (handle.kind === 'file') {
|
|
660
|
+
const file = await handle.getFile();
|
|
661
|
+
size += file.size;
|
|
662
|
+
}
|
|
663
|
+
else if (handle.kind === 'directory') {
|
|
664
|
+
size += await calculateDirSize(handle);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
console.warn(`Error calculating size for directory:`, error);
|
|
670
|
+
}
|
|
671
|
+
return size;
|
|
672
|
+
};
|
|
673
|
+
// Helper function to count files in a directory
|
|
674
|
+
const countFilesInDirectory = async (dirHandle) => {
|
|
675
|
+
let count = 0;
|
|
676
|
+
try {
|
|
677
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
678
|
+
if (handle.kind === 'file') {
|
|
679
|
+
count++;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
console.warn(`Error counting files in directory:`, error);
|
|
685
|
+
}
|
|
686
|
+
return count;
|
|
687
|
+
};
|
|
688
|
+
// Calculate size for each directory
|
|
689
|
+
if (this.nounsDir) {
|
|
690
|
+
totalSize += await calculateDirSize(this.nounsDir);
|
|
691
|
+
}
|
|
692
|
+
if (this.verbsDir) {
|
|
693
|
+
totalSize += await calculateDirSize(this.verbsDir);
|
|
694
|
+
}
|
|
695
|
+
if (this.metadataDir) {
|
|
696
|
+
totalSize += await calculateDirSize(this.metadataDir);
|
|
697
|
+
}
|
|
698
|
+
if (this.indexDir) {
|
|
699
|
+
totalSize += await calculateDirSize(this.indexDir);
|
|
700
|
+
}
|
|
701
|
+
// Get storage quota information using the Storage API
|
|
702
|
+
let quota = null;
|
|
703
|
+
let details = {
|
|
704
|
+
isPersistent: await this.isPersistent(),
|
|
705
|
+
nounTypes: {}
|
|
706
|
+
};
|
|
707
|
+
try {
|
|
708
|
+
if (navigator.storage && navigator.storage.estimate) {
|
|
709
|
+
const estimate = await navigator.storage.estimate();
|
|
710
|
+
quota = estimate.quota || null;
|
|
711
|
+
details = {
|
|
712
|
+
...details,
|
|
713
|
+
usage: estimate.usage,
|
|
714
|
+
quota: estimate.quota,
|
|
715
|
+
freePercentage: estimate.quota
|
|
716
|
+
? ((estimate.quota - (estimate.usage || 0)) / estimate.quota) *
|
|
717
|
+
100
|
|
718
|
+
: null
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
console.warn('Unable to get storage estimate:', error);
|
|
724
|
+
}
|
|
725
|
+
// Count files in each directory
|
|
726
|
+
if (this.nounsDir) {
|
|
727
|
+
details.nounsCount = await countFilesInDirectory(this.nounsDir);
|
|
728
|
+
}
|
|
729
|
+
if (this.verbsDir) {
|
|
730
|
+
details.verbsCount = await countFilesInDirectory(this.verbsDir);
|
|
731
|
+
}
|
|
732
|
+
if (this.metadataDir) {
|
|
733
|
+
details.metadataCount = await countFilesInDirectory(this.metadataDir);
|
|
734
|
+
}
|
|
735
|
+
// Count nouns by type using metadata
|
|
736
|
+
const nounTypeCounts = {};
|
|
737
|
+
if (this.metadataDir) {
|
|
738
|
+
for await (const [name, handle] of this.metadataDir.entries()) {
|
|
739
|
+
if (handle.kind === 'file') {
|
|
740
|
+
try {
|
|
741
|
+
const file = await safeGetFile(handle);
|
|
742
|
+
const text = await file.text();
|
|
743
|
+
const metadata = JSON.parse(text);
|
|
744
|
+
if (metadata.noun) {
|
|
745
|
+
nounTypeCounts[metadata.noun] =
|
|
746
|
+
(nounTypeCounts[metadata.noun] || 0) + 1;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
console.error(`Error reading metadata file ${name}:`, error);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
details.nounTypes = nounTypeCounts;
|
|
756
|
+
return {
|
|
757
|
+
type: 'opfs',
|
|
758
|
+
used: totalSize,
|
|
759
|
+
quota,
|
|
760
|
+
details
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
console.error('Failed to get storage status:', error);
|
|
765
|
+
return {
|
|
766
|
+
type: 'opfs',
|
|
767
|
+
used: 0,
|
|
768
|
+
quota: null,
|
|
769
|
+
details: { error: String(error) }
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Get the statistics key for a specific date
|
|
775
|
+
* @param date The date to get the key for
|
|
776
|
+
* @returns The statistics key for the specified date
|
|
777
|
+
*/
|
|
778
|
+
getStatisticsKeyForDate(date) {
|
|
779
|
+
const year = date.getUTCFullYear();
|
|
780
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
781
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
782
|
+
return `statistics_${year}${month}${day}.json`;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Get the current statistics key
|
|
786
|
+
* @returns The current statistics key
|
|
787
|
+
*/
|
|
788
|
+
getCurrentStatisticsKey() {
|
|
789
|
+
return this.getStatisticsKeyForDate(new Date());
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Get the legacy statistics key (for backward compatibility)
|
|
793
|
+
* @returns The legacy statistics key
|
|
794
|
+
*/
|
|
795
|
+
getLegacyStatisticsKey() {
|
|
796
|
+
return 'statistics.json';
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Acquire a browser-based lock for coordinating operations across multiple tabs
|
|
800
|
+
* @param lockKey The key to lock on
|
|
801
|
+
* @param ttl Time to live for the lock in milliseconds (default: 30 seconds)
|
|
802
|
+
* @returns Promise that resolves to true if lock was acquired, false otherwise
|
|
803
|
+
*/
|
|
804
|
+
async acquireLock(lockKey, ttl = 30000) {
|
|
805
|
+
if (typeof localStorage === 'undefined') {
|
|
806
|
+
console.warn('localStorage not available, proceeding without lock');
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
const lockStorageKey = `${this.lockPrefix}${lockKey}`;
|
|
810
|
+
const lockValue = `${Date.now()}_${Math.random()}_${window.location.href}`;
|
|
811
|
+
const expiresAt = Date.now() + ttl;
|
|
812
|
+
try {
|
|
813
|
+
// Check if lock already exists and is still valid
|
|
814
|
+
const existingLock = localStorage.getItem(lockStorageKey);
|
|
815
|
+
if (existingLock) {
|
|
816
|
+
try {
|
|
817
|
+
const lockInfo = JSON.parse(existingLock);
|
|
818
|
+
if (lockInfo.expiresAt > Date.now()) {
|
|
819
|
+
// Lock exists and is still valid
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
// Invalid lock data, we can proceed to create a new lock
|
|
825
|
+
console.warn(`Invalid lock data for ${lockStorageKey}:`, error);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Try to create the lock
|
|
829
|
+
const lockInfo = {
|
|
830
|
+
lockValue,
|
|
831
|
+
expiresAt,
|
|
832
|
+
tabId: window.location.href,
|
|
833
|
+
timestamp: Date.now()
|
|
834
|
+
};
|
|
835
|
+
localStorage.setItem(lockStorageKey, JSON.stringify(lockInfo));
|
|
836
|
+
// Add to active locks for cleanup
|
|
837
|
+
this.activeLocks.add(lockKey);
|
|
838
|
+
// Schedule automatic cleanup when lock expires
|
|
839
|
+
setTimeout(() => {
|
|
840
|
+
this.releaseLock(lockKey, lockValue).catch((error) => {
|
|
841
|
+
console.warn(`Failed to auto-release expired lock ${lockKey}:`, error);
|
|
842
|
+
});
|
|
843
|
+
}, ttl);
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
console.warn(`Failed to acquire lock ${lockKey}:`, error);
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Release a browser-based lock
|
|
853
|
+
* @param lockKey The key to unlock
|
|
854
|
+
* @param lockValue The value used when acquiring the lock (for verification)
|
|
855
|
+
* @returns Promise that resolves when lock is released
|
|
856
|
+
*/
|
|
857
|
+
async releaseLock(lockKey, lockValue) {
|
|
858
|
+
if (typeof localStorage === 'undefined') {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const lockStorageKey = `${this.lockPrefix}${lockKey}`;
|
|
862
|
+
try {
|
|
863
|
+
// If lockValue is provided, verify it matches before releasing
|
|
864
|
+
if (lockValue) {
|
|
865
|
+
const existingLock = localStorage.getItem(lockStorageKey);
|
|
866
|
+
if (existingLock) {
|
|
867
|
+
try {
|
|
868
|
+
const lockInfo = JSON.parse(existingLock);
|
|
869
|
+
if (lockInfo.lockValue !== lockValue) {
|
|
870
|
+
// Lock was acquired by someone else, don't release it
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
// Invalid lock data, remove it
|
|
876
|
+
localStorage.removeItem(lockStorageKey);
|
|
877
|
+
this.activeLocks.delete(lockKey);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Remove the lock
|
|
883
|
+
localStorage.removeItem(lockStorageKey);
|
|
884
|
+
// Remove from active locks
|
|
885
|
+
this.activeLocks.delete(lockKey);
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
console.warn(`Failed to release lock ${lockKey}:`, error);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Clean up expired locks from localStorage
|
|
893
|
+
*/
|
|
894
|
+
async cleanupExpiredLocks() {
|
|
895
|
+
if (typeof localStorage === 'undefined') {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const now = Date.now();
|
|
900
|
+
const keysToRemove = [];
|
|
901
|
+
// Iterate through localStorage to find expired locks
|
|
902
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
903
|
+
const key = localStorage.key(i);
|
|
904
|
+
if (key && key.startsWith(this.lockPrefix)) {
|
|
905
|
+
try {
|
|
906
|
+
const lockData = localStorage.getItem(key);
|
|
907
|
+
if (lockData) {
|
|
908
|
+
const lockInfo = JSON.parse(lockData);
|
|
909
|
+
if (lockInfo.expiresAt <= now) {
|
|
910
|
+
keysToRemove.push(key);
|
|
911
|
+
const lockKey = key.replace(this.lockPrefix, '');
|
|
912
|
+
this.activeLocks.delete(lockKey);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
// Invalid lock data, mark for removal
|
|
918
|
+
keysToRemove.push(key);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// Remove expired locks
|
|
923
|
+
keysToRemove.forEach((key) => {
|
|
924
|
+
localStorage.removeItem(key);
|
|
925
|
+
});
|
|
926
|
+
if (keysToRemove.length > 0) {
|
|
927
|
+
console.log(`Cleaned up ${keysToRemove.length} expired locks`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
console.warn('Failed to cleanup expired locks:', error);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Save statistics data to storage with browser-based locking
|
|
936
|
+
* @param statistics The statistics data to save
|
|
937
|
+
*/
|
|
938
|
+
async saveStatisticsData(statistics) {
|
|
939
|
+
const lockKey = 'statistics';
|
|
940
|
+
const lockAcquired = await this.acquireLock(lockKey, 10000); // 10 second timeout
|
|
941
|
+
if (!lockAcquired) {
|
|
942
|
+
console.warn('Failed to acquire lock for statistics update, proceeding without lock');
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
// Get existing statistics to merge with new data
|
|
946
|
+
const existingStats = await this.getStatisticsData();
|
|
947
|
+
let mergedStats;
|
|
948
|
+
if (existingStats) {
|
|
949
|
+
// Merge statistics data
|
|
950
|
+
mergedStats = {
|
|
951
|
+
nounCount: {
|
|
952
|
+
...existingStats.nounCount,
|
|
953
|
+
...statistics.nounCount
|
|
954
|
+
},
|
|
955
|
+
verbCount: {
|
|
956
|
+
...existingStats.verbCount,
|
|
957
|
+
...statistics.verbCount
|
|
958
|
+
},
|
|
959
|
+
metadataCount: {
|
|
960
|
+
...existingStats.metadataCount,
|
|
961
|
+
...statistics.metadataCount
|
|
962
|
+
},
|
|
963
|
+
hnswIndexSize: Math.max(statistics.hnswIndexSize || 0, existingStats.hnswIndexSize || 0),
|
|
964
|
+
lastUpdated: new Date().toISOString()
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
// No existing statistics, use new ones
|
|
969
|
+
mergedStats = {
|
|
970
|
+
...statistics,
|
|
971
|
+
lastUpdated: new Date().toISOString()
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
// Create a deep copy to avoid reference issues
|
|
975
|
+
this.statistics = {
|
|
976
|
+
nounCount: { ...mergedStats.nounCount },
|
|
977
|
+
verbCount: { ...mergedStats.verbCount },
|
|
978
|
+
metadataCount: { ...mergedStats.metadataCount },
|
|
979
|
+
hnswIndexSize: mergedStats.hnswIndexSize,
|
|
980
|
+
lastUpdated: mergedStats.lastUpdated
|
|
981
|
+
};
|
|
982
|
+
// Ensure the root directory is initialized
|
|
983
|
+
await this.ensureInitialized();
|
|
984
|
+
// Get or create the index directory
|
|
985
|
+
if (!this.indexDir) {
|
|
986
|
+
throw new Error('Index directory not initialized');
|
|
987
|
+
}
|
|
988
|
+
// Get the current statistics key
|
|
989
|
+
const currentKey = this.getCurrentStatisticsKey();
|
|
990
|
+
// Create a file for the statistics data
|
|
991
|
+
const fileHandle = await this.indexDir.getFileHandle(currentKey, {
|
|
992
|
+
create: true
|
|
993
|
+
});
|
|
994
|
+
// Create a writable stream
|
|
995
|
+
const writable = await fileHandle.createWritable();
|
|
996
|
+
// Write the statistics data to the file
|
|
997
|
+
await writable.write(JSON.stringify(this.statistics, null, 2));
|
|
998
|
+
// Close the stream
|
|
999
|
+
await writable.close();
|
|
1000
|
+
// Also update the legacy key for backward compatibility, but less frequently
|
|
1001
|
+
if (Math.random() < 0.1) {
|
|
1002
|
+
const legacyKey = this.getLegacyStatisticsKey();
|
|
1003
|
+
const legacyFileHandle = await this.indexDir.getFileHandle(legacyKey, {
|
|
1004
|
+
create: true
|
|
1005
|
+
});
|
|
1006
|
+
const legacyWritable = await legacyFileHandle.createWritable();
|
|
1007
|
+
await legacyWritable.write(JSON.stringify(this.statistics, null, 2));
|
|
1008
|
+
await legacyWritable.close();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
console.error('Failed to save statistics data:', error);
|
|
1013
|
+
throw new Error(`Failed to save statistics data: ${error}`);
|
|
1014
|
+
}
|
|
1015
|
+
finally {
|
|
1016
|
+
if (lockAcquired) {
|
|
1017
|
+
await this.releaseLock(lockKey);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Get statistics data from storage
|
|
1023
|
+
* @returns Promise that resolves to the statistics data or null if not found
|
|
1024
|
+
*/
|
|
1025
|
+
async getStatisticsData() {
|
|
1026
|
+
// If we have cached statistics, return a deep copy
|
|
1027
|
+
if (this.statistics) {
|
|
1028
|
+
return {
|
|
1029
|
+
nounCount: { ...this.statistics.nounCount },
|
|
1030
|
+
verbCount: { ...this.statistics.verbCount },
|
|
1031
|
+
metadataCount: { ...this.statistics.metadataCount },
|
|
1032
|
+
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
1033
|
+
lastUpdated: this.statistics.lastUpdated
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
// Ensure the root directory is initialized
|
|
1038
|
+
await this.ensureInitialized();
|
|
1039
|
+
if (!this.indexDir) {
|
|
1040
|
+
throw new Error('Index directory not initialized');
|
|
1041
|
+
}
|
|
1042
|
+
// First try to get statistics from today's file
|
|
1043
|
+
const currentKey = this.getCurrentStatisticsKey();
|
|
1044
|
+
try {
|
|
1045
|
+
const fileHandle = await this.indexDir.getFileHandle(currentKey, {
|
|
1046
|
+
create: false
|
|
1047
|
+
});
|
|
1048
|
+
const file = await fileHandle.getFile();
|
|
1049
|
+
const text = await file.text();
|
|
1050
|
+
this.statistics = JSON.parse(text);
|
|
1051
|
+
if (this.statistics) {
|
|
1052
|
+
return {
|
|
1053
|
+
nounCount: { ...this.statistics.nounCount },
|
|
1054
|
+
verbCount: { ...this.statistics.verbCount },
|
|
1055
|
+
metadataCount: { ...this.statistics.metadataCount },
|
|
1056
|
+
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
1057
|
+
lastUpdated: this.statistics.lastUpdated
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
// If today's file doesn't exist, try yesterday's file
|
|
1063
|
+
const yesterday = new Date();
|
|
1064
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
1065
|
+
const yesterdayKey = this.getStatisticsKeyForDate(yesterday);
|
|
1066
|
+
try {
|
|
1067
|
+
const fileHandle = await this.indexDir.getFileHandle(yesterdayKey, {
|
|
1068
|
+
create: false
|
|
1069
|
+
});
|
|
1070
|
+
const file = await fileHandle.getFile();
|
|
1071
|
+
const text = await file.text();
|
|
1072
|
+
this.statistics = JSON.parse(text);
|
|
1073
|
+
if (this.statistics) {
|
|
1074
|
+
return {
|
|
1075
|
+
nounCount: { ...this.statistics.nounCount },
|
|
1076
|
+
verbCount: { ...this.statistics.verbCount },
|
|
1077
|
+
metadataCount: { ...this.statistics.metadataCount },
|
|
1078
|
+
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
1079
|
+
lastUpdated: this.statistics.lastUpdated
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
// If yesterday's file doesn't exist, try the legacy file
|
|
1085
|
+
const legacyKey = this.getLegacyStatisticsKey();
|
|
1086
|
+
try {
|
|
1087
|
+
const fileHandle = await this.indexDir.getFileHandle(legacyKey, {
|
|
1088
|
+
create: false
|
|
1089
|
+
});
|
|
1090
|
+
const file = await fileHandle.getFile();
|
|
1091
|
+
const text = await file.text();
|
|
1092
|
+
this.statistics = JSON.parse(text);
|
|
1093
|
+
if (this.statistics) {
|
|
1094
|
+
return {
|
|
1095
|
+
nounCount: { ...this.statistics.nounCount },
|
|
1096
|
+
verbCount: { ...this.statistics.verbCount },
|
|
1097
|
+
metadataCount: { ...this.statistics.metadataCount },
|
|
1098
|
+
hnswIndexSize: this.statistics.hnswIndexSize,
|
|
1099
|
+
lastUpdated: this.statistics.lastUpdated
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
// If the legacy file doesn't exist either, return null
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// If we get here and statistics is null, return default statistics
|
|
1110
|
+
return this.statistics ? this.statistics : null;
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
console.error('Failed to get statistics data:', error);
|
|
1114
|
+
throw new Error(`Failed to get statistics data: ${error}`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
//# sourceMappingURL=opfsStorage.js.map
|