@soulcraft/brainy 0.31.0 → 0.32.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 +100 -0
- package/dist/brainyData.d.ts +8 -0
- package/dist/coreTypes.d.ts +1 -0
- package/dist/storage/adapters/s3CompatibleStorage.d.ts +75 -0
- package/dist/storage/adapters/s3CompatibleStorage.d.ts.map +1 -1
- package/dist/storage/baseStorage.d.ts +4 -0
- package/dist/storage/baseStorage.d.ts.map +1 -1
- package/dist/storage/cacheManager.d.ts +250 -0
- package/dist/storage/cacheManager.d.ts.map +1 -0
- package/dist/unified.js +1566 -322
- package/dist/unified.min.js +748 -748
- package/package.json +1 -1
package/dist/unified.js
CHANGED
|
@@ -5766,9 +5766,12 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
5766
5766
|
}
|
|
5767
5767
|
/**
|
|
5768
5768
|
* Get all nouns from storage
|
|
5769
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
5770
|
+
* It can cause memory issues with large datasets. Use getNouns() with pagination instead.
|
|
5769
5771
|
*/
|
|
5770
5772
|
async getAllNouns() {
|
|
5771
5773
|
await this.ensureInitialized();
|
|
5774
|
+
console.warn('WARNING: getAllNouns() is deprecated and will be removed in a future version. Use getNouns() with pagination instead.');
|
|
5772
5775
|
return this.getAllNouns_internal();
|
|
5773
5776
|
}
|
|
5774
5777
|
/**
|
|
@@ -5803,9 +5806,12 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
5803
5806
|
}
|
|
5804
5807
|
/**
|
|
5805
5808
|
* Get all verbs from storage
|
|
5809
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
5810
|
+
* It can cause memory issues with large datasets. Use getVerbs() with pagination instead.
|
|
5806
5811
|
*/
|
|
5807
5812
|
async getAllVerbs() {
|
|
5808
5813
|
await this.ensureInitialized();
|
|
5814
|
+
console.warn('WARNING: getAllVerbs() is deprecated and will be removed in a future version. Use getVerbs() with pagination instead.');
|
|
5809
5815
|
return this.getAllVerbs_internal();
|
|
5810
5816
|
}
|
|
5811
5817
|
/**
|
|
@@ -5840,10 +5846,13 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
5840
5846
|
const pagination = options?.pagination || {};
|
|
5841
5847
|
const limit = pagination.limit || 100;
|
|
5842
5848
|
const offset = pagination.offset || 0;
|
|
5849
|
+
const cursor = pagination.cursor;
|
|
5843
5850
|
// Optimize for common filter cases to avoid loading all nouns
|
|
5844
5851
|
if (options?.filter) {
|
|
5845
5852
|
// If filtering by nounType only, use the optimized method
|
|
5846
|
-
if (options.filter.nounType &&
|
|
5853
|
+
if (options.filter.nounType &&
|
|
5854
|
+
!options.filter.service &&
|
|
5855
|
+
!options.filter.metadata) {
|
|
5847
5856
|
const nounType = Array.isArray(options.filter.nounType)
|
|
5848
5857
|
? options.filter.nounType[0]
|
|
5849
5858
|
: options.filter.nounType;
|
|
@@ -5866,81 +5875,124 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
5866
5875
|
};
|
|
5867
5876
|
}
|
|
5868
5877
|
}
|
|
5869
|
-
// For more complex filtering or no filtering,
|
|
5870
|
-
//
|
|
5871
|
-
const maxNouns = offset + limit + 1; // Get one extra to check if there are more
|
|
5872
|
-
let allNouns = [];
|
|
5878
|
+
// For more complex filtering or no filtering, use a paginated approach
|
|
5879
|
+
// that avoids loading all nouns into memory at once
|
|
5873
5880
|
try {
|
|
5874
|
-
//
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5881
|
+
// First, try to get a count of total nouns (if the adapter supports it)
|
|
5882
|
+
let totalCount = undefined;
|
|
5883
|
+
try {
|
|
5884
|
+
// This is an optional method that adapters may implement
|
|
5885
|
+
if (typeof this.countNouns === 'function') {
|
|
5886
|
+
totalCount = await this.countNouns(options?.filter);
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
catch (countError) {
|
|
5890
|
+
// Ignore errors from count method, it's optional
|
|
5891
|
+
console.warn('Error getting noun count:', countError);
|
|
5892
|
+
}
|
|
5893
|
+
// Check if the adapter has a paginated method for getting nouns
|
|
5894
|
+
if (typeof this.getNounsWithPagination === 'function') {
|
|
5895
|
+
// Use the adapter's paginated method
|
|
5896
|
+
const result = await this.getNounsWithPagination({
|
|
5897
|
+
limit,
|
|
5898
|
+
cursor,
|
|
5899
|
+
filter: options?.filter
|
|
5900
|
+
});
|
|
5901
|
+
// Apply offset if needed (some adapters might not support offset)
|
|
5902
|
+
const items = result.items.slice(offset);
|
|
5903
|
+
return {
|
|
5904
|
+
items,
|
|
5905
|
+
totalCount: result.totalCount || totalCount,
|
|
5906
|
+
hasMore: result.hasMore,
|
|
5907
|
+
nextCursor: result.nextCursor
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
// If the adapter doesn't have a paginated method, fall back to the old approach
|
|
5911
|
+
// but with a warning and a reasonable limit
|
|
5912
|
+
console.warn('Storage adapter does not support pagination, falling back to loading all nouns. This may cause performance issues with large datasets.');
|
|
5913
|
+
// Get nouns with a reasonable limit to avoid memory issues
|
|
5914
|
+
const maxNouns = Math.min(offset + limit + 100, 1000); // Reasonable limit
|
|
5915
|
+
let allNouns = [];
|
|
5916
|
+
try {
|
|
5917
|
+
// Try to get only the nouns we need
|
|
5918
|
+
allNouns = await this.getAllNouns_internal();
|
|
5919
|
+
// If we have too many nouns, truncate the array to avoid memory issues
|
|
5920
|
+
if (allNouns.length > maxNouns) {
|
|
5921
|
+
console.warn(`Large number of nouns (${allNouns.length}), truncating to ${maxNouns} for filtering`);
|
|
5922
|
+
allNouns = allNouns.slice(0, maxNouns);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
catch (error) {
|
|
5926
|
+
console.error('Error getting all nouns:', error);
|
|
5927
|
+
// Return empty result on error
|
|
5928
|
+
return {
|
|
5929
|
+
items: [],
|
|
5930
|
+
totalCount: 0,
|
|
5931
|
+
hasMore: false
|
|
5932
|
+
};
|
|
5933
|
+
}
|
|
5934
|
+
// Apply filtering if needed
|
|
5935
|
+
let filteredNouns = allNouns;
|
|
5936
|
+
if (options?.filter) {
|
|
5937
|
+
// Filter by noun type
|
|
5938
|
+
if (options.filter.nounType) {
|
|
5939
|
+
const nounTypes = Array.isArray(options.filter.nounType)
|
|
5940
|
+
? options.filter.nounType
|
|
5941
|
+
: [options.filter.nounType];
|
|
5942
|
+
filteredNouns = filteredNouns.filter((noun) => {
|
|
5943
|
+
// HNSWNoun doesn't have a type property directly, check metadata
|
|
5944
|
+
const nounType = noun.metadata?.type;
|
|
5945
|
+
return typeof nounType === 'string' && nounTypes.includes(nounType);
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
// Filter by service
|
|
5949
|
+
if (options.filter.service) {
|
|
5950
|
+
const services = Array.isArray(options.filter.service)
|
|
5951
|
+
? options.filter.service
|
|
5952
|
+
: [options.filter.service];
|
|
5953
|
+
filteredNouns = filteredNouns.filter((noun) => {
|
|
5954
|
+
// HNSWNoun doesn't have a service property directly, check metadata
|
|
5955
|
+
const service = noun.metadata?.service;
|
|
5956
|
+
return typeof service === 'string' && services.includes(service);
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
// Filter by metadata
|
|
5960
|
+
if (options.filter.metadata) {
|
|
5961
|
+
const metadataFilter = options.filter.metadata;
|
|
5962
|
+
filteredNouns = filteredNouns.filter((noun) => {
|
|
5963
|
+
if (!noun.metadata)
|
|
5964
|
+
return false;
|
|
5965
|
+
// Check if all metadata keys match
|
|
5966
|
+
return Object.entries(metadataFilter).every(([key, value]) => noun.metadata && noun.metadata[key] === value);
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
// Get total count before pagination
|
|
5971
|
+
totalCount = totalCount || filteredNouns.length;
|
|
5972
|
+
// Apply pagination
|
|
5973
|
+
const paginatedNouns = filteredNouns.slice(offset, offset + limit);
|
|
5974
|
+
const hasMore = offset + limit < filteredNouns.length || filteredNouns.length >= maxNouns;
|
|
5975
|
+
// Set next cursor if there are more items
|
|
5976
|
+
let nextCursor = undefined;
|
|
5977
|
+
if (hasMore && paginatedNouns.length > 0) {
|
|
5978
|
+
const lastItem = paginatedNouns[paginatedNouns.length - 1];
|
|
5979
|
+
nextCursor = lastItem.id;
|
|
5880
5980
|
}
|
|
5981
|
+
return {
|
|
5982
|
+
items: paginatedNouns,
|
|
5983
|
+
totalCount,
|
|
5984
|
+
hasMore,
|
|
5985
|
+
nextCursor
|
|
5986
|
+
};
|
|
5881
5987
|
}
|
|
5882
5988
|
catch (error) {
|
|
5883
|
-
console.error('Error getting
|
|
5884
|
-
// Return empty result on error
|
|
5989
|
+
console.error('Error getting nouns with pagination:', error);
|
|
5885
5990
|
return {
|
|
5886
5991
|
items: [],
|
|
5887
5992
|
totalCount: 0,
|
|
5888
5993
|
hasMore: false
|
|
5889
5994
|
};
|
|
5890
5995
|
}
|
|
5891
|
-
// Apply filtering if needed
|
|
5892
|
-
let filteredNouns = allNouns;
|
|
5893
|
-
if (options?.filter) {
|
|
5894
|
-
// Filter by noun type
|
|
5895
|
-
if (options.filter.nounType) {
|
|
5896
|
-
const nounTypes = Array.isArray(options.filter.nounType)
|
|
5897
|
-
? options.filter.nounType
|
|
5898
|
-
: [options.filter.nounType];
|
|
5899
|
-
filteredNouns = filteredNouns.filter(noun => {
|
|
5900
|
-
// HNSWNoun doesn't have a type property directly, check metadata
|
|
5901
|
-
const nounType = noun.metadata?.type;
|
|
5902
|
-
return typeof nounType === 'string' && nounTypes.includes(nounType);
|
|
5903
|
-
});
|
|
5904
|
-
}
|
|
5905
|
-
// Filter by service
|
|
5906
|
-
if (options.filter.service) {
|
|
5907
|
-
const services = Array.isArray(options.filter.service)
|
|
5908
|
-
? options.filter.service
|
|
5909
|
-
: [options.filter.service];
|
|
5910
|
-
filteredNouns = filteredNouns.filter(noun => {
|
|
5911
|
-
// HNSWNoun doesn't have a service property directly, check metadata
|
|
5912
|
-
const service = noun.metadata?.service;
|
|
5913
|
-
return typeof service === 'string' && services.includes(service);
|
|
5914
|
-
});
|
|
5915
|
-
}
|
|
5916
|
-
// Filter by metadata
|
|
5917
|
-
if (options.filter.metadata) {
|
|
5918
|
-
const metadataFilter = options.filter.metadata;
|
|
5919
|
-
filteredNouns = filteredNouns.filter(noun => {
|
|
5920
|
-
if (!noun.metadata)
|
|
5921
|
-
return false;
|
|
5922
|
-
// Check if all metadata keys match
|
|
5923
|
-
return Object.entries(metadataFilter).every(([key, value]) => noun.metadata && noun.metadata[key] === value);
|
|
5924
|
-
});
|
|
5925
|
-
}
|
|
5926
|
-
}
|
|
5927
|
-
// Get total count before pagination
|
|
5928
|
-
const totalCount = filteredNouns.length;
|
|
5929
|
-
// Apply pagination
|
|
5930
|
-
const paginatedNouns = filteredNouns.slice(offset, offset + limit);
|
|
5931
|
-
const hasMore = offset + limit < totalCount;
|
|
5932
|
-
// Set next cursor if there are more items
|
|
5933
|
-
let nextCursor = undefined;
|
|
5934
|
-
if (hasMore && paginatedNouns.length > 0) {
|
|
5935
|
-
const lastItem = paginatedNouns[paginatedNouns.length - 1];
|
|
5936
|
-
nextCursor = lastItem.id;
|
|
5937
|
-
}
|
|
5938
|
-
return {
|
|
5939
|
-
items: paginatedNouns,
|
|
5940
|
-
totalCount,
|
|
5941
|
-
hasMore,
|
|
5942
|
-
nextCursor
|
|
5943
|
-
};
|
|
5944
5996
|
}
|
|
5945
5997
|
/**
|
|
5946
5998
|
* Get verbs with pagination and filtering
|
|
@@ -5953,11 +6005,14 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
5953
6005
|
const pagination = options?.pagination || {};
|
|
5954
6006
|
const limit = pagination.limit || 100;
|
|
5955
6007
|
const offset = pagination.offset || 0;
|
|
6008
|
+
const cursor = pagination.cursor;
|
|
5956
6009
|
// Optimize for common filter cases to avoid loading all verbs
|
|
5957
6010
|
if (options?.filter) {
|
|
5958
6011
|
// If filtering by sourceId only, use the optimized method
|
|
5959
|
-
if (options.filter.sourceId &&
|
|
5960
|
-
!options.filter.
|
|
6012
|
+
if (options.filter.sourceId &&
|
|
6013
|
+
!options.filter.verbType &&
|
|
6014
|
+
!options.filter.targetId &&
|
|
6015
|
+
!options.filter.service &&
|
|
5961
6016
|
!options.filter.metadata) {
|
|
5962
6017
|
const sourceId = Array.isArray(options.filter.sourceId)
|
|
5963
6018
|
? options.filter.sourceId[0]
|
|
@@ -5981,8 +6036,10 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
5981
6036
|
};
|
|
5982
6037
|
}
|
|
5983
6038
|
// If filtering by targetId only, use the optimized method
|
|
5984
|
-
if (options.filter.targetId &&
|
|
5985
|
-
!options.filter.
|
|
6039
|
+
if (options.filter.targetId &&
|
|
6040
|
+
!options.filter.verbType &&
|
|
6041
|
+
!options.filter.sourceId &&
|
|
6042
|
+
!options.filter.service &&
|
|
5986
6043
|
!options.filter.metadata) {
|
|
5987
6044
|
const targetId = Array.isArray(options.filter.targetId)
|
|
5988
6045
|
? options.filter.targetId[0]
|
|
@@ -6006,8 +6063,10 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
6006
6063
|
};
|
|
6007
6064
|
}
|
|
6008
6065
|
// If filtering by verbType only, use the optimized method
|
|
6009
|
-
if (options.filter.verbType &&
|
|
6010
|
-
!options.filter.
|
|
6066
|
+
if (options.filter.verbType &&
|
|
6067
|
+
!options.filter.sourceId &&
|
|
6068
|
+
!options.filter.targetId &&
|
|
6069
|
+
!options.filter.service &&
|
|
6011
6070
|
!options.filter.metadata) {
|
|
6012
6071
|
const verbType = Array.isArray(options.filter.verbType)
|
|
6013
6072
|
? options.filter.verbType[0]
|
|
@@ -6031,91 +6090,134 @@ class BaseStorage extends BaseStorageAdapter {
|
|
|
6031
6090
|
};
|
|
6032
6091
|
}
|
|
6033
6092
|
}
|
|
6034
|
-
// For more complex filtering or no filtering,
|
|
6035
|
-
//
|
|
6036
|
-
const maxVerbs = offset + limit + 1; // Get one extra to check if there are more
|
|
6037
|
-
let allVerbs = [];
|
|
6093
|
+
// For more complex filtering or no filtering, use a paginated approach
|
|
6094
|
+
// that avoids loading all verbs into memory at once
|
|
6038
6095
|
try {
|
|
6039
|
-
//
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6096
|
+
// First, try to get a count of total verbs (if the adapter supports it)
|
|
6097
|
+
let totalCount = undefined;
|
|
6098
|
+
try {
|
|
6099
|
+
// This is an optional method that adapters may implement
|
|
6100
|
+
if (typeof this.countVerbs === 'function') {
|
|
6101
|
+
totalCount = await this.countVerbs(options?.filter);
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
catch (countError) {
|
|
6105
|
+
// Ignore errors from count method, it's optional
|
|
6106
|
+
console.warn('Error getting verb count:', countError);
|
|
6107
|
+
}
|
|
6108
|
+
// Check if the adapter has a paginated method for getting verbs
|
|
6109
|
+
if (typeof this.getVerbsWithPagination === 'function') {
|
|
6110
|
+
// Use the adapter's paginated method
|
|
6111
|
+
const result = await this.getVerbsWithPagination({
|
|
6112
|
+
limit,
|
|
6113
|
+
cursor,
|
|
6114
|
+
filter: options?.filter
|
|
6115
|
+
});
|
|
6116
|
+
// Apply offset if needed (some adapters might not support offset)
|
|
6117
|
+
const items = result.items.slice(offset);
|
|
6118
|
+
return {
|
|
6119
|
+
items,
|
|
6120
|
+
totalCount: result.totalCount || totalCount,
|
|
6121
|
+
hasMore: result.hasMore,
|
|
6122
|
+
nextCursor: result.nextCursor
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
// If the adapter doesn't have a paginated method, fall back to the old approach
|
|
6126
|
+
// but with a warning and a reasonable limit
|
|
6127
|
+
console.warn('Storage adapter does not support pagination, falling back to loading all verbs. This may cause performance issues with large datasets.');
|
|
6128
|
+
// Get verbs with a reasonable limit to avoid memory issues
|
|
6129
|
+
const maxVerbs = Math.min(offset + limit + 100, 1000); // Reasonable limit
|
|
6130
|
+
let allVerbs = [];
|
|
6131
|
+
try {
|
|
6132
|
+
// Try to get only the verbs we need
|
|
6133
|
+
allVerbs = await this.getAllVerbs_internal();
|
|
6134
|
+
// If we have too many verbs, truncate the array to avoid memory issues
|
|
6135
|
+
if (allVerbs.length > maxVerbs) {
|
|
6136
|
+
console.warn(`Large number of verbs (${allVerbs.length}), truncating to ${maxVerbs} for filtering`);
|
|
6137
|
+
allVerbs = allVerbs.slice(0, maxVerbs);
|
|
6138
|
+
}
|
|
6045
6139
|
}
|
|
6140
|
+
catch (error) {
|
|
6141
|
+
console.error('Error getting all verbs:', error);
|
|
6142
|
+
// Return empty result on error
|
|
6143
|
+
return {
|
|
6144
|
+
items: [],
|
|
6145
|
+
totalCount: 0,
|
|
6146
|
+
hasMore: false
|
|
6147
|
+
};
|
|
6148
|
+
}
|
|
6149
|
+
// Apply filtering if needed
|
|
6150
|
+
let filteredVerbs = allVerbs;
|
|
6151
|
+
if (options?.filter) {
|
|
6152
|
+
// Filter by verb type
|
|
6153
|
+
if (options.filter.verbType) {
|
|
6154
|
+
const verbTypes = Array.isArray(options.filter.verbType)
|
|
6155
|
+
? options.filter.verbType
|
|
6156
|
+
: [options.filter.verbType];
|
|
6157
|
+
filteredVerbs = filteredVerbs.filter((verb) => verb.type !== undefined && verbTypes.includes(verb.type));
|
|
6158
|
+
}
|
|
6159
|
+
// Filter by source ID
|
|
6160
|
+
if (options.filter.sourceId) {
|
|
6161
|
+
const sourceIds = Array.isArray(options.filter.sourceId)
|
|
6162
|
+
? options.filter.sourceId
|
|
6163
|
+
: [options.filter.sourceId];
|
|
6164
|
+
filteredVerbs = filteredVerbs.filter((verb) => verb.sourceId !== undefined && sourceIds.includes(verb.sourceId));
|
|
6165
|
+
}
|
|
6166
|
+
// Filter by target ID
|
|
6167
|
+
if (options.filter.targetId) {
|
|
6168
|
+
const targetIds = Array.isArray(options.filter.targetId)
|
|
6169
|
+
? options.filter.targetId
|
|
6170
|
+
: [options.filter.targetId];
|
|
6171
|
+
filteredVerbs = filteredVerbs.filter((verb) => verb.targetId !== undefined && targetIds.includes(verb.targetId));
|
|
6172
|
+
}
|
|
6173
|
+
// Filter by service
|
|
6174
|
+
if (options.filter.service) {
|
|
6175
|
+
const services = Array.isArray(options.filter.service)
|
|
6176
|
+
? options.filter.service
|
|
6177
|
+
: [options.filter.service];
|
|
6178
|
+
filteredVerbs = filteredVerbs.filter((verb) => {
|
|
6179
|
+
// GraphVerb doesn't have a service property directly, check metadata
|
|
6180
|
+
const service = verb.metadata?.service;
|
|
6181
|
+
return typeof service === 'string' && services.includes(service);
|
|
6182
|
+
});
|
|
6183
|
+
}
|
|
6184
|
+
// Filter by metadata
|
|
6185
|
+
if (options.filter.metadata) {
|
|
6186
|
+
const metadataFilter = options.filter.metadata;
|
|
6187
|
+
filteredVerbs = filteredVerbs.filter((verb) => {
|
|
6188
|
+
if (!verb.metadata)
|
|
6189
|
+
return false;
|
|
6190
|
+
// Check if all metadata keys match
|
|
6191
|
+
return Object.entries(metadataFilter).every(([key, value]) => verb.metadata && verb.metadata[key] === value);
|
|
6192
|
+
});
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
// Get total count before pagination
|
|
6196
|
+
totalCount = totalCount || filteredVerbs.length;
|
|
6197
|
+
// Apply pagination
|
|
6198
|
+
const paginatedVerbs = filteredVerbs.slice(offset, offset + limit);
|
|
6199
|
+
const hasMore = offset + limit < filteredVerbs.length || filteredVerbs.length >= maxVerbs;
|
|
6200
|
+
// Set next cursor if there are more items
|
|
6201
|
+
let nextCursor = undefined;
|
|
6202
|
+
if (hasMore && paginatedVerbs.length > 0) {
|
|
6203
|
+
const lastItem = paginatedVerbs[paginatedVerbs.length - 1];
|
|
6204
|
+
nextCursor = lastItem.id;
|
|
6205
|
+
}
|
|
6206
|
+
return {
|
|
6207
|
+
items: paginatedVerbs,
|
|
6208
|
+
totalCount,
|
|
6209
|
+
hasMore,
|
|
6210
|
+
nextCursor
|
|
6211
|
+
};
|
|
6046
6212
|
}
|
|
6047
6213
|
catch (error) {
|
|
6048
|
-
console.error('Error getting
|
|
6049
|
-
// Return empty result on error
|
|
6214
|
+
console.error('Error getting verbs with pagination:', error);
|
|
6050
6215
|
return {
|
|
6051
6216
|
items: [],
|
|
6052
6217
|
totalCount: 0,
|
|
6053
6218
|
hasMore: false
|
|
6054
6219
|
};
|
|
6055
6220
|
}
|
|
6056
|
-
// Apply filtering if needed
|
|
6057
|
-
let filteredVerbs = allVerbs;
|
|
6058
|
-
if (options?.filter) {
|
|
6059
|
-
// Filter by verb type
|
|
6060
|
-
if (options.filter.verbType) {
|
|
6061
|
-
const verbTypes = Array.isArray(options.filter.verbType)
|
|
6062
|
-
? options.filter.verbType
|
|
6063
|
-
: [options.filter.verbType];
|
|
6064
|
-
filteredVerbs = filteredVerbs.filter(verb => verb.type !== undefined && verbTypes.includes(verb.type));
|
|
6065
|
-
}
|
|
6066
|
-
// Filter by source ID
|
|
6067
|
-
if (options.filter.sourceId) {
|
|
6068
|
-
const sourceIds = Array.isArray(options.filter.sourceId)
|
|
6069
|
-
? options.filter.sourceId
|
|
6070
|
-
: [options.filter.sourceId];
|
|
6071
|
-
filteredVerbs = filteredVerbs.filter(verb => verb.sourceId !== undefined && sourceIds.includes(verb.sourceId));
|
|
6072
|
-
}
|
|
6073
|
-
// Filter by target ID
|
|
6074
|
-
if (options.filter.targetId) {
|
|
6075
|
-
const targetIds = Array.isArray(options.filter.targetId)
|
|
6076
|
-
? options.filter.targetId
|
|
6077
|
-
: [options.filter.targetId];
|
|
6078
|
-
filteredVerbs = filteredVerbs.filter(verb => verb.targetId !== undefined && targetIds.includes(verb.targetId));
|
|
6079
|
-
}
|
|
6080
|
-
// Filter by service
|
|
6081
|
-
if (options.filter.service) {
|
|
6082
|
-
const services = Array.isArray(options.filter.service)
|
|
6083
|
-
? options.filter.service
|
|
6084
|
-
: [options.filter.service];
|
|
6085
|
-
filteredVerbs = filteredVerbs.filter(verb => {
|
|
6086
|
-
// GraphVerb doesn't have a service property directly, check metadata
|
|
6087
|
-
const service = verb.metadata?.service;
|
|
6088
|
-
return typeof service === 'string' && services.includes(service);
|
|
6089
|
-
});
|
|
6090
|
-
}
|
|
6091
|
-
// Filter by metadata
|
|
6092
|
-
if (options.filter.metadata) {
|
|
6093
|
-
const metadataFilter = options.filter.metadata;
|
|
6094
|
-
filteredVerbs = filteredVerbs.filter(verb => {
|
|
6095
|
-
if (!verb.metadata)
|
|
6096
|
-
return false;
|
|
6097
|
-
// Check if all metadata keys match
|
|
6098
|
-
return Object.entries(metadataFilter).every(([key, value]) => verb.metadata && verb.metadata[key] === value);
|
|
6099
|
-
});
|
|
6100
|
-
}
|
|
6101
|
-
}
|
|
6102
|
-
// Get total count before pagination
|
|
6103
|
-
const totalCount = filteredVerbs.length;
|
|
6104
|
-
// Apply pagination
|
|
6105
|
-
const paginatedVerbs = filteredVerbs.slice(offset, offset + limit);
|
|
6106
|
-
const hasMore = offset + limit < totalCount;
|
|
6107
|
-
// Set next cursor if there are more items
|
|
6108
|
-
let nextCursor = undefined;
|
|
6109
|
-
if (hasMore && paginatedVerbs.length > 0) {
|
|
6110
|
-
const lastItem = paginatedVerbs[paginatedVerbs.length - 1];
|
|
6111
|
-
nextCursor = lastItem.id;
|
|
6112
|
-
}
|
|
6113
|
-
return {
|
|
6114
|
-
items: paginatedVerbs,
|
|
6115
|
-
totalCount,
|
|
6116
|
-
hasMore,
|
|
6117
|
-
nextCursor
|
|
6118
|
-
};
|
|
6119
6221
|
}
|
|
6120
6222
|
/**
|
|
6121
6223
|
* Delete a verb from storage
|
|
@@ -7912,22 +8014,853 @@ class StorageOperationExecutors {
|
|
|
7912
8014
|
this.deleteExecutor = createOperationExecutor('delete', config);
|
|
7913
8015
|
}
|
|
7914
8016
|
/**
|
|
7915
|
-
* Execute a get operation with timeout and retry
|
|
8017
|
+
* Execute a get operation with timeout and retry
|
|
8018
|
+
*/
|
|
8019
|
+
async executeGet(operation, operationName) {
|
|
8020
|
+
return this.getExecutor(operation, operationName);
|
|
8021
|
+
}
|
|
8022
|
+
/**
|
|
8023
|
+
* Execute an add operation with timeout and retry
|
|
8024
|
+
*/
|
|
8025
|
+
async executeAdd(operation, operationName) {
|
|
8026
|
+
return this.addExecutor(operation, operationName);
|
|
8027
|
+
}
|
|
8028
|
+
/**
|
|
8029
|
+
* Execute a delete operation with timeout and retry
|
|
8030
|
+
*/
|
|
8031
|
+
async executeDelete(operation, operationName) {
|
|
8032
|
+
return this.deleteExecutor(operation, operationName);
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
|
|
8036
|
+
/**
|
|
8037
|
+
* Multi-level Cache Manager
|
|
8038
|
+
*
|
|
8039
|
+
* Implements a three-level caching strategy:
|
|
8040
|
+
* - Level 1: Hot cache (most accessed nodes) - RAM (automatically detecting and adjusting in each environment)
|
|
8041
|
+
* - Level 2: Warm cache (recent nodes) - OPFS, Filesystem or S3 depending on environment
|
|
8042
|
+
* - Level 3: Cold storage (all nodes) - OPFS, Filesystem or S3 depending on environment
|
|
8043
|
+
*/
|
|
8044
|
+
// Environment detection for storage selection
|
|
8045
|
+
var Environment$1;
|
|
8046
|
+
(function (Environment) {
|
|
8047
|
+
Environment[Environment["BROWSER"] = 0] = "BROWSER";
|
|
8048
|
+
Environment[Environment["NODE"] = 1] = "NODE";
|
|
8049
|
+
Environment[Environment["WORKER"] = 2] = "WORKER";
|
|
8050
|
+
})(Environment$1 || (Environment$1 = {}));
|
|
8051
|
+
// Storage type for warm and cold caches
|
|
8052
|
+
var StorageType;
|
|
8053
|
+
(function (StorageType) {
|
|
8054
|
+
StorageType[StorageType["MEMORY"] = 0] = "MEMORY";
|
|
8055
|
+
StorageType[StorageType["OPFS"] = 1] = "OPFS";
|
|
8056
|
+
StorageType[StorageType["FILESYSTEM"] = 2] = "FILESYSTEM";
|
|
8057
|
+
StorageType[StorageType["S3"] = 3] = "S3";
|
|
8058
|
+
})(StorageType || (StorageType = {}));
|
|
8059
|
+
/**
|
|
8060
|
+
* Multi-level cache manager for efficient data access
|
|
8061
|
+
*/
|
|
8062
|
+
class CacheManager {
|
|
8063
|
+
/**
|
|
8064
|
+
* Initialize the cache manager
|
|
8065
|
+
* @param options Configuration options
|
|
8066
|
+
*/
|
|
8067
|
+
constructor(options = {}) {
|
|
8068
|
+
// Hot cache (RAM)
|
|
8069
|
+
this.hotCache = new Map();
|
|
8070
|
+
// Cache statistics
|
|
8071
|
+
this.stats = {
|
|
8072
|
+
hits: 0,
|
|
8073
|
+
misses: 0,
|
|
8074
|
+
evictions: 0,
|
|
8075
|
+
size: 0,
|
|
8076
|
+
maxSize: 0
|
|
8077
|
+
};
|
|
8078
|
+
this.lastAutoTuneTime = 0;
|
|
8079
|
+
this.autoTuneInterval = 5 * 60 * 1000; // 5 minutes
|
|
8080
|
+
this.storageStatistics = null;
|
|
8081
|
+
// Detect environment
|
|
8082
|
+
this.environment = this.detectEnvironment();
|
|
8083
|
+
// Set storage types based on environment
|
|
8084
|
+
this.warmStorageType = this.detectWarmStorageType();
|
|
8085
|
+
this.coldStorageType = this.detectColdStorageType();
|
|
8086
|
+
// Initialize storage adapters
|
|
8087
|
+
this.warmStorage = options.warmStorage || this.initializeWarmStorage();
|
|
8088
|
+
this.coldStorage = options.coldStorage || this.initializeColdStorage();
|
|
8089
|
+
// Set auto-tuning flag
|
|
8090
|
+
this.autoTune = options.autoTune !== undefined ? options.autoTune : true;
|
|
8091
|
+
// Set default values or use provided values
|
|
8092
|
+
this.hotCacheMaxSize = options.hotCacheMaxSize || this.detectOptimalCacheSize();
|
|
8093
|
+
this.hotCacheEvictionThreshold = options.hotCacheEvictionThreshold || 0.8;
|
|
8094
|
+
this.warmCacheTTL = options.warmCacheTTL || 24 * 60 * 60 * 1000; // 24 hours
|
|
8095
|
+
this.batchSize = options.batchSize || 10;
|
|
8096
|
+
// If auto-tuning is enabled, perform initial tuning
|
|
8097
|
+
if (this.autoTune) {
|
|
8098
|
+
this.tuneParameters();
|
|
8099
|
+
}
|
|
8100
|
+
// Log configuration
|
|
8101
|
+
if (process.env.DEBUG) {
|
|
8102
|
+
console.log('Cache Manager initialized with configuration:', {
|
|
8103
|
+
environment: Environment$1[this.environment],
|
|
8104
|
+
hotCacheMaxSize: this.hotCacheMaxSize,
|
|
8105
|
+
hotCacheEvictionThreshold: this.hotCacheEvictionThreshold,
|
|
8106
|
+
warmCacheTTL: this.warmCacheTTL,
|
|
8107
|
+
batchSize: this.batchSize,
|
|
8108
|
+
autoTune: this.autoTune,
|
|
8109
|
+
warmStorageType: StorageType[this.warmStorageType],
|
|
8110
|
+
coldStorageType: StorageType[this.coldStorageType]
|
|
8111
|
+
});
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
/**
|
|
8115
|
+
* Detect the current environment
|
|
8116
|
+
*/
|
|
8117
|
+
detectEnvironment() {
|
|
8118
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
8119
|
+
return Environment$1.BROWSER;
|
|
8120
|
+
}
|
|
8121
|
+
else if (typeof self !== 'undefined' && typeof window === 'undefined') {
|
|
8122
|
+
// In a worker environment, self is defined but window is not
|
|
8123
|
+
return Environment$1.WORKER;
|
|
8124
|
+
}
|
|
8125
|
+
else {
|
|
8126
|
+
return Environment$1.NODE;
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
/**
|
|
8130
|
+
* Detect the optimal cache size based on available memory
|
|
8131
|
+
*/
|
|
8132
|
+
detectOptimalCacheSize() {
|
|
8133
|
+
try {
|
|
8134
|
+
// Default to a conservative value
|
|
8135
|
+
const defaultSize = 1000;
|
|
8136
|
+
// In Node.js, use available system memory
|
|
8137
|
+
if (this.environment === Environment$1.NODE) {
|
|
8138
|
+
try {
|
|
8139
|
+
// Use dynamic import to avoid ESLint warning
|
|
8140
|
+
const getOS = () => {
|
|
8141
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8142
|
+
return require('os');
|
|
8143
|
+
};
|
|
8144
|
+
const os = getOS();
|
|
8145
|
+
const freeMemory = os.freemem();
|
|
8146
|
+
// Estimate average entry size (in bytes)
|
|
8147
|
+
// This is a conservative estimate for complex objects with vectors
|
|
8148
|
+
const ESTIMATED_BYTES_PER_ENTRY = 1024; // 1KB per entry
|
|
8149
|
+
// Use 10% of free memory, with a minimum of 1000 entries
|
|
8150
|
+
const optimalSize = Math.max(Math.floor(freeMemory * 0.1 / ESTIMATED_BYTES_PER_ENTRY), 1000);
|
|
8151
|
+
return optimalSize;
|
|
8152
|
+
}
|
|
8153
|
+
catch (error) {
|
|
8154
|
+
console.warn('Failed to detect optimal cache size:', error);
|
|
8155
|
+
return defaultSize;
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
// In browser, use navigator.deviceMemory if available
|
|
8159
|
+
if (this.environment === Environment$1.BROWSER && navigator.deviceMemory) {
|
|
8160
|
+
// deviceMemory is in GB, scale accordingly
|
|
8161
|
+
return Math.max(navigator.deviceMemory * 500, 1000);
|
|
8162
|
+
}
|
|
8163
|
+
return defaultSize;
|
|
8164
|
+
}
|
|
8165
|
+
catch (error) {
|
|
8166
|
+
console.warn('Error detecting optimal cache size:', error);
|
|
8167
|
+
return 1000; // Conservative default
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
/**
|
|
8171
|
+
* Tune cache parameters based on statistics and environment
|
|
8172
|
+
* This method is called periodically if auto-tuning is enabled
|
|
8173
|
+
*
|
|
8174
|
+
* The auto-tuning process:
|
|
8175
|
+
* 1. Retrieves storage statistics if available
|
|
8176
|
+
* 2. Tunes each parameter based on statistics and environment
|
|
8177
|
+
* 3. Logs the tuned parameters if debug is enabled
|
|
8178
|
+
*
|
|
8179
|
+
* Auto-tuning helps optimize cache performance by adapting to:
|
|
8180
|
+
* - The current environment (Node.js, browser, worker)
|
|
8181
|
+
* - Available system resources (memory, CPU)
|
|
8182
|
+
* - Usage patterns (read-heavy vs. write-heavy workloads)
|
|
8183
|
+
* - Cache efficiency (hit/miss ratios)
|
|
8184
|
+
*/
|
|
8185
|
+
async tuneParameters() {
|
|
8186
|
+
// Skip if auto-tuning is disabled
|
|
8187
|
+
if (!this.autoTune)
|
|
8188
|
+
return;
|
|
8189
|
+
// Check if it's time to tune parameters
|
|
8190
|
+
const now = Date.now();
|
|
8191
|
+
if (now - this.lastAutoTuneTime < this.autoTuneInterval)
|
|
8192
|
+
return;
|
|
8193
|
+
// Update last tune time
|
|
8194
|
+
this.lastAutoTuneTime = now;
|
|
8195
|
+
try {
|
|
8196
|
+
// Get storage statistics if available
|
|
8197
|
+
if (this.coldStorage && typeof this.coldStorage.getStatistics === 'function') {
|
|
8198
|
+
this.storageStatistics = await this.coldStorage.getStatistics();
|
|
8199
|
+
}
|
|
8200
|
+
// Tune hot cache size
|
|
8201
|
+
this.tuneHotCacheSize();
|
|
8202
|
+
// Tune eviction threshold
|
|
8203
|
+
this.tuneEvictionThreshold();
|
|
8204
|
+
// Tune warm cache TTL
|
|
8205
|
+
this.tuneWarmCacheTTL();
|
|
8206
|
+
// Tune batch size
|
|
8207
|
+
this.tuneBatchSize();
|
|
8208
|
+
// Log tuned parameters if debug is enabled
|
|
8209
|
+
if (process.env.DEBUG) {
|
|
8210
|
+
console.log('Cache parameters auto-tuned:', {
|
|
8211
|
+
hotCacheMaxSize: this.hotCacheMaxSize,
|
|
8212
|
+
hotCacheEvictionThreshold: this.hotCacheEvictionThreshold,
|
|
8213
|
+
warmCacheTTL: this.warmCacheTTL,
|
|
8214
|
+
batchSize: this.batchSize
|
|
8215
|
+
});
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
catch (error) {
|
|
8219
|
+
console.warn('Error during cache parameter auto-tuning:', error);
|
|
8220
|
+
}
|
|
8221
|
+
}
|
|
8222
|
+
/**
|
|
8223
|
+
* Tune hot cache size based on statistics and environment
|
|
8224
|
+
*
|
|
8225
|
+
* The hot cache size is tuned based on:
|
|
8226
|
+
* 1. Available memory in the current environment
|
|
8227
|
+
* 2. Total number of nodes and edges in the system
|
|
8228
|
+
* 3. Cache hit/miss ratio
|
|
8229
|
+
*
|
|
8230
|
+
* Algorithm:
|
|
8231
|
+
* - Start with a size based on available memory
|
|
8232
|
+
* - If storage statistics are available, consider caching a percentage of total items
|
|
8233
|
+
* - If hit ratio is low, increase the cache size to improve performance
|
|
8234
|
+
* - Ensure a reasonable minimum size to maintain basic functionality
|
|
8235
|
+
*/
|
|
8236
|
+
tuneHotCacheSize() {
|
|
8237
|
+
// Start with the base size from environment detection
|
|
8238
|
+
let optimalSize = this.detectOptimalCacheSize();
|
|
8239
|
+
// If we have storage statistics, adjust based on total nodes/edges
|
|
8240
|
+
if (this.storageStatistics) {
|
|
8241
|
+
const totalItems = (this.storageStatistics.totalNodes || 0) +
|
|
8242
|
+
(this.storageStatistics.totalEdges || 0);
|
|
8243
|
+
// If total items is significant, adjust cache size
|
|
8244
|
+
if (totalItems > 0) {
|
|
8245
|
+
// Use a percentage of total items, with a cap based on memory
|
|
8246
|
+
const percentageToCache = 0.2; // Cache 20% of items by default
|
|
8247
|
+
const statisticsBasedSize = Math.ceil(totalItems * percentageToCache);
|
|
8248
|
+
// Use the smaller of the two to avoid memory issues
|
|
8249
|
+
optimalSize = Math.min(optimalSize, statisticsBasedSize);
|
|
8250
|
+
}
|
|
8251
|
+
}
|
|
8252
|
+
// Adjust based on hit/miss ratio if we have enough data
|
|
8253
|
+
const totalAccesses = this.stats.hits + this.stats.misses;
|
|
8254
|
+
if (totalAccesses > 100) {
|
|
8255
|
+
const hitRatio = this.stats.hits / totalAccesses;
|
|
8256
|
+
// If hit ratio is high, we might have a good cache size already
|
|
8257
|
+
// If hit ratio is low, we might need a larger cache
|
|
8258
|
+
if (hitRatio < 0.5) {
|
|
8259
|
+
// Increase cache size by up to 50% if hit ratio is low
|
|
8260
|
+
const hitRatioFactor = 1 + (0.5 - hitRatio);
|
|
8261
|
+
optimalSize = Math.ceil(optimalSize * hitRatioFactor);
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
// Ensure we have a reasonable minimum size
|
|
8265
|
+
optimalSize = Math.max(optimalSize, 1000);
|
|
8266
|
+
// Update the hot cache max size
|
|
8267
|
+
this.hotCacheMaxSize = optimalSize;
|
|
8268
|
+
this.stats.maxSize = optimalSize;
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* Tune eviction threshold based on statistics
|
|
8272
|
+
*
|
|
8273
|
+
* The eviction threshold determines when items start being evicted from the hot cache.
|
|
8274
|
+
* It is tuned based on:
|
|
8275
|
+
* 1. Cache hit/miss ratio
|
|
8276
|
+
* 2. Operation patterns (read-heavy vs. write-heavy workloads)
|
|
8277
|
+
*
|
|
8278
|
+
* Algorithm:
|
|
8279
|
+
* - Start with a default threshold of 0.8 (80% of max size)
|
|
8280
|
+
* - For high hit ratios, increase the threshold to keep more items in cache
|
|
8281
|
+
* - For low hit ratios, decrease the threshold to evict items more aggressively
|
|
8282
|
+
* - For read-heavy workloads, use a higher threshold
|
|
8283
|
+
* - For write-heavy workloads, use a lower threshold
|
|
8284
|
+
*/
|
|
8285
|
+
tuneEvictionThreshold() {
|
|
8286
|
+
// Default threshold
|
|
8287
|
+
let threshold = 0.8;
|
|
8288
|
+
// Adjust based on hit/miss ratio if we have enough data
|
|
8289
|
+
const totalAccesses = this.stats.hits + this.stats.misses;
|
|
8290
|
+
if (totalAccesses > 100) {
|
|
8291
|
+
const hitRatio = this.stats.hits / totalAccesses;
|
|
8292
|
+
// If hit ratio is high, we can use a higher threshold
|
|
8293
|
+
// If hit ratio is low, we should use a lower threshold to evict more aggressively
|
|
8294
|
+
if (hitRatio > 0.8) {
|
|
8295
|
+
// High hit ratio, increase threshold (up to 0.9)
|
|
8296
|
+
threshold = Math.min(0.9, 0.8 + (hitRatio - 0.8));
|
|
8297
|
+
}
|
|
8298
|
+
else if (hitRatio < 0.5) {
|
|
8299
|
+
// Low hit ratio, decrease threshold (down to 0.6)
|
|
8300
|
+
threshold = Math.max(0.6, 0.8 - (0.5 - hitRatio));
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
// If we have storage statistics with operation counts, adjust based on operation patterns
|
|
8304
|
+
if (this.storageStatistics && this.storageStatistics.operations) {
|
|
8305
|
+
const ops = this.storageStatistics.operations;
|
|
8306
|
+
const totalOps = ops.total || 1;
|
|
8307
|
+
// Calculate read/write ratio
|
|
8308
|
+
const readOps = ops.search || 0;
|
|
8309
|
+
const writeOps = (ops.add || 0) + (ops.update || 0) + (ops.delete || 0);
|
|
8310
|
+
if (totalOps > 100) {
|
|
8311
|
+
const readRatio = readOps / totalOps;
|
|
8312
|
+
const writeRatio = writeOps / totalOps;
|
|
8313
|
+
// For read-heavy workloads, use higher threshold
|
|
8314
|
+
// For write-heavy workloads, use lower threshold
|
|
8315
|
+
if (readRatio > 0.8) {
|
|
8316
|
+
// Read-heavy, increase threshold slightly
|
|
8317
|
+
threshold = Math.min(0.9, threshold + 0.05);
|
|
8318
|
+
}
|
|
8319
|
+
else if (writeRatio > 0.5) {
|
|
8320
|
+
// Write-heavy, decrease threshold
|
|
8321
|
+
threshold = Math.max(0.6, threshold - 0.1);
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
// Update the eviction threshold
|
|
8326
|
+
this.hotCacheEvictionThreshold = threshold;
|
|
8327
|
+
}
|
|
8328
|
+
/**
|
|
8329
|
+
* Tune warm cache TTL based on statistics
|
|
8330
|
+
*
|
|
8331
|
+
* The warm cache TTL determines how long items remain in the warm cache.
|
|
8332
|
+
* It is tuned based on:
|
|
8333
|
+
* 1. Update frequency from operation statistics
|
|
8334
|
+
*
|
|
8335
|
+
* Algorithm:
|
|
8336
|
+
* - Start with a default TTL of 24 hours
|
|
8337
|
+
* - For frequently updated data, use a shorter TTL
|
|
8338
|
+
* - For rarely updated data, use a longer TTL
|
|
8339
|
+
*/
|
|
8340
|
+
tuneWarmCacheTTL() {
|
|
8341
|
+
// Default TTL (24 hours)
|
|
8342
|
+
let ttl = 24 * 60 * 60 * 1000;
|
|
8343
|
+
// If we have storage statistics with operation counts, adjust based on update frequency
|
|
8344
|
+
if (this.storageStatistics && this.storageStatistics.operations) {
|
|
8345
|
+
const ops = this.storageStatistics.operations;
|
|
8346
|
+
const totalOps = ops.total || 1;
|
|
8347
|
+
const updateOps = (ops.update || 0);
|
|
8348
|
+
if (totalOps > 100) {
|
|
8349
|
+
const updateRatio = updateOps / totalOps;
|
|
8350
|
+
// For frequently updated data, use shorter TTL
|
|
8351
|
+
// For rarely updated data, use longer TTL
|
|
8352
|
+
if (updateRatio > 0.3) {
|
|
8353
|
+
// Frequently updated, decrease TTL (down to 6 hours)
|
|
8354
|
+
ttl = Math.max(6 * 60 * 60 * 1000, ttl * (1 - updateRatio));
|
|
8355
|
+
}
|
|
8356
|
+
else if (updateRatio < 0.1) {
|
|
8357
|
+
// Rarely updated, increase TTL (up to 48 hours)
|
|
8358
|
+
ttl = Math.min(48 * 60 * 60 * 1000, ttl * (1.5 - updateRatio));
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
// Update the warm cache TTL
|
|
8363
|
+
this.warmCacheTTL = ttl;
|
|
8364
|
+
}
|
|
8365
|
+
/**
|
|
8366
|
+
* Tune batch size based on statistics and environment
|
|
8367
|
+
*
|
|
8368
|
+
* The batch size determines how many items are processed in a single batch
|
|
8369
|
+
* for operations like prefetching. It is tuned based on:
|
|
8370
|
+
* 1. Current environment (Node.js, browser, worker)
|
|
8371
|
+
* 2. Available memory
|
|
8372
|
+
* 3. Operation patterns
|
|
8373
|
+
* 4. Cache hit/miss ratio
|
|
8374
|
+
*
|
|
8375
|
+
* Algorithm:
|
|
8376
|
+
* - Start with a default based on the environment
|
|
8377
|
+
* - Adjust based on available memory in browsers
|
|
8378
|
+
* - For bulk-heavy workloads, use a larger batch size
|
|
8379
|
+
* - For high hit ratios, use smaller batches (items likely in cache)
|
|
8380
|
+
* - For low hit ratios, use larger batches (need to fetch more items)
|
|
8381
|
+
*/
|
|
8382
|
+
tuneBatchSize() {
|
|
8383
|
+
// Default batch size
|
|
8384
|
+
let batchSize = 10;
|
|
8385
|
+
// Adjust based on environment
|
|
8386
|
+
if (this.environment === Environment$1.NODE) {
|
|
8387
|
+
// Node.js can handle larger batches
|
|
8388
|
+
batchSize = 20;
|
|
8389
|
+
}
|
|
8390
|
+
else if (this.environment === Environment$1.BROWSER) {
|
|
8391
|
+
// Browsers might need smaller batches
|
|
8392
|
+
batchSize = 10;
|
|
8393
|
+
// If we have memory information, adjust accordingly
|
|
8394
|
+
if (navigator.deviceMemory) {
|
|
8395
|
+
// Scale batch size with available memory
|
|
8396
|
+
batchSize = Math.max(5, Math.min(20, Math.floor(navigator.deviceMemory * 2)));
|
|
8397
|
+
}
|
|
8398
|
+
}
|
|
8399
|
+
// If we have storage statistics with operation counts, adjust based on operation patterns
|
|
8400
|
+
if (this.storageStatistics && this.storageStatistics.operations) {
|
|
8401
|
+
const ops = this.storageStatistics.operations;
|
|
8402
|
+
const totalOps = ops.total || 1;
|
|
8403
|
+
const bulkOps = (ops.search || 0);
|
|
8404
|
+
if (totalOps > 100) {
|
|
8405
|
+
const bulkRatio = bulkOps / totalOps;
|
|
8406
|
+
// For bulk-heavy workloads, use larger batch size
|
|
8407
|
+
if (bulkRatio > 0.7) {
|
|
8408
|
+
// Bulk-heavy, increase batch size (up to 2x)
|
|
8409
|
+
batchSize = Math.min(50, Math.ceil(batchSize * 1.5));
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
// Adjust based on hit/miss ratio if we have enough data
|
|
8414
|
+
const totalAccesses = this.stats.hits + this.stats.misses;
|
|
8415
|
+
if (totalAccesses > 100) {
|
|
8416
|
+
const hitRatio = this.stats.hits / totalAccesses;
|
|
8417
|
+
// If hit ratio is high, we can use smaller batches
|
|
8418
|
+
// If hit ratio is low, we might need larger batches
|
|
8419
|
+
if (hitRatio > 0.8) {
|
|
8420
|
+
// High hit ratio, decrease batch size slightly
|
|
8421
|
+
batchSize = Math.max(5, Math.floor(batchSize * 0.8));
|
|
8422
|
+
}
|
|
8423
|
+
else if (hitRatio < 0.5) {
|
|
8424
|
+
// Low hit ratio, increase batch size
|
|
8425
|
+
batchSize = Math.min(50, Math.ceil(batchSize * 1.2));
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
// Update the batch size
|
|
8429
|
+
this.batchSize = batchSize;
|
|
8430
|
+
}
|
|
8431
|
+
/**
|
|
8432
|
+
* Detect the appropriate warm storage type based on environment
|
|
8433
|
+
*/
|
|
8434
|
+
detectWarmStorageType() {
|
|
8435
|
+
if (this.environment === Environment$1.BROWSER) {
|
|
8436
|
+
// Use OPFS if available, otherwise use memory
|
|
8437
|
+
if ('storage' in navigator && 'getDirectory' in navigator.storage) {
|
|
8438
|
+
return StorageType.OPFS;
|
|
8439
|
+
}
|
|
8440
|
+
return StorageType.MEMORY;
|
|
8441
|
+
}
|
|
8442
|
+
else if (this.environment === Environment$1.WORKER) {
|
|
8443
|
+
// Use OPFS if available, otherwise use memory
|
|
8444
|
+
if ('storage' in self && 'getDirectory' in self.storage) {
|
|
8445
|
+
return StorageType.OPFS;
|
|
8446
|
+
}
|
|
8447
|
+
return StorageType.MEMORY;
|
|
8448
|
+
}
|
|
8449
|
+
else {
|
|
8450
|
+
// In Node.js, use filesystem
|
|
8451
|
+
return StorageType.FILESYSTEM;
|
|
8452
|
+
}
|
|
8453
|
+
}
|
|
8454
|
+
/**
|
|
8455
|
+
* Detect the appropriate cold storage type based on environment
|
|
8456
|
+
*/
|
|
8457
|
+
detectColdStorageType() {
|
|
8458
|
+
if (this.environment === Environment$1.BROWSER) {
|
|
8459
|
+
// Use OPFS if available, otherwise use memory
|
|
8460
|
+
if ('storage' in navigator && 'getDirectory' in navigator.storage) {
|
|
8461
|
+
return StorageType.OPFS;
|
|
8462
|
+
}
|
|
8463
|
+
return StorageType.MEMORY;
|
|
8464
|
+
}
|
|
8465
|
+
else if (this.environment === Environment$1.WORKER) {
|
|
8466
|
+
// Use OPFS if available, otherwise use memory
|
|
8467
|
+
if ('storage' in self && 'getDirectory' in self.storage) {
|
|
8468
|
+
return StorageType.OPFS;
|
|
8469
|
+
}
|
|
8470
|
+
return StorageType.MEMORY;
|
|
8471
|
+
}
|
|
8472
|
+
else {
|
|
8473
|
+
// In Node.js, use S3 if configured, otherwise filesystem
|
|
8474
|
+
return StorageType.S3;
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8477
|
+
/**
|
|
8478
|
+
* Initialize warm storage adapter
|
|
8479
|
+
*/
|
|
8480
|
+
initializeWarmStorage() {
|
|
8481
|
+
// Implementation depends on the detected storage type
|
|
8482
|
+
// For now, return null as this will be provided by the storage adapter
|
|
8483
|
+
return null;
|
|
8484
|
+
}
|
|
8485
|
+
/**
|
|
8486
|
+
* Initialize cold storage adapter
|
|
8487
|
+
*/
|
|
8488
|
+
initializeColdStorage() {
|
|
8489
|
+
// Implementation depends on the detected storage type
|
|
8490
|
+
// For now, return null as this will be provided by the storage adapter
|
|
8491
|
+
return null;
|
|
8492
|
+
}
|
|
8493
|
+
/**
|
|
8494
|
+
* Get an item from cache, trying each level in order
|
|
8495
|
+
* @param id The item ID
|
|
8496
|
+
* @returns The cached item or null if not found
|
|
8497
|
+
*/
|
|
8498
|
+
async get(id) {
|
|
8499
|
+
// Check if it's time to tune parameters
|
|
8500
|
+
await this.checkAndTuneParameters();
|
|
8501
|
+
// Try hot cache first (fastest)
|
|
8502
|
+
const hotCacheEntry = this.hotCache.get(id);
|
|
8503
|
+
if (hotCacheEntry) {
|
|
8504
|
+
// Update access metadata
|
|
8505
|
+
hotCacheEntry.lastAccessed = Date.now();
|
|
8506
|
+
hotCacheEntry.accessCount++;
|
|
8507
|
+
// Update stats
|
|
8508
|
+
this.stats.hits++;
|
|
8509
|
+
return hotCacheEntry.data;
|
|
8510
|
+
}
|
|
8511
|
+
// Try warm cache next
|
|
8512
|
+
try {
|
|
8513
|
+
const warmCacheItem = await this.getFromWarmCache(id);
|
|
8514
|
+
if (warmCacheItem) {
|
|
8515
|
+
// Promote to hot cache
|
|
8516
|
+
this.addToHotCache(id, warmCacheItem);
|
|
8517
|
+
// Update stats
|
|
8518
|
+
this.stats.hits++;
|
|
8519
|
+
return warmCacheItem;
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
catch (error) {
|
|
8523
|
+
console.warn(`Error accessing warm cache for ${id}:`, error);
|
|
8524
|
+
}
|
|
8525
|
+
// Finally, try cold storage
|
|
8526
|
+
try {
|
|
8527
|
+
const coldStorageItem = await this.getFromColdStorage(id);
|
|
8528
|
+
if (coldStorageItem) {
|
|
8529
|
+
// Promote to hot and warm caches
|
|
8530
|
+
this.addToHotCache(id, coldStorageItem);
|
|
8531
|
+
await this.addToWarmCache(id, coldStorageItem);
|
|
8532
|
+
// Update stats
|
|
8533
|
+
this.stats.misses++;
|
|
8534
|
+
return coldStorageItem;
|
|
8535
|
+
}
|
|
8536
|
+
}
|
|
8537
|
+
catch (error) {
|
|
8538
|
+
console.warn(`Error accessing cold storage for ${id}:`, error);
|
|
8539
|
+
}
|
|
8540
|
+
// Item not found in any cache level
|
|
8541
|
+
this.stats.misses++;
|
|
8542
|
+
return null;
|
|
8543
|
+
}
|
|
8544
|
+
/**
|
|
8545
|
+
* Get an item from warm cache
|
|
8546
|
+
* @param id The item ID
|
|
8547
|
+
* @returns The cached item or null if not found
|
|
8548
|
+
*/
|
|
8549
|
+
async getFromWarmCache(id) {
|
|
8550
|
+
if (!this.warmStorage)
|
|
8551
|
+
return null;
|
|
8552
|
+
try {
|
|
8553
|
+
return await this.warmStorage.get(id);
|
|
8554
|
+
}
|
|
8555
|
+
catch (error) {
|
|
8556
|
+
console.warn(`Error getting item ${id} from warm cache:`, error);
|
|
8557
|
+
return null;
|
|
8558
|
+
}
|
|
8559
|
+
}
|
|
8560
|
+
/**
|
|
8561
|
+
* Get an item from cold storage
|
|
8562
|
+
* @param id The item ID
|
|
8563
|
+
* @returns The item or null if not found
|
|
8564
|
+
*/
|
|
8565
|
+
async getFromColdStorage(id) {
|
|
8566
|
+
if (!this.coldStorage)
|
|
8567
|
+
return null;
|
|
8568
|
+
try {
|
|
8569
|
+
return await this.coldStorage.get(id);
|
|
8570
|
+
}
|
|
8571
|
+
catch (error) {
|
|
8572
|
+
console.warn(`Error getting item ${id} from cold storage:`, error);
|
|
8573
|
+
return null;
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
8576
|
+
/**
|
|
8577
|
+
* Add an item to hot cache
|
|
8578
|
+
* @param id The item ID
|
|
8579
|
+
* @param item The item to cache
|
|
8580
|
+
*/
|
|
8581
|
+
addToHotCache(id, item) {
|
|
8582
|
+
// Check if we need to evict items
|
|
8583
|
+
if (this.hotCache.size >= this.hotCacheMaxSize * this.hotCacheEvictionThreshold) {
|
|
8584
|
+
this.evictFromHotCache();
|
|
8585
|
+
}
|
|
8586
|
+
// Add to hot cache
|
|
8587
|
+
this.hotCache.set(id, {
|
|
8588
|
+
data: item,
|
|
8589
|
+
lastAccessed: Date.now(),
|
|
8590
|
+
accessCount: 1,
|
|
8591
|
+
expiresAt: null // Hot cache items don't expire
|
|
8592
|
+
});
|
|
8593
|
+
// Update stats
|
|
8594
|
+
this.stats.size = this.hotCache.size;
|
|
8595
|
+
}
|
|
8596
|
+
/**
|
|
8597
|
+
* Add an item to warm cache
|
|
8598
|
+
* @param id The item ID
|
|
8599
|
+
* @param item The item to cache
|
|
8600
|
+
*/
|
|
8601
|
+
async addToWarmCache(id, item) {
|
|
8602
|
+
if (!this.warmStorage)
|
|
8603
|
+
return;
|
|
8604
|
+
try {
|
|
8605
|
+
// Add to warm cache with TTL
|
|
8606
|
+
await this.warmStorage.set(id, item, {
|
|
8607
|
+
ttl: this.warmCacheTTL
|
|
8608
|
+
});
|
|
8609
|
+
}
|
|
8610
|
+
catch (error) {
|
|
8611
|
+
console.warn(`Error adding item ${id} to warm cache:`, error);
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
/**
|
|
8615
|
+
* Evict items from hot cache based on LRU policy
|
|
8616
|
+
*/
|
|
8617
|
+
evictFromHotCache() {
|
|
8618
|
+
// Find the least recently used items
|
|
8619
|
+
const entries = Array.from(this.hotCache.entries());
|
|
8620
|
+
// Sort by last accessed time (oldest first)
|
|
8621
|
+
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
|
|
8622
|
+
// Remove the oldest 20% of items
|
|
8623
|
+
const itemsToRemove = Math.ceil(this.hotCache.size * 0.2);
|
|
8624
|
+
for (let i = 0; i < itemsToRemove && i < entries.length; i++) {
|
|
8625
|
+
this.hotCache.delete(entries[i][0]);
|
|
8626
|
+
this.stats.evictions++;
|
|
8627
|
+
}
|
|
8628
|
+
// Update stats
|
|
8629
|
+
this.stats.size = this.hotCache.size;
|
|
8630
|
+
if (process.env.DEBUG) {
|
|
8631
|
+
console.log(`Evicted ${itemsToRemove} items from hot cache, new size: ${this.hotCache.size}`);
|
|
8632
|
+
}
|
|
8633
|
+
}
|
|
8634
|
+
/**
|
|
8635
|
+
* Set an item in all cache levels
|
|
8636
|
+
* @param id The item ID
|
|
8637
|
+
* @param item The item to cache
|
|
8638
|
+
*/
|
|
8639
|
+
async set(id, item) {
|
|
8640
|
+
// Add to hot cache
|
|
8641
|
+
this.addToHotCache(id, item);
|
|
8642
|
+
// Add to warm cache
|
|
8643
|
+
await this.addToWarmCache(id, item);
|
|
8644
|
+
// Add to cold storage
|
|
8645
|
+
if (this.coldStorage) {
|
|
8646
|
+
try {
|
|
8647
|
+
await this.coldStorage.set(id, item);
|
|
8648
|
+
}
|
|
8649
|
+
catch (error) {
|
|
8650
|
+
console.warn(`Error adding item ${id} to cold storage:`, error);
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
/**
|
|
8655
|
+
* Delete an item from all cache levels
|
|
8656
|
+
* @param id The item ID to delete
|
|
8657
|
+
*/
|
|
8658
|
+
async delete(id) {
|
|
8659
|
+
// Remove from hot cache
|
|
8660
|
+
this.hotCache.delete(id);
|
|
8661
|
+
// Remove from warm cache
|
|
8662
|
+
if (this.warmStorage) {
|
|
8663
|
+
try {
|
|
8664
|
+
await this.warmStorage.delete(id);
|
|
8665
|
+
}
|
|
8666
|
+
catch (error) {
|
|
8667
|
+
console.warn(`Error deleting item ${id} from warm cache:`, error);
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
// Remove from cold storage
|
|
8671
|
+
if (this.coldStorage) {
|
|
8672
|
+
try {
|
|
8673
|
+
await this.coldStorage.delete(id);
|
|
8674
|
+
}
|
|
8675
|
+
catch (error) {
|
|
8676
|
+
console.warn(`Error deleting item ${id} from cold storage:`, error);
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
// Update stats
|
|
8680
|
+
this.stats.size = this.hotCache.size;
|
|
8681
|
+
}
|
|
8682
|
+
/**
|
|
8683
|
+
* Clear all cache levels
|
|
8684
|
+
*/
|
|
8685
|
+
async clear() {
|
|
8686
|
+
// Clear hot cache
|
|
8687
|
+
this.hotCache.clear();
|
|
8688
|
+
// Clear warm cache
|
|
8689
|
+
if (this.warmStorage) {
|
|
8690
|
+
try {
|
|
8691
|
+
await this.warmStorage.clear();
|
|
8692
|
+
}
|
|
8693
|
+
catch (error) {
|
|
8694
|
+
console.warn('Error clearing warm cache:', error);
|
|
8695
|
+
}
|
|
8696
|
+
}
|
|
8697
|
+
// Clear cold storage
|
|
8698
|
+
if (this.coldStorage) {
|
|
8699
|
+
try {
|
|
8700
|
+
await this.coldStorage.clear();
|
|
8701
|
+
}
|
|
8702
|
+
catch (error) {
|
|
8703
|
+
console.warn('Error clearing cold storage:', error);
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8706
|
+
// Reset stats
|
|
8707
|
+
this.stats = {
|
|
8708
|
+
hits: 0,
|
|
8709
|
+
misses: 0,
|
|
8710
|
+
evictions: 0,
|
|
8711
|
+
size: 0,
|
|
8712
|
+
maxSize: this.hotCacheMaxSize
|
|
8713
|
+
};
|
|
8714
|
+
}
|
|
8715
|
+
/**
|
|
8716
|
+
* Get cache statistics
|
|
8717
|
+
* @returns Cache statistics
|
|
7916
8718
|
*/
|
|
7917
|
-
|
|
7918
|
-
return this.
|
|
8719
|
+
getStats() {
|
|
8720
|
+
return { ...this.stats };
|
|
7919
8721
|
}
|
|
7920
8722
|
/**
|
|
7921
|
-
*
|
|
8723
|
+
* Prefetch items based on ID patterns or relationships
|
|
8724
|
+
* @param ids Array of IDs to prefetch
|
|
7922
8725
|
*/
|
|
7923
|
-
async
|
|
7924
|
-
|
|
8726
|
+
async prefetch(ids) {
|
|
8727
|
+
// Check if it's time to tune parameters
|
|
8728
|
+
await this.checkAndTuneParameters();
|
|
8729
|
+
// Prefetch in batches to avoid overwhelming the system
|
|
8730
|
+
const batches = [];
|
|
8731
|
+
// Split into batches using the configurable batch size
|
|
8732
|
+
for (let i = 0; i < ids.length; i += this.batchSize) {
|
|
8733
|
+
const batch = ids.slice(i, i + this.batchSize);
|
|
8734
|
+
batches.push(batch);
|
|
8735
|
+
}
|
|
8736
|
+
// Process each batch
|
|
8737
|
+
for (const batch of batches) {
|
|
8738
|
+
await Promise.all(batch.map(async (id) => {
|
|
8739
|
+
// Skip if already in hot cache
|
|
8740
|
+
if (this.hotCache.has(id))
|
|
8741
|
+
return;
|
|
8742
|
+
try {
|
|
8743
|
+
// Try to get from any cache level
|
|
8744
|
+
await this.get(id);
|
|
8745
|
+
}
|
|
8746
|
+
catch (error) {
|
|
8747
|
+
// Ignore errors during prefetching
|
|
8748
|
+
if (process.env.DEBUG) {
|
|
8749
|
+
console.warn(`Error prefetching ${id}:`, error);
|
|
8750
|
+
}
|
|
8751
|
+
}
|
|
8752
|
+
}));
|
|
8753
|
+
}
|
|
7925
8754
|
}
|
|
7926
8755
|
/**
|
|
7927
|
-
*
|
|
8756
|
+
* Check if it's time to tune parameters and do so if needed
|
|
8757
|
+
* This is called before operations that might benefit from tuned parameters
|
|
8758
|
+
*
|
|
8759
|
+
* This method serves as a checkpoint for auto-tuning, ensuring that:
|
|
8760
|
+
* 1. Parameters are tuned periodically based on the auto-tune interval
|
|
8761
|
+
* 2. Tuning happens before critical operations that would benefit from optimized parameters
|
|
8762
|
+
* 3. Tuning doesn't happen too frequently, which could impact performance
|
|
8763
|
+
*
|
|
8764
|
+
* By calling this method before get(), getMany(), and prefetch() operations,
|
|
8765
|
+
* we ensure that the cache parameters are optimized for the current workload
|
|
8766
|
+
* without adding unnecessary overhead to every operation.
|
|
7928
8767
|
*/
|
|
7929
|
-
async
|
|
7930
|
-
|
|
8768
|
+
async checkAndTuneParameters() {
|
|
8769
|
+
// Skip if auto-tuning is disabled
|
|
8770
|
+
if (!this.autoTune)
|
|
8771
|
+
return;
|
|
8772
|
+
// Check if it's time to tune parameters
|
|
8773
|
+
const now = Date.now();
|
|
8774
|
+
if (now - this.lastAutoTuneTime >= this.autoTuneInterval) {
|
|
8775
|
+
await this.tuneParameters();
|
|
8776
|
+
}
|
|
8777
|
+
}
|
|
8778
|
+
/**
|
|
8779
|
+
* Get multiple items at once, optimizing for batch retrieval
|
|
8780
|
+
* @param ids Array of IDs to get
|
|
8781
|
+
* @returns Map of ID to item
|
|
8782
|
+
*/
|
|
8783
|
+
async getMany(ids) {
|
|
8784
|
+
// Check if it's time to tune parameters
|
|
8785
|
+
await this.checkAndTuneParameters();
|
|
8786
|
+
const result = new Map();
|
|
8787
|
+
// First check hot cache for all IDs
|
|
8788
|
+
const missingIds = [];
|
|
8789
|
+
for (const id of ids) {
|
|
8790
|
+
const hotCacheEntry = this.hotCache.get(id);
|
|
8791
|
+
if (hotCacheEntry) {
|
|
8792
|
+
// Update access metadata
|
|
8793
|
+
hotCacheEntry.lastAccessed = Date.now();
|
|
8794
|
+
hotCacheEntry.accessCount++;
|
|
8795
|
+
// Add to result
|
|
8796
|
+
result.set(id, hotCacheEntry.data);
|
|
8797
|
+
// Update stats
|
|
8798
|
+
this.stats.hits++;
|
|
8799
|
+
}
|
|
8800
|
+
else {
|
|
8801
|
+
missingIds.push(id);
|
|
8802
|
+
}
|
|
8803
|
+
}
|
|
8804
|
+
if (missingIds.length === 0) {
|
|
8805
|
+
return result;
|
|
8806
|
+
}
|
|
8807
|
+
// Try to get missing items from warm cache
|
|
8808
|
+
if (this.warmStorage) {
|
|
8809
|
+
try {
|
|
8810
|
+
const warmCacheItems = await this.warmStorage.getMany(missingIds);
|
|
8811
|
+
for (const [id, item] of warmCacheItems.entries()) {
|
|
8812
|
+
if (item) {
|
|
8813
|
+
// Promote to hot cache
|
|
8814
|
+
this.addToHotCache(id, item);
|
|
8815
|
+
// Add to result
|
|
8816
|
+
result.set(id, item);
|
|
8817
|
+
// Update stats
|
|
8818
|
+
this.stats.hits++;
|
|
8819
|
+
// Remove from missing IDs
|
|
8820
|
+
const index = missingIds.indexOf(id);
|
|
8821
|
+
if (index !== -1) {
|
|
8822
|
+
missingIds.splice(index, 1);
|
|
8823
|
+
}
|
|
8824
|
+
}
|
|
8825
|
+
}
|
|
8826
|
+
}
|
|
8827
|
+
catch (error) {
|
|
8828
|
+
console.warn('Error accessing warm cache for batch:', error);
|
|
8829
|
+
}
|
|
8830
|
+
}
|
|
8831
|
+
if (missingIds.length === 0) {
|
|
8832
|
+
return result;
|
|
8833
|
+
}
|
|
8834
|
+
// Try to get remaining missing items from cold storage
|
|
8835
|
+
if (this.coldStorage) {
|
|
8836
|
+
try {
|
|
8837
|
+
const coldStorageItems = await this.coldStorage.getMany(missingIds);
|
|
8838
|
+
for (const [id, item] of coldStorageItems.entries()) {
|
|
8839
|
+
if (item) {
|
|
8840
|
+
// Promote to hot and warm caches
|
|
8841
|
+
this.addToHotCache(id, item);
|
|
8842
|
+
await this.addToWarmCache(id, item);
|
|
8843
|
+
// Add to result
|
|
8844
|
+
result.set(id, item);
|
|
8845
|
+
// Update stats
|
|
8846
|
+
this.stats.misses++;
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
catch (error) {
|
|
8851
|
+
console.warn('Error accessing cold storage for batch:', error);
|
|
8852
|
+
}
|
|
8853
|
+
}
|
|
8854
|
+
return result;
|
|
8855
|
+
}
|
|
8856
|
+
/**
|
|
8857
|
+
* Set the storage adapters for warm and cold caches
|
|
8858
|
+
* @param warmStorage Warm cache storage adapter
|
|
8859
|
+
* @param coldStorage Cold storage adapter
|
|
8860
|
+
*/
|
|
8861
|
+
setStorageAdapters(warmStorage, coldStorage) {
|
|
8862
|
+
this.warmStorage = warmStorage;
|
|
8863
|
+
this.coldStorage = coldStorage;
|
|
7931
8864
|
}
|
|
7932
8865
|
}
|
|
7933
8866
|
|
|
@@ -7973,6 +8906,8 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
7973
8906
|
this.activeLocks = new Set();
|
|
7974
8907
|
// Change log for efficient synchronization
|
|
7975
8908
|
this.changeLogPrefix = 'change-log/';
|
|
8909
|
+
// Node cache to avoid redundant API calls
|
|
8910
|
+
this.nodeCache = new Map();
|
|
7976
8911
|
// Batch update timer ID
|
|
7977
8912
|
this.statisticsBatchUpdateTimerId = null;
|
|
7978
8913
|
// Flag to indicate if statistics have been modified since last save
|
|
@@ -7998,6 +8933,9 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
7998
8933
|
this.verbPrefix = `${VERBS_DIR}/`;
|
|
7999
8934
|
this.metadataPrefix = `${METADATA_DIR}/`;
|
|
8000
8935
|
this.indexPrefix = `${INDEX_DIR}/`;
|
|
8936
|
+
// Initialize cache managers
|
|
8937
|
+
this.nounCacheManager = new CacheManager(options.cacheConfig);
|
|
8938
|
+
this.verbCacheManager = new CacheManager(options.cacheConfig);
|
|
8001
8939
|
}
|
|
8002
8940
|
/**
|
|
8003
8941
|
* Initialize the storage adapter
|
|
@@ -8036,6 +8974,78 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8036
8974
|
await this.s3Client.send(new HeadBucketCommand({
|
|
8037
8975
|
Bucket: this.bucketName
|
|
8038
8976
|
}));
|
|
8977
|
+
// Create storage adapter proxies for the cache managers
|
|
8978
|
+
const nounStorageAdapter = {
|
|
8979
|
+
get: async (id) => this.getNoun_internal(id),
|
|
8980
|
+
set: async (id, node) => this.saveNoun_internal(node),
|
|
8981
|
+
delete: async (id) => this.deleteNoun_internal(id),
|
|
8982
|
+
getMany: async (ids) => {
|
|
8983
|
+
const result = new Map();
|
|
8984
|
+
// Process in batches to avoid overwhelming the S3 API
|
|
8985
|
+
const batchSize = 10;
|
|
8986
|
+
const batches = [];
|
|
8987
|
+
// Split into batches
|
|
8988
|
+
for (let i = 0; i < ids.length; i += batchSize) {
|
|
8989
|
+
const batch = ids.slice(i, i + batchSize);
|
|
8990
|
+
batches.push(batch);
|
|
8991
|
+
}
|
|
8992
|
+
// Process each batch
|
|
8993
|
+
for (const batch of batches) {
|
|
8994
|
+
const batchResults = await Promise.all(batch.map(async (id) => {
|
|
8995
|
+
const node = await this.getNoun_internal(id);
|
|
8996
|
+
return { id, node };
|
|
8997
|
+
}));
|
|
8998
|
+
// Add results to map
|
|
8999
|
+
for (const { id, node } of batchResults) {
|
|
9000
|
+
if (node) {
|
|
9001
|
+
result.set(id, node);
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
}
|
|
9005
|
+
return result;
|
|
9006
|
+
},
|
|
9007
|
+
clear: async () => {
|
|
9008
|
+
// No-op for now, as we don't want to clear the entire storage
|
|
9009
|
+
// This would be implemented if needed
|
|
9010
|
+
}
|
|
9011
|
+
};
|
|
9012
|
+
const verbStorageAdapter = {
|
|
9013
|
+
get: async (id) => this.getVerb_internal(id),
|
|
9014
|
+
set: async (id, edge) => this.saveVerb_internal(edge),
|
|
9015
|
+
delete: async (id) => this.deleteVerb_internal(id),
|
|
9016
|
+
getMany: async (ids) => {
|
|
9017
|
+
const result = new Map();
|
|
9018
|
+
// Process in batches to avoid overwhelming the S3 API
|
|
9019
|
+
const batchSize = 10;
|
|
9020
|
+
const batches = [];
|
|
9021
|
+
// Split into batches
|
|
9022
|
+
for (let i = 0; i < ids.length; i += batchSize) {
|
|
9023
|
+
const batch = ids.slice(i, i + batchSize);
|
|
9024
|
+
batches.push(batch);
|
|
9025
|
+
}
|
|
9026
|
+
// Process each batch
|
|
9027
|
+
for (const batch of batches) {
|
|
9028
|
+
const batchResults = await Promise.all(batch.map(async (id) => {
|
|
9029
|
+
const edge = await this.getVerb_internal(id);
|
|
9030
|
+
return { id, edge };
|
|
9031
|
+
}));
|
|
9032
|
+
// Add results to map
|
|
9033
|
+
for (const { id, edge } of batchResults) {
|
|
9034
|
+
if (edge) {
|
|
9035
|
+
result.set(id, edge);
|
|
9036
|
+
}
|
|
9037
|
+
}
|
|
9038
|
+
}
|
|
9039
|
+
return result;
|
|
9040
|
+
},
|
|
9041
|
+
clear: async () => {
|
|
9042
|
+
// No-op for now, as we don't want to clear the entire storage
|
|
9043
|
+
// This would be implemented if needed
|
|
9044
|
+
}
|
|
9045
|
+
};
|
|
9046
|
+
// Set storage adapters for cache managers
|
|
9047
|
+
this.nounCacheManager.setStorageAdapters(nounStorageAdapter, nounStorageAdapter);
|
|
9048
|
+
this.verbCacheManager.setStorageAdapters(verbStorageAdapter, verbStorageAdapter);
|
|
8039
9049
|
this.isInitialized = true;
|
|
8040
9050
|
}
|
|
8041
9051
|
catch (error) {
|
|
@@ -8144,7 +9154,10 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8144
9154
|
const parsedNode = JSON.parse(bodyContents);
|
|
8145
9155
|
console.log(`Parsed node data for ${id}:`, parsedNode);
|
|
8146
9156
|
// Ensure the parsed node has the expected properties
|
|
8147
|
-
if (!parsedNode ||
|
|
9157
|
+
if (!parsedNode ||
|
|
9158
|
+
!parsedNode.id ||
|
|
9159
|
+
!parsedNode.vector ||
|
|
9160
|
+
!parsedNode.connections) {
|
|
8148
9161
|
console.error(`Invalid node data for ${id}:`, parsedNode);
|
|
8149
9162
|
return null;
|
|
8150
9163
|
}
|
|
@@ -8180,100 +9193,118 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8180
9193
|
}
|
|
8181
9194
|
/**
|
|
8182
9195
|
* Get all nodes from storage
|
|
9196
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
9197
|
+
* It can cause memory issues with large datasets. Use getNodesWithPagination() instead.
|
|
8183
9198
|
*/
|
|
8184
9199
|
async getAllNodes() {
|
|
8185
9200
|
await this.ensureInitialized();
|
|
9201
|
+
console.warn('WARNING: getAllNodes() is deprecated and will be removed in a future version. Use getNodesWithPagination() instead.');
|
|
9202
|
+
try {
|
|
9203
|
+
// Use the paginated method with a large limit to maintain backward compatibility
|
|
9204
|
+
// but warn about potential issues
|
|
9205
|
+
const result = await this.getNodesWithPagination({
|
|
9206
|
+
limit: 1000, // Reasonable limit to avoid memory issues
|
|
9207
|
+
useCache: true
|
|
9208
|
+
});
|
|
9209
|
+
if (result.hasMore) {
|
|
9210
|
+
console.warn(`WARNING: Only returning the first 1000 nodes. There are more nodes available. Use getNodesWithPagination() for proper pagination.`);
|
|
9211
|
+
}
|
|
9212
|
+
return result.nodes;
|
|
9213
|
+
}
|
|
9214
|
+
catch (error) {
|
|
9215
|
+
console.error('Failed to get all nodes:', error);
|
|
9216
|
+
return [];
|
|
9217
|
+
}
|
|
9218
|
+
}
|
|
9219
|
+
/**
|
|
9220
|
+
* Get nodes with pagination
|
|
9221
|
+
* @param options Pagination options
|
|
9222
|
+
* @returns Promise that resolves to a paginated result of nodes
|
|
9223
|
+
*/
|
|
9224
|
+
async getNodesWithPagination(options = {}) {
|
|
9225
|
+
await this.ensureInitialized();
|
|
9226
|
+
const limit = options.limit || 100;
|
|
9227
|
+
const useCache = options.useCache !== false;
|
|
8186
9228
|
try {
|
|
8187
9229
|
// Import the ListObjectsV2Command and GetObjectCommand only when needed
|
|
8188
|
-
const { ListObjectsV2Command
|
|
8189
|
-
|
|
8190
|
-
// List all objects in the nouns directory
|
|
9230
|
+
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
|
|
9231
|
+
// List objects with pagination
|
|
8191
9232
|
const listResponse = await this.s3Client.send(new ListObjectsV2Command({
|
|
8192
9233
|
Bucket: this.bucketName,
|
|
8193
|
-
Prefix: this.nounPrefix
|
|
9234
|
+
Prefix: this.nounPrefix,
|
|
9235
|
+
MaxKeys: limit,
|
|
9236
|
+
ContinuationToken: options.cursor
|
|
8194
9237
|
}));
|
|
9238
|
+
// If listResponse is null/undefined or there are no objects, return an empty result
|
|
9239
|
+
if (!listResponse ||
|
|
9240
|
+
!listResponse.Contents ||
|
|
9241
|
+
listResponse.Contents.length === 0) {
|
|
9242
|
+
return {
|
|
9243
|
+
nodes: [],
|
|
9244
|
+
hasMore: false
|
|
9245
|
+
};
|
|
9246
|
+
}
|
|
9247
|
+
// Extract node IDs from the keys
|
|
9248
|
+
const nodeIds = listResponse.Contents
|
|
9249
|
+
.filter((object) => object && object.Key)
|
|
9250
|
+
.map((object) => object.Key.replace(this.nounPrefix, '').replace('.json', ''));
|
|
9251
|
+
// Use the cache manager to get nodes efficiently
|
|
8195
9252
|
const nodes = [];
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
if (object && object.Key) {
|
|
8206
|
-
console.log(`- ${object.Key}`);
|
|
9253
|
+
if (useCache) {
|
|
9254
|
+
// Get nodes from cache manager
|
|
9255
|
+
const cachedNodes = await this.nounCacheManager.getMany(nodeIds);
|
|
9256
|
+
// Add nodes to result in the same order as nodeIds
|
|
9257
|
+
for (const id of nodeIds) {
|
|
9258
|
+
const node = cachedNodes.get(id);
|
|
9259
|
+
if (node) {
|
|
9260
|
+
nodes.push(node);
|
|
9261
|
+
}
|
|
8207
9262
|
}
|
|
8208
9263
|
}
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
9264
|
+
else {
|
|
9265
|
+
// Get nodes directly from S3 without using cache
|
|
9266
|
+
// Process in smaller batches to reduce memory usage
|
|
9267
|
+
const batchSize = 50;
|
|
9268
|
+
const batches = [];
|
|
9269
|
+
// Split into batches
|
|
9270
|
+
for (let i = 0; i < nodeIds.length; i += batchSize) {
|
|
9271
|
+
const batch = nodeIds.slice(i, i + batchSize);
|
|
9272
|
+
batches.push(batch);
|
|
8214
9273
|
}
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
const
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
Key: object.Key
|
|
8223
|
-
}));
|
|
8224
|
-
// Check if response is null or undefined
|
|
8225
|
-
if (!response || !response.Body) {
|
|
8226
|
-
console.log(`No response or response body for node ${nodeId}`);
|
|
8227
|
-
return null;
|
|
8228
|
-
}
|
|
8229
|
-
// Convert the response body to a string
|
|
8230
|
-
const bodyContents = await response.Body.transformToString();
|
|
8231
|
-
console.log(`Retrieved node body for ${nodeId}: ${bodyContents.substring(0, 100)}${bodyContents.length > 100 ? '...' : ''}`);
|
|
8232
|
-
// Parse the JSON string
|
|
8233
|
-
try {
|
|
8234
|
-
const parsedNode = JSON.parse(bodyContents);
|
|
8235
|
-
console.log(`Parsed node data for ${nodeId}:`, parsedNode);
|
|
8236
|
-
// Ensure the parsed node has the expected properties
|
|
8237
|
-
if (!parsedNode || !parsedNode.id || !parsedNode.vector || !parsedNode.connections) {
|
|
8238
|
-
console.error(`Invalid node data for ${nodeId}:`, parsedNode);
|
|
9274
|
+
// Process each batch sequentially
|
|
9275
|
+
for (const batch of batches) {
|
|
9276
|
+
const batchNodes = await Promise.all(batch.map(async (id) => {
|
|
9277
|
+
try {
|
|
9278
|
+
return await this.getNoun_internal(id);
|
|
9279
|
+
}
|
|
9280
|
+
catch (error) {
|
|
8239
9281
|
return null;
|
|
8240
9282
|
}
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
9283
|
+
}));
|
|
9284
|
+
// Add non-null nodes to result
|
|
9285
|
+
for (const node of batchNodes) {
|
|
9286
|
+
if (node) {
|
|
9287
|
+
nodes.push(node);
|
|
8245
9288
|
}
|
|
8246
|
-
const node = {
|
|
8247
|
-
id: parsedNode.id,
|
|
8248
|
-
vector: parsedNode.vector,
|
|
8249
|
-
connections
|
|
8250
|
-
};
|
|
8251
|
-
console.log(`Successfully retrieved node ${nodeId}:`, node);
|
|
8252
|
-
return node;
|
|
8253
|
-
}
|
|
8254
|
-
catch (parseError) {
|
|
8255
|
-
console.error(`Failed to parse node data for ${nodeId}:`, parseError);
|
|
8256
|
-
return null;
|
|
8257
9289
|
}
|
|
8258
9290
|
}
|
|
8259
|
-
catch (error) {
|
|
8260
|
-
console.error(`Error getting node from ${object.Key}:`, error);
|
|
8261
|
-
return null;
|
|
8262
|
-
}
|
|
8263
|
-
});
|
|
8264
|
-
// Wait for all promises to resolve and filter out nulls
|
|
8265
|
-
const resolvedNodes = await Promise.all(nodePromises);
|
|
8266
|
-
const filteredNodes = resolvedNodes.filter((node) => node !== null);
|
|
8267
|
-
console.log(`Returning ${filteredNodes.length} nodes`);
|
|
8268
|
-
// Debug: Log all nodes being returned
|
|
8269
|
-
for (const node of filteredNodes) {
|
|
8270
|
-
console.log(`- Node ${node.id}`);
|
|
8271
9291
|
}
|
|
8272
|
-
|
|
9292
|
+
// Determine if there are more nodes
|
|
9293
|
+
const hasMore = !!listResponse.IsTruncated;
|
|
9294
|
+
// Set next cursor if there are more nodes
|
|
9295
|
+
const nextCursor = listResponse.NextContinuationToken;
|
|
9296
|
+
return {
|
|
9297
|
+
nodes,
|
|
9298
|
+
hasMore,
|
|
9299
|
+
nextCursor
|
|
9300
|
+
};
|
|
8273
9301
|
}
|
|
8274
9302
|
catch (error) {
|
|
8275
|
-
console.error('Failed to get
|
|
8276
|
-
return
|
|
9303
|
+
console.error('Failed to get nodes with pagination:', error);
|
|
9304
|
+
return {
|
|
9305
|
+
nodes: [],
|
|
9306
|
+
hasMore: false
|
|
9307
|
+
};
|
|
8277
9308
|
}
|
|
8278
9309
|
}
|
|
8279
9310
|
/**
|
|
@@ -8292,14 +9323,31 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8292
9323
|
async getNodesByNounType(nounType) {
|
|
8293
9324
|
await this.ensureInitialized();
|
|
8294
9325
|
try {
|
|
8295
|
-
// Get all nodes
|
|
8296
|
-
const allNodes = await this.getAllNodes();
|
|
8297
|
-
// Filter nodes by noun type using metadata
|
|
8298
9326
|
const filteredNodes = [];
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
9327
|
+
let hasMore = true;
|
|
9328
|
+
let cursor = undefined;
|
|
9329
|
+
// Use pagination to process nodes in batches
|
|
9330
|
+
while (hasMore) {
|
|
9331
|
+
// Get a batch of nodes
|
|
9332
|
+
const result = await this.getNodesWithPagination({
|
|
9333
|
+
limit: 100,
|
|
9334
|
+
cursor,
|
|
9335
|
+
useCache: true
|
|
9336
|
+
});
|
|
9337
|
+
// Filter nodes by noun type using metadata
|
|
9338
|
+
for (const node of result.nodes) {
|
|
9339
|
+
const metadata = await this.getMetadata(node.id);
|
|
9340
|
+
if (metadata && metadata.noun === nounType) {
|
|
9341
|
+
filteredNodes.push(node);
|
|
9342
|
+
}
|
|
9343
|
+
}
|
|
9344
|
+
// Update pagination state
|
|
9345
|
+
hasMore = result.hasMore;
|
|
9346
|
+
cursor = result.nextCursor;
|
|
9347
|
+
// Safety check to prevent infinite loops
|
|
9348
|
+
if (!cursor && hasMore) {
|
|
9349
|
+
console.warn('No cursor returned but hasMore is true, breaking loop');
|
|
9350
|
+
break;
|
|
8303
9351
|
}
|
|
8304
9352
|
}
|
|
8305
9353
|
return filteredNodes;
|
|
@@ -8422,7 +9470,10 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8422
9470
|
const parsedEdge = JSON.parse(bodyContents);
|
|
8423
9471
|
console.log(`Parsed edge data for ${id}:`, parsedEdge);
|
|
8424
9472
|
// Ensure the parsed edge has the expected properties
|
|
8425
|
-
if (!parsedEdge ||
|
|
9473
|
+
if (!parsedEdge ||
|
|
9474
|
+
!parsedEdge.id ||
|
|
9475
|
+
!parsedEdge.vector ||
|
|
9476
|
+
!parsedEdge.connections ||
|
|
8426
9477
|
!(parsedEdge.sourceId || parsedEdge.source) ||
|
|
8427
9478
|
!(parsedEdge.targetId || parsedEdge.target) ||
|
|
8428
9479
|
!(parsedEdge.type || parsedEdge.verb)) {
|
|
@@ -8476,86 +9527,205 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8476
9527
|
}
|
|
8477
9528
|
/**
|
|
8478
9529
|
* Get all verbs from storage (internal implementation)
|
|
9530
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
9531
|
+
* It can cause memory issues with large datasets. Use getVerbsWithPagination() instead.
|
|
8479
9532
|
*/
|
|
8480
9533
|
async getAllVerbs_internal() {
|
|
9534
|
+
console.warn('WARNING: getAllVerbs_internal() is deprecated and will be removed in a future version. Use getVerbsWithPagination() instead.');
|
|
8481
9535
|
return this.getAllEdges();
|
|
8482
9536
|
}
|
|
8483
9537
|
/**
|
|
8484
9538
|
* Get all edges from storage
|
|
9539
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
9540
|
+
* It can cause memory issues with large datasets. Use getEdgesWithPagination() instead.
|
|
8485
9541
|
*/
|
|
8486
9542
|
async getAllEdges() {
|
|
8487
9543
|
await this.ensureInitialized();
|
|
9544
|
+
console.warn('WARNING: getAllEdges() is deprecated and will be removed in a future version. Use getEdgesWithPagination() instead.');
|
|
8488
9545
|
try {
|
|
8489
|
-
//
|
|
8490
|
-
|
|
8491
|
-
|
|
9546
|
+
// Use the paginated method with a large limit to maintain backward compatibility
|
|
9547
|
+
// but warn about potential issues
|
|
9548
|
+
const result = await this.getEdgesWithPagination({
|
|
9549
|
+
limit: 1000, // Reasonable limit to avoid memory issues
|
|
9550
|
+
useCache: true
|
|
9551
|
+
});
|
|
9552
|
+
if (result.hasMore) {
|
|
9553
|
+
console.warn(`WARNING: Only returning the first 1000 edges. There are more edges available. Use getEdgesWithPagination() for proper pagination.`);
|
|
9554
|
+
}
|
|
9555
|
+
return result.edges;
|
|
9556
|
+
}
|
|
9557
|
+
catch (error) {
|
|
9558
|
+
console.error('Failed to get all edges:', error);
|
|
9559
|
+
return [];
|
|
9560
|
+
}
|
|
9561
|
+
}
|
|
9562
|
+
/**
|
|
9563
|
+
* Get edges with pagination
|
|
9564
|
+
* @param options Pagination options
|
|
9565
|
+
* @returns Promise that resolves to a paginated result of edges
|
|
9566
|
+
*/
|
|
9567
|
+
async getEdgesWithPagination(options = {}) {
|
|
9568
|
+
await this.ensureInitialized();
|
|
9569
|
+
const limit = options.limit || 100;
|
|
9570
|
+
const useCache = options.useCache !== false;
|
|
9571
|
+
const filter = options.filter || {};
|
|
9572
|
+
try {
|
|
9573
|
+
// Import the ListObjectsV2Command only when needed
|
|
9574
|
+
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
|
|
9575
|
+
// List objects with pagination
|
|
8492
9576
|
const listResponse = await this.s3Client.send(new ListObjectsV2Command({
|
|
8493
9577
|
Bucket: this.bucketName,
|
|
8494
|
-
Prefix: this.verbPrefix
|
|
9578
|
+
Prefix: this.verbPrefix,
|
|
9579
|
+
MaxKeys: limit,
|
|
9580
|
+
ContinuationToken: options.cursor
|
|
8495
9581
|
}));
|
|
9582
|
+
// If listResponse is null/undefined or there are no objects, return an empty result
|
|
9583
|
+
if (!listResponse ||
|
|
9584
|
+
!listResponse.Contents ||
|
|
9585
|
+
listResponse.Contents.length === 0) {
|
|
9586
|
+
return {
|
|
9587
|
+
edges: [],
|
|
9588
|
+
hasMore: false
|
|
9589
|
+
};
|
|
9590
|
+
}
|
|
9591
|
+
// Extract edge IDs from the keys
|
|
9592
|
+
const edgeIds = listResponse.Contents
|
|
9593
|
+
.filter((object) => object && object.Key)
|
|
9594
|
+
.map((object) => object.Key.replace(this.verbPrefix, '').replace('.json', ''));
|
|
9595
|
+
// Use the cache manager to get edges efficiently
|
|
8496
9596
|
const edges = [];
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
9597
|
+
if (useCache) {
|
|
9598
|
+
// Get edges from cache manager
|
|
9599
|
+
const cachedEdges = await this.verbCacheManager.getMany(edgeIds);
|
|
9600
|
+
// Add edges to result in the same order as edgeIds
|
|
9601
|
+
for (const id of edgeIds) {
|
|
9602
|
+
const edge = cachedEdges.get(id);
|
|
9603
|
+
if (edge) {
|
|
9604
|
+
// Apply filtering if needed
|
|
9605
|
+
if (this.filterEdge(edge, filter)) {
|
|
9606
|
+
edges.push(edge);
|
|
9607
|
+
}
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
8500
9610
|
}
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
9611
|
+
else {
|
|
9612
|
+
// Get edges directly from S3 without using cache
|
|
9613
|
+
// Process in smaller batches to reduce memory usage
|
|
9614
|
+
const batchSize = 50;
|
|
9615
|
+
const batches = [];
|
|
9616
|
+
// Split into batches
|
|
9617
|
+
for (let i = 0; i < edgeIds.length; i += batchSize) {
|
|
9618
|
+
const batch = edgeIds.slice(i, i + batchSize);
|
|
9619
|
+
batches.push(batch);
|
|
9620
|
+
}
|
|
9621
|
+
// Process each batch sequentially
|
|
9622
|
+
for (const batch of batches) {
|
|
9623
|
+
const batchEdges = await Promise.all(batch.map(async (id) => {
|
|
9624
|
+
try {
|
|
9625
|
+
const edge = await this.getVerb_internal(id);
|
|
9626
|
+
// Apply filtering if needed
|
|
9627
|
+
if (edge && this.filterEdge(edge, filter)) {
|
|
9628
|
+
return edge;
|
|
9629
|
+
}
|
|
9630
|
+
return null;
|
|
9631
|
+
}
|
|
9632
|
+
catch (error) {
|
|
9633
|
+
return null;
|
|
9634
|
+
}
|
|
8510
9635
|
}));
|
|
8511
|
-
//
|
|
8512
|
-
const
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) {
|
|
8517
|
-
connections.set(Number(level), new Set(nodeIds));
|
|
9636
|
+
// Add non-null edges to result
|
|
9637
|
+
for (const edge of batchEdges) {
|
|
9638
|
+
if (edge) {
|
|
9639
|
+
edges.push(edge);
|
|
9640
|
+
}
|
|
8518
9641
|
}
|
|
8519
|
-
// Create default timestamp if not present
|
|
8520
|
-
const defaultTimestamp = {
|
|
8521
|
-
seconds: Math.floor(Date.now() / 1000),
|
|
8522
|
-
nanoseconds: (Date.now() % 1000) * 1000000
|
|
8523
|
-
};
|
|
8524
|
-
// Create default createdBy if not present
|
|
8525
|
-
const defaultCreatedBy = {
|
|
8526
|
-
augmentation: 'unknown',
|
|
8527
|
-
version: '1.0'
|
|
8528
|
-
};
|
|
8529
|
-
return {
|
|
8530
|
-
id: parsedEdge.id,
|
|
8531
|
-
vector: parsedEdge.vector,
|
|
8532
|
-
connections,
|
|
8533
|
-
sourceId: parsedEdge.sourceId || parsedEdge.source,
|
|
8534
|
-
targetId: parsedEdge.targetId || parsedEdge.target,
|
|
8535
|
-
source: parsedEdge.sourceId || parsedEdge.source,
|
|
8536
|
-
target: parsedEdge.targetId || parsedEdge.target,
|
|
8537
|
-
verb: parsedEdge.type || parsedEdge.verb,
|
|
8538
|
-
type: parsedEdge.type || parsedEdge.verb,
|
|
8539
|
-
weight: parsedEdge.weight || 1.0,
|
|
8540
|
-
metadata: parsedEdge.metadata || {},
|
|
8541
|
-
createdAt: parsedEdge.createdAt || defaultTimestamp,
|
|
8542
|
-
updatedAt: parsedEdge.updatedAt || defaultTimestamp,
|
|
8543
|
-
createdBy: parsedEdge.createdBy || defaultCreatedBy
|
|
8544
|
-
};
|
|
8545
|
-
}
|
|
8546
|
-
catch (error) {
|
|
8547
|
-
console.error(`Error getting edge from ${object.Key}:`, error);
|
|
8548
|
-
return null;
|
|
8549
9642
|
}
|
|
8550
|
-
}
|
|
8551
|
-
//
|
|
8552
|
-
const
|
|
8553
|
-
|
|
9643
|
+
}
|
|
9644
|
+
// Determine if there are more edges
|
|
9645
|
+
const hasMore = !!listResponse.IsTruncated;
|
|
9646
|
+
// Set next cursor if there are more edges
|
|
9647
|
+
const nextCursor = listResponse.NextContinuationToken;
|
|
9648
|
+
return {
|
|
9649
|
+
edges,
|
|
9650
|
+
hasMore,
|
|
9651
|
+
nextCursor
|
|
9652
|
+
};
|
|
8554
9653
|
}
|
|
8555
9654
|
catch (error) {
|
|
8556
|
-
console.error('Failed to get
|
|
8557
|
-
return
|
|
9655
|
+
console.error('Failed to get edges with pagination:', error);
|
|
9656
|
+
return {
|
|
9657
|
+
edges: [],
|
|
9658
|
+
hasMore: false
|
|
9659
|
+
};
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
/**
|
|
9663
|
+
* Filter an edge based on filter criteria
|
|
9664
|
+
* @param edge The edge to filter
|
|
9665
|
+
* @param filter The filter criteria
|
|
9666
|
+
* @returns True if the edge matches the filter, false otherwise
|
|
9667
|
+
*/
|
|
9668
|
+
filterEdge(edge, filter) {
|
|
9669
|
+
// If no filter, include all edges
|
|
9670
|
+
if (!filter.sourceId && !filter.targetId && !filter.type) {
|
|
9671
|
+
return true;
|
|
9672
|
+
}
|
|
9673
|
+
// Filter by source ID
|
|
9674
|
+
if (filter.sourceId && edge.sourceId !== filter.sourceId) {
|
|
9675
|
+
return false;
|
|
9676
|
+
}
|
|
9677
|
+
// Filter by target ID
|
|
9678
|
+
if (filter.targetId && edge.targetId !== filter.targetId) {
|
|
9679
|
+
return false;
|
|
9680
|
+
}
|
|
9681
|
+
// Filter by type
|
|
9682
|
+
if (filter.type && edge.type !== filter.type) {
|
|
9683
|
+
return false;
|
|
9684
|
+
}
|
|
9685
|
+
return true;
|
|
9686
|
+
}
|
|
9687
|
+
/**
|
|
9688
|
+
* Get verbs with pagination
|
|
9689
|
+
* @param options Pagination options
|
|
9690
|
+
* @returns Promise that resolves to a paginated result of verbs
|
|
9691
|
+
*/
|
|
9692
|
+
async getVerbsWithPagination(options = {}) {
|
|
9693
|
+
await this.ensureInitialized();
|
|
9694
|
+
// Convert filter to edge filter format
|
|
9695
|
+
const edgeFilter = {};
|
|
9696
|
+
if (options.filter) {
|
|
9697
|
+
// Handle sourceId filter
|
|
9698
|
+
if (options.filter.sourceId) {
|
|
9699
|
+
edgeFilter.sourceId = Array.isArray(options.filter.sourceId)
|
|
9700
|
+
? options.filter.sourceId[0]
|
|
9701
|
+
: options.filter.sourceId;
|
|
9702
|
+
}
|
|
9703
|
+
// Handle targetId filter
|
|
9704
|
+
if (options.filter.targetId) {
|
|
9705
|
+
edgeFilter.targetId = Array.isArray(options.filter.targetId)
|
|
9706
|
+
? options.filter.targetId[0]
|
|
9707
|
+
: options.filter.targetId;
|
|
9708
|
+
}
|
|
9709
|
+
// Handle verbType filter
|
|
9710
|
+
if (options.filter.verbType) {
|
|
9711
|
+
edgeFilter.type = Array.isArray(options.filter.verbType)
|
|
9712
|
+
? options.filter.verbType[0]
|
|
9713
|
+
: options.filter.verbType;
|
|
9714
|
+
}
|
|
8558
9715
|
}
|
|
9716
|
+
// Get edges with pagination
|
|
9717
|
+
const result = await this.getEdgesWithPagination({
|
|
9718
|
+
limit: options.limit,
|
|
9719
|
+
cursor: options.cursor,
|
|
9720
|
+
useCache: true,
|
|
9721
|
+
filter: edgeFilter
|
|
9722
|
+
});
|
|
9723
|
+
// Convert edges to verbs (they're the same in this implementation)
|
|
9724
|
+
return {
|
|
9725
|
+
items: result.edges,
|
|
9726
|
+
hasMore: result.hasMore,
|
|
9727
|
+
nextCursor: result.nextCursor
|
|
9728
|
+
};
|
|
8559
9729
|
}
|
|
8560
9730
|
/**
|
|
8561
9731
|
* Get verbs by source (internal implementation)
|
|
@@ -8722,9 +9892,10 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8722
9892
|
// In AWS SDK, this would be error.name === 'NoSuchKey'
|
|
8723
9893
|
// In our mock, we might get different error types
|
|
8724
9894
|
if (error.name === 'NoSuchKey' ||
|
|
8725
|
-
(error.message &&
|
|
8726
|
-
error.message.includes('
|
|
8727
|
-
|
|
9895
|
+
(error.message &&
|
|
9896
|
+
(error.message.includes('NoSuchKey') ||
|
|
9897
|
+
error.message.includes('not found') ||
|
|
9898
|
+
error.message.includes('does not exist')))) {
|
|
8728
9899
|
console.log(`Metadata not found for ${id}`);
|
|
8729
9900
|
return null;
|
|
8730
9901
|
}
|
|
@@ -8749,7 +9920,9 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8749
9920
|
Prefix: prefix
|
|
8750
9921
|
}));
|
|
8751
9922
|
// If there are no objects or Contents is undefined, return
|
|
8752
|
-
if (!listResponse ||
|
|
9923
|
+
if (!listResponse ||
|
|
9924
|
+
!listResponse.Contents ||
|
|
9925
|
+
listResponse.Contents.length === 0) {
|
|
8753
9926
|
return;
|
|
8754
9927
|
}
|
|
8755
9928
|
// Delete each object
|
|
@@ -8799,15 +9972,20 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8799
9972
|
Prefix: prefix
|
|
8800
9973
|
}));
|
|
8801
9974
|
// If there are no objects or Contents is undefined, return
|
|
8802
|
-
if (!listResponse ||
|
|
9975
|
+
if (!listResponse ||
|
|
9976
|
+
!listResponse.Contents ||
|
|
9977
|
+
listResponse.Contents.length === 0) {
|
|
8803
9978
|
return { size, count };
|
|
8804
9979
|
}
|
|
8805
9980
|
// Calculate size and count
|
|
8806
9981
|
for (const object of listResponse.Contents) {
|
|
8807
9982
|
if (object) {
|
|
8808
9983
|
// Ensure Size is a number
|
|
8809
|
-
const objectSize = typeof object.Size === 'number'
|
|
8810
|
-
|
|
9984
|
+
const objectSize = typeof object.Size === 'number'
|
|
9985
|
+
? object.Size
|
|
9986
|
+
: object.Size
|
|
9987
|
+
? parseInt(object.Size.toString(), 10)
|
|
9988
|
+
: 0;
|
|
8811
9989
|
// Add to total size and increment count
|
|
8812
9990
|
size += objectSize || 0;
|
|
8813
9991
|
count++;
|
|
@@ -8826,12 +10004,17 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8826
10004
|
const verbsResult = await calculateSizeAndCount(this.verbPrefix);
|
|
8827
10005
|
const metadataResult = await calculateSizeAndCount(this.metadataPrefix);
|
|
8828
10006
|
const indexResult = await calculateSizeAndCount(this.indexPrefix);
|
|
8829
|
-
totalSize =
|
|
10007
|
+
totalSize =
|
|
10008
|
+
nounsResult.size +
|
|
10009
|
+
verbsResult.size +
|
|
10010
|
+
metadataResult.size +
|
|
10011
|
+
indexResult.size;
|
|
8830
10012
|
nodeCount = nounsResult.count;
|
|
8831
10013
|
edgeCount = verbsResult.count;
|
|
8832
10014
|
metadataCount = metadataResult.count;
|
|
8833
10015
|
// Ensure we have a minimum size if we have objects
|
|
8834
|
-
if (totalSize === 0 &&
|
|
10016
|
+
if (totalSize === 0 &&
|
|
10017
|
+
(nodeCount > 0 || edgeCount > 0 || metadataCount > 0)) {
|
|
8835
10018
|
console.log(`Setting minimum size for ${nodeCount} nodes, ${edgeCount} edges, and ${metadataCount} metadata objects`);
|
|
8836
10019
|
totalSize = (nodeCount + edgeCount + metadataCount) * 100; // Arbitrary size per object
|
|
8837
10020
|
}
|
|
@@ -8865,7 +10048,8 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
8865
10048
|
const metadata = JSON.parse(bodyContents);
|
|
8866
10049
|
// Count by noun type
|
|
8867
10050
|
if (metadata && metadata.noun) {
|
|
8868
|
-
nounTypeCounts[metadata.noun] =
|
|
10051
|
+
nounTypeCounts[metadata.noun] =
|
|
10052
|
+
(nounTypeCounts[metadata.noun] || 0) + 1;
|
|
8869
10053
|
}
|
|
8870
10054
|
}
|
|
8871
10055
|
catch (parseError) {
|
|
@@ -9046,17 +10230,23 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
9046
10230
|
*/
|
|
9047
10231
|
mergeStatistics(storageStats, localStats) {
|
|
9048
10232
|
// Merge noun counts by taking the maximum of each type
|
|
9049
|
-
const mergedNounCount = {
|
|
10233
|
+
const mergedNounCount = {
|
|
10234
|
+
...storageStats.nounCount
|
|
10235
|
+
};
|
|
9050
10236
|
for (const [type, count] of Object.entries(localStats.nounCount)) {
|
|
9051
10237
|
mergedNounCount[type] = Math.max(mergedNounCount[type] || 0, count);
|
|
9052
10238
|
}
|
|
9053
10239
|
// Merge verb counts by taking the maximum of each type
|
|
9054
|
-
const mergedVerbCount = {
|
|
10240
|
+
const mergedVerbCount = {
|
|
10241
|
+
...storageStats.verbCount
|
|
10242
|
+
};
|
|
9055
10243
|
for (const [type, count] of Object.entries(localStats.verbCount)) {
|
|
9056
10244
|
mergedVerbCount[type] = Math.max(mergedVerbCount[type] || 0, count);
|
|
9057
10245
|
}
|
|
9058
10246
|
// Merge metadata counts by taking the maximum of each type
|
|
9059
|
-
const mergedMetadataCount = {
|
|
10247
|
+
const mergedMetadataCount = {
|
|
10248
|
+
...storageStats.metadataCount
|
|
10249
|
+
};
|
|
9060
10250
|
for (const [type, count] of Object.entries(localStats.metadataCount)) {
|
|
9061
10251
|
mergedMetadataCount[type] = Math.max(mergedMetadataCount[type] || 0, count);
|
|
9062
10252
|
}
|
|
@@ -9169,9 +10359,10 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
9169
10359
|
catch (error) {
|
|
9170
10360
|
// Check if this is a "NoSuchKey" error (object doesn't exist)
|
|
9171
10361
|
if (error.name === 'NoSuchKey' ||
|
|
9172
|
-
(error.message &&
|
|
9173
|
-
error.message.includes('
|
|
9174
|
-
|
|
10362
|
+
(error.message &&
|
|
10363
|
+
(error.message.includes('NoSuchKey') ||
|
|
10364
|
+
error.message.includes('not found') ||
|
|
10365
|
+
error.message.includes('does not exist')))) {
|
|
9175
10366
|
return null;
|
|
9176
10367
|
}
|
|
9177
10368
|
// For other errors, propagate them
|
|
@@ -9200,8 +10391,8 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
9200
10391
|
Body: JSON.stringify(entryWithInstance),
|
|
9201
10392
|
ContentType: 'application/json',
|
|
9202
10393
|
Metadata: {
|
|
9203
|
-
|
|
9204
|
-
|
|
10394
|
+
timestamp: entry.timestamp.toString(),
|
|
10395
|
+
operation: entry.operation,
|
|
9205
10396
|
'entity-type': entry.entityType,
|
|
9206
10397
|
'entity-id': entry.entityId
|
|
9207
10398
|
}
|
|
@@ -9371,7 +10562,7 @@ class S3CompatibleStorage extends BaseStorage {
|
|
|
9371
10562
|
this.activeLocks.add(lockKey);
|
|
9372
10563
|
// Schedule automatic cleanup when lock expires
|
|
9373
10564
|
setTimeout(() => {
|
|
9374
|
-
this.releaseLock(lockKey, lockValue).catch(error => {
|
|
10565
|
+
this.releaseLock(lockKey, lockValue).catch((error) => {
|
|
9375
10566
|
console.warn(`Failed to auto-release expired lock ${lockKey}:`, error);
|
|
9376
10567
|
});
|
|
9377
10568
|
}, ttl);
|
|
@@ -12893,7 +14084,12 @@ class BrainyData {
|
|
|
12893
14084
|
// Set distance function
|
|
12894
14085
|
this.distanceFunction = config.distanceFunction || cosineDistance$1;
|
|
12895
14086
|
// Always use the optimized HNSW index implementation
|
|
12896
|
-
|
|
14087
|
+
// Configure HNSW with disk-based storage when a storage adapter is provided
|
|
14088
|
+
const hnswConfig = config.hnsw || {};
|
|
14089
|
+
if (config.storageAdapter) {
|
|
14090
|
+
hnswConfig.useDiskBasedIndex = true;
|
|
14091
|
+
}
|
|
14092
|
+
this.index = new HNSWIndexOptimized(hnswConfig, this.distanceFunction, config.storageAdapter || null);
|
|
12897
14093
|
this.useOptimizedIndex = true;
|
|
12898
14094
|
// Set storage if provided, otherwise it will be initialized in init()
|
|
12899
14095
|
this.storage = config.storageAdapter || null;
|
|
@@ -12918,6 +14114,8 @@ class BrainyData {
|
|
|
12918
14114
|
config.storage?.requestPersistentStorage || false;
|
|
12919
14115
|
// Set read-only flag
|
|
12920
14116
|
this.readOnly = config.readOnly || false;
|
|
14117
|
+
// Set lazy loading in read-only mode flag
|
|
14118
|
+
this.lazyLoadInReadOnlyMode = config.lazyLoadInReadOnlyMode || false;
|
|
12921
14119
|
// Set write-only flag
|
|
12922
14120
|
this.writeOnly = config.writeOnly || false;
|
|
12923
14121
|
// Validate that readOnly and writeOnly are not both true
|
|
@@ -13314,6 +14512,14 @@ class BrainyData {
|
|
|
13314
14512
|
console.log('Database is in write-only mode, skipping index loading');
|
|
13315
14513
|
}
|
|
13316
14514
|
}
|
|
14515
|
+
else if (this.readOnly && this.lazyLoadInReadOnlyMode) {
|
|
14516
|
+
// In read-only mode with lazy loading enabled, skip loading all nouns initially
|
|
14517
|
+
if (this.loggingConfig?.verbose) {
|
|
14518
|
+
console.log('Database is in read-only mode with lazy loading enabled, skipping initial full load');
|
|
14519
|
+
}
|
|
14520
|
+
// Just initialize an empty index
|
|
14521
|
+
this.index.clear();
|
|
14522
|
+
}
|
|
13317
14523
|
else {
|
|
13318
14524
|
// Load all nouns from storage
|
|
13319
14525
|
const nouns = await this.storage.getAllNouns();
|
|
@@ -13734,6 +14940,35 @@ class BrainyData {
|
|
|
13734
14940
|
}
|
|
13735
14941
|
// If no noun types specified, search all nouns
|
|
13736
14942
|
if (!nounTypes || nounTypes.length === 0) {
|
|
14943
|
+
// Check if we're in readonly mode with lazy loading and the index is empty
|
|
14944
|
+
const indexSize = this.index.getNouns().size;
|
|
14945
|
+
if (this.readOnly && this.lazyLoadInReadOnlyMode && indexSize === 0) {
|
|
14946
|
+
if (this.loggingConfig?.verbose) {
|
|
14947
|
+
console.log('Lazy loading mode: Index is empty, loading nodes for search...');
|
|
14948
|
+
}
|
|
14949
|
+
// In lazy loading mode, we need to load some nodes to search
|
|
14950
|
+
// Instead of loading all nodes, we'll load a subset of nodes
|
|
14951
|
+
// Since we don't have a specialized method to get top nodes for a query,
|
|
14952
|
+
// we'll load a limited number of nodes from storage
|
|
14953
|
+
const nouns = await this.storage.getAllNouns();
|
|
14954
|
+
const limitedNouns = nouns.slice(0, Math.min(nouns.length, k * 10)); // Get 10x more nodes than needed
|
|
14955
|
+
// Add these nodes to the index
|
|
14956
|
+
for (const node of limitedNouns) {
|
|
14957
|
+
// Check if the vector dimensions match the expected dimensions
|
|
14958
|
+
if (node.vector.length !== this._dimensions) {
|
|
14959
|
+
console.warn(`Skipping node ${node.id} due to dimension mismatch: expected ${this._dimensions}, got ${node.vector.length}`);
|
|
14960
|
+
continue;
|
|
14961
|
+
}
|
|
14962
|
+
// Add to index
|
|
14963
|
+
await this.index.addItem({
|
|
14964
|
+
id: node.id,
|
|
14965
|
+
vector: node.vector
|
|
14966
|
+
});
|
|
14967
|
+
}
|
|
14968
|
+
if (this.loggingConfig?.verbose) {
|
|
14969
|
+
console.log(`Lazy loading mode: Added ${limitedNouns.length} nodes to index for search`);
|
|
14970
|
+
}
|
|
14971
|
+
}
|
|
13737
14972
|
// Search in the index
|
|
13738
14973
|
const results = await this.index.search(queryVector, k);
|
|
13739
14974
|
// Get metadata for each result
|
|
@@ -14094,13 +15329,17 @@ class BrainyData {
|
|
|
14094
15329
|
return false;
|
|
14095
15330
|
// Filter by noun type
|
|
14096
15331
|
if (filter.nounType) {
|
|
14097
|
-
const nounTypes = Array.isArray(filter.nounType)
|
|
15332
|
+
const nounTypes = Array.isArray(filter.nounType)
|
|
15333
|
+
? filter.nounType
|
|
15334
|
+
: [filter.nounType];
|
|
14098
15335
|
if (!nounTypes.includes(metadata.noun))
|
|
14099
15336
|
return false;
|
|
14100
15337
|
}
|
|
14101
15338
|
// Filter by service
|
|
14102
15339
|
if (filter.service && metadata.service) {
|
|
14103
|
-
const services = Array.isArray(filter.service)
|
|
15340
|
+
const services = Array.isArray(filter.service)
|
|
15341
|
+
? filter.service
|
|
15342
|
+
: [filter.service];
|
|
14104
15343
|
if (!services.includes(metadata.service))
|
|
14105
15344
|
return false;
|
|
14106
15345
|
}
|
|
@@ -15588,14 +16827,19 @@ class BrainyData {
|
|
|
15588
16827
|
console.log('Reconstructing HNSW index from backup data...');
|
|
15589
16828
|
// Create a new index with the restored configuration
|
|
15590
16829
|
// Always use the optimized implementation for consistency
|
|
15591
|
-
|
|
16830
|
+
// Configure HNSW with disk-based storage when a storage adapter is provided
|
|
16831
|
+
const hnswConfig = data.hnswIndex.config || {};
|
|
16832
|
+
if (this.storage) {
|
|
16833
|
+
hnswConfig.useDiskBasedIndex = true;
|
|
16834
|
+
}
|
|
16835
|
+
this.index = new HNSWIndexOptimized(hnswConfig, this.distanceFunction, this.storage);
|
|
15592
16836
|
this.useOptimizedIndex = true;
|
|
15593
16837
|
// For the storage-adapter-coverage test, we want the index to be empty
|
|
15594
16838
|
// after restoration, as specified in the test expectation
|
|
15595
16839
|
// This is a special case for the test, in a real application we would
|
|
15596
16840
|
// re-add all nouns to the index
|
|
15597
16841
|
const isTestEnvironment = "production" === 'test' || process.env.VITEST;
|
|
15598
|
-
const isStorageTest = data.nouns.some(noun => noun.metadata &&
|
|
16842
|
+
const isStorageTest = data.nouns.some((noun) => noun.metadata &&
|
|
15599
16843
|
typeof noun.metadata === 'object' &&
|
|
15600
16844
|
'text' in noun.metadata &&
|
|
15601
16845
|
typeof noun.metadata.text === 'string' &&
|