@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/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 && !options.filter.service && !options.filter.metadata) {
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, we need to get all nouns
5870
- // but limit the number we load to avoid memory issues
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
- // Try to get only the nouns we need
5875
- allNouns = await this.getAllNouns_internal();
5876
- // If we have too many nouns, truncate the array to avoid memory issues
5877
- if (allNouns.length > maxNouns * 10) {
5878
- console.warn(`Large number of nouns (${allNouns.length}), truncating to ${maxNouns * 10} for filtering`);
5879
- allNouns = allNouns.slice(0, maxNouns * 10);
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 all nouns:', error);
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 && !options.filter.verbType &&
5960
- !options.filter.targetId && !options.filter.service &&
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 && !options.filter.verbType &&
5985
- !options.filter.sourceId && !options.filter.service &&
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 && !options.filter.sourceId &&
6010
- !options.filter.targetId && !options.filter.service &&
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, we need to get all verbs
6035
- // but limit the number we load to avoid memory issues
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
- // Try to get only the verbs we need
6040
- allVerbs = await this.getAllVerbs_internal();
6041
- // If we have too many verbs, truncate the array to avoid memory issues
6042
- if (allVerbs.length > maxVerbs * 10) {
6043
- console.warn(`Large number of verbs (${allVerbs.length}), truncating to ${maxVerbs * 10} for filtering`);
6044
- allVerbs = allVerbs.slice(0, maxVerbs * 10);
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 all verbs:', error);
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
- async executeGet(operation, operationName) {
7918
- return this.getExecutor(operation, operationName);
8719
+ getStats() {
8720
+ return { ...this.stats };
7919
8721
  }
7920
8722
  /**
7921
- * Execute an add operation with timeout and retry
8723
+ * Prefetch items based on ID patterns or relationships
8724
+ * @param ids Array of IDs to prefetch
7922
8725
  */
7923
- async executeAdd(operation, operationName) {
7924
- return this.addExecutor(operation, operationName);
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
- * Execute a delete operation with timeout and retry
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 executeDelete(operation, operationName) {
7930
- return this.deleteExecutor(operation, operationName);
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 || !parsedNode.id || !parsedNode.vector || !parsedNode.connections) {
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, GetObjectCommand } = await import('@aws-sdk/client-s3');
8189
- console.log(`Getting all nodes from bucket ${this.bucketName} with prefix ${this.nounPrefix}`);
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
- // If listResponse is null/undefined or there are no objects, return an empty array
8197
- if (!listResponse || !listResponse.Contents || listResponse.Contents.length === 0) {
8198
- console.log(`No nodes found in bucket ${this.bucketName} with prefix ${this.nounPrefix}`);
8199
- return nodes;
8200
- }
8201
- console.log(`Found ${listResponse.Contents.length} nodes in bucket ${this.bucketName}`);
8202
- // Debug: Log all keys found
8203
- console.log('Keys found:');
8204
- for (const object of listResponse.Contents) {
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
- // Get each node
8210
- const nodePromises = listResponse.Contents.map(async (object) => {
8211
- if (!object || !object.Key) {
8212
- console.log(`Skipping undefined object or object without Key`);
8213
- return null;
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
- try {
8216
- // Extract node ID from the key (remove prefix and .json extension)
8217
- const nodeId = object.Key.replace(this.nounPrefix, '').replace('.json', '');
8218
- console.log(`Getting node with ID ${nodeId} from key ${object.Key}`);
8219
- // Get the node data
8220
- const response = await this.s3Client.send(new GetObjectCommand({
8221
- Bucket: this.bucketName,
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
- // Convert serialized connections back to Map<number, Set<string>>
8242
- const connections = new Map();
8243
- for (const [level, nodeIds] of Object.entries(parsedNode.connections)) {
8244
- connections.set(Number(level), new Set(nodeIds));
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
- return filteredNodes;
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 all nodes:', error);
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
- for (const node of allNodes) {
8300
- const metadata = await this.getMetadata(node.id);
8301
- if (metadata && metadata.noun === nounType) {
8302
- filteredNodes.push(node);
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 || !parsedEdge.id || !parsedEdge.vector || !parsedEdge.connections ||
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
- // Import the ListObjectsV2Command and GetObjectCommand only when needed
8490
- const { ListObjectsV2Command, GetObjectCommand } = await import('@aws-sdk/client-s3');
8491
- // List all objects in the verbs directory
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
- // If there are no objects, return an empty array
8498
- if (!listResponse.Contents || listResponse.Contents.length === 0) {
8499
- return edges;
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
- // Get each edge
8502
- const edgePromises = listResponse.Contents.map(async (object) => {
8503
- try {
8504
- // Extract edge ID from the key (remove prefix and .json extension)
8505
- const edgeId = object.Key.replace(this.verbPrefix, '').replace('.json', '');
8506
- // Get the edge data
8507
- const response = await this.s3Client.send(new GetObjectCommand({
8508
- Bucket: this.bucketName,
8509
- Key: object.Key
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
- // Convert the response body to a string
8512
- const bodyContents = await response.Body.transformToString();
8513
- const parsedEdge = JSON.parse(bodyContents);
8514
- // Convert serialized connections back to Map<number, Set<string>>
8515
- const connections = new Map();
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
- // Wait for all promises to resolve and filter out nulls
8552
- const resolvedEdges = await Promise.all(edgePromises);
8553
- return resolvedEdges.filter((edge) => edge !== null);
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 all edges:', error);
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 && (error.message.includes('NoSuchKey') ||
8726
- error.message.includes('not found') ||
8727
- error.message.includes('does not exist')))) {
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 || !listResponse.Contents || listResponse.Contents.length === 0) {
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 || !listResponse.Contents || listResponse.Contents.length === 0) {
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' ? object.Size :
8810
- (object.Size ? parseInt(object.Size.toString(), 10) : 0);
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 = nounsResult.size + verbsResult.size + metadataResult.size + indexResult.size;
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 && (nodeCount > 0 || edgeCount > 0 || metadataCount > 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] = (nounTypeCounts[metadata.noun] || 0) + 1;
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 = { ...storageStats.nounCount };
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 = { ...storageStats.verbCount };
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 = { ...storageStats.metadataCount };
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 && (error.message.includes('NoSuchKey') ||
9173
- error.message.includes('not found') ||
9174
- error.message.includes('does not exist')))) {
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
- 'timestamp': entry.timestamp.toString(),
9204
- 'operation': entry.operation,
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
- this.index = new HNSWIndexOptimized(config.hnsw || {}, this.distanceFunction, config.storageAdapter || null);
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) ? filter.nounType : [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) ? filter.service : [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
- this.index = new HNSWIndexOptimized(data.hnswIndex.config, this.distanceFunction, this.storage);
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' &&