jexidb 2.1.9 → 2.2.1

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/Database.cjs CHANGED
@@ -418,6 +418,10 @@ class IndexManager {
418
418
  this.rangeThreshold = 10; // Sensible threshold: 10+ consecutive numbers justify ranges
419
419
  this.binarySearchThreshold = 32; // Much higher for better performance
420
420
  this.database = database; // Reference to database for term manager access
421
+ this.indexLoaded = false;
422
+ this.indexIdleUnloadMs = typeof this.opts.indexIdleUnloadMs === 'number' ? this.opts.indexIdleUnloadMs : 30000;
423
+ this._idleUnloadTimer = null;
424
+ this._indexLoadPromise = null;
421
425
 
422
426
  // CRITICAL: Use database mutex to prevent deadlocks
423
427
  // If no database mutex provided, create a local one (for backward compatibility)
@@ -429,9 +433,9 @@ class IndexManager {
429
433
  this.totalLines = total;
430
434
  }
431
435
 
432
- /**
433
- * Update indexes configuration and ensure internal structures stay in sync
434
- * @param {Object|Array<string>} indexes
436
+ /**
437
+ * Update indexes configuration and ensure internal structures stay in sync
438
+ * @param {Object|Array<string>} indexes
435
439
  */
436
440
  setIndexesConfig(indexes) {
437
441
  if (!indexes) {
@@ -463,11 +467,57 @@ class IndexManager {
463
467
  }
464
468
  }
465
469
  }
470
+ _clearIdleUnloadTimer() {
471
+ if (this._idleUnloadTimer) {
472
+ clearTimeout(this._idleUnloadTimer);
473
+ this._idleUnloadTimer = null;
474
+ }
475
+ }
476
+ cancelIdleUnloadTimer() {
477
+ this._clearIdleUnloadTimer();
478
+ }
479
+ markIndexUsed() {
480
+ if (!this.indexLoaded) return;
481
+ this._clearIdleUnloadTimer();
482
+ if (this.indexIdleUnloadMs > 0) {
483
+ const timer = setTimeout(() => {
484
+ try {
485
+ this.unload();
486
+ } catch (error) {
487
+ if (this.opts.debugMode) {
488
+ console.error('⚠️ IndexManager.markIndexUsed: Failed to unload index', error);
489
+ }
490
+ }
491
+ }, this.indexIdleUnloadMs);
492
+ if (timer && typeof timer.unref === 'function') {
493
+ timer.unref();
494
+ }
495
+ this._idleUnloadTimer = timer;
496
+ }
497
+ }
498
+ unload() {
499
+ if (!this.indexLoaded) return;
500
+ if (this.opts.debugMode) {
501
+ console.log(`🧹 IndexManager.unload: Unloading index data after ${this.indexIdleUnloadMs}ms idle`);
502
+ }
466
503
 
467
- /**
468
- * Check if a field is configured as an index
469
- * @param {string} field - Field name
470
- * @returns {boolean}
504
+ // Mark rebuild as needed so score() can trigger lazy reload
505
+ if (this.database) {
506
+ this.database._indexRebuildNeeded = true;
507
+ }
508
+ this._clearIdleUnloadTimer();
509
+ const newData = {};
510
+ for (const field of this.indexedFields) {
511
+ newData[field] = {};
512
+ }
513
+ this.index.data = newData;
514
+ this.indexLoaded = false;
515
+ }
516
+
517
+ /**
518
+ * Check if a field is configured as an index
519
+ * @param {string} field - Field name
520
+ * @returns {boolean}
471
521
  */
472
522
  isFieldIndexed(field) {
473
523
  if (!field) return false;
@@ -477,10 +527,10 @@ class IndexManager {
477
527
  return this.indexedFields.includes(field);
478
528
  }
479
529
 
480
- /**
481
- * Determine whether the index has usable data for a given field
482
- * @param {string} field - Field name
483
- * @returns {boolean}
530
+ /**
531
+ * Determine whether the index has usable data for a given field
532
+ * @param {string} field - Field name
533
+ * @returns {boolean}
484
534
  */
485
535
  hasUsableIndexData(field) {
486
536
  if (!field) return false;
@@ -875,6 +925,7 @@ class IndexManager {
875
925
 
876
926
  // OPTIMIZATION 6: Ultra-fast add operation with incremental index updates
877
927
  async add(row, lineNumber) {
928
+ this.markIndexUsed();
878
929
  if (typeof row !== 'object' || !row) {
879
930
  throw new Error('Invalid \'row\' parameter, it must be an object');
880
931
  }
@@ -950,12 +1001,13 @@ class IndexManager {
950
1001
  }
951
1002
  }
952
1003
 
953
- /**
954
- * OPTIMIZATION 6: Add multiple records to the index in batch with optimized operations
955
- * @param {Array} records - Records to add
956
- * @param {number} startLineNumber - Starting line number
1004
+ /**
1005
+ * OPTIMIZATION 6: Add multiple records to the index in batch with optimized operations
1006
+ * @param {Array} records - Records to add
1007
+ * @param {number} startLineNumber - Starting line number
957
1008
  */
958
1009
  async addBatch(records, startLineNumber) {
1010
+ this.markIndexUsed();
959
1011
  if (!records || !records.length) return;
960
1012
 
961
1013
  // OPTIMIZATION 6: Pre-allocate index structures for better performance
@@ -1072,6 +1124,7 @@ class IndexManager {
1072
1124
 
1073
1125
  // Cleanup method to free memory
1074
1126
  cleanup() {
1127
+ this._clearIdleUnloadTimer();
1075
1128
  const data = this.index.data;
1076
1129
  for (const field in data) {
1077
1130
  for (const value in data[field]) {
@@ -1096,12 +1149,14 @@ class IndexManager {
1096
1149
 
1097
1150
  // Clear all indexes
1098
1151
  clear() {
1152
+ this._clearIdleUnloadTimer();
1099
1153
  this.index.data = {};
1100
1154
  this.totalLines = 0;
1101
1155
  }
1102
1156
 
1103
1157
  // Update a record in the index
1104
1158
  async update(oldRecord, newRecord, lineNumber = null) {
1159
+ this.markIndexUsed();
1105
1160
  if (!oldRecord || !newRecord) return;
1106
1161
 
1107
1162
  // Remove old record by ID
@@ -1130,6 +1185,7 @@ class IndexManager {
1130
1185
 
1131
1186
  // Remove a record from the index
1132
1187
  async remove(record) {
1188
+ this.markIndexUsed();
1133
1189
  if (!record) return;
1134
1190
 
1135
1191
  // If record is an array of line numbers, use the original method
@@ -1307,6 +1363,7 @@ class IndexManager {
1307
1363
 
1308
1364
  // Ultra-fast query with early exit and smart processing
1309
1365
  query(criteria, options = {}) {
1366
+ this.markIndexUsed();
1310
1367
  if (typeof options === 'boolean') {
1311
1368
  options = {
1312
1369
  matchAny: options
@@ -1772,36 +1829,37 @@ class IndexManager {
1772
1829
  return matchingLines || new Set();
1773
1830
  }
1774
1831
 
1775
- /**
1776
- * Check if any records exist for given field and terms (index-only, ultra-fast)
1777
- * Stops at first match for maximum performance - no disk I/O required
1778
- *
1779
- * @param {string} fieldName - Indexed field name (e.g., 'nameTerms', 'groupTerms')
1780
- * @param {string|Array<string>} terms - Single term or array of terms to check
1781
- * @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
1782
- * @returns {boolean} - True if at least one match exists
1783
- *
1784
- * @example
1785
- * // Check if any record has 'channel' in nameTerms
1786
- * indexManager.exists('nameTerms', 'channel')
1787
- *
1788
- * @example
1789
- * // Check if any record has ALL terms ['a', 'e'] in nameTerms ($all)
1790
- * indexManager.exists('nameTerms', ['a', 'e'], { $all: true })
1791
- *
1792
- * @example
1793
- * // Check if any record has ANY of the terms ['channel', 'tv'] in nameTerms
1794
- * indexManager.exists('nameTerms', ['channel', 'tv'], { $all: false })
1795
- *
1796
- * @example
1797
- * // Check if any record has 'tv' but NOT 'globo' in nameTerms
1798
- * indexManager.exists('nameTerms', 'tv', { excludes: ['globo'] })
1799
- *
1800
- * @example
1801
- * // Check if any record has ['tv', 'news'] but NOT 'sports' in nameTerms
1802
- * indexManager.exists('nameTerms', ['tv', 'news'], { $all: true, excludes: ['sports'] })
1832
+ /**
1833
+ * Check if any records exist for given field and terms (index-only, ultra-fast)
1834
+ * Stops at first match for maximum performance - no disk I/O required
1835
+ *
1836
+ * @param {string} fieldName - Indexed field name (e.g., 'nameTerms', 'groupTerms')
1837
+ * @param {string|Array<string>} terms - Single term or array of terms to check
1838
+ * @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
1839
+ * @returns {boolean} - True if at least one match exists
1840
+ *
1841
+ * @example
1842
+ * // Check if any record has 'channel' in nameTerms
1843
+ * indexManager.exists('nameTerms', 'channel')
1844
+ *
1845
+ * @example
1846
+ * // Check if any record has ALL terms ['a', 'e'] in nameTerms ($all)
1847
+ * indexManager.exists('nameTerms', ['a', 'e'], { $all: true })
1848
+ *
1849
+ * @example
1850
+ * // Check if any record has ANY of the terms ['channel', 'tv'] in nameTerms
1851
+ * indexManager.exists('nameTerms', ['channel', 'tv'], { $all: false })
1852
+ *
1853
+ * @example
1854
+ * // Check if any record has 'tv' but NOT 'globo' in nameTerms
1855
+ * indexManager.exists('nameTerms', 'tv', { excludes: ['globo'] })
1856
+ *
1857
+ * @example
1858
+ * // Check if any record has ['tv', 'news'] but NOT 'sports' in nameTerms
1859
+ * indexManager.exists('nameTerms', ['tv', 'news'], { $all: true, excludes: ['sports'] })
1803
1860
  */
1804
1861
  exists(fieldName, terms, options = {}) {
1862
+ this.markIndexUsed();
1805
1863
  // Early exit: validate fieldName
1806
1864
  if (!fieldName || typeof fieldName !== 'string') {
1807
1865
  return false;
@@ -2154,16 +2212,17 @@ class IndexManager {
2154
2212
  return candidateLines.size > 0;
2155
2213
  }
2156
2214
 
2157
- /**
2158
- * Evaluate many index-only existence checks while sharing the same field data.
2159
- * Returns a map keyed by the criteria id so callers can reuse a single scan per field.
2160
- *
2161
- * @param {string} fieldName - Indexed field name
2162
- * @param {Array<Object>} criteriaArray - Array of { id, terms, options }
2163
- * @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
2164
- * @returns {Object<string, boolean>} - Map of criteria id to boolean existence
2215
+ /**
2216
+ * Evaluate many index-only existence checks while sharing the same field data.
2217
+ * Returns a map keyed by the criteria id so callers can reuse a single scan per field.
2218
+ *
2219
+ * @param {string} fieldName - Indexed field name
2220
+ * @param {Array<Object>} criteriaArray - Array of { id, terms, options }
2221
+ * @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
2222
+ * @returns {Object<string, boolean>} - Map of criteria id to boolean existence
2165
2223
  */
2166
2224
  multiExists(fieldName, criteriaArray, opts = {}) {
2225
+ this.markIndexUsed();
2167
2226
  const results = {};
2168
2227
  if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
2169
2228
  return results;
@@ -2549,6 +2608,8 @@ class IndexManager {
2549
2608
  console.log(`🔍 IndexManager.load: No data loaded, preserving initialized fields: ${Object.keys(this.index.data).join(', ')}`);
2550
2609
  }
2551
2610
  // Keep the current index with initialized fields
2611
+ this.indexLoaded = true;
2612
+ this.markIndexUsed();
2552
2613
  return;
2553
2614
  }
2554
2615
 
@@ -2560,11 +2621,13 @@ class IndexManager {
2560
2621
  }
2561
2622
  }
2562
2623
  this.index = processedIndex;
2624
+ this.indexLoaded = true;
2625
+ this.markIndexUsed();
2563
2626
  }
2564
2627
 
2565
- /**
2566
- * CRITICAL FIX: Initialize default index structure when invalid data is provided
2567
- * This prevents TypeError when Object.keys() is called on null/undefined
2628
+ /**
2629
+ * CRITICAL FIX: Initialize default index structure when invalid data is provided
2630
+ * This prevents TypeError when Object.keys() is called on null/undefined
2568
2631
  */
2569
2632
  _initializeDefaults() {
2570
2633
  if (this.opts.debugMode) {
@@ -2585,17 +2648,13 @@ class IndexManager {
2585
2648
  }
2586
2649
  }
2587
2650
  }
2588
- if (this.opts.debugMode) {
2589
- console.log(`🔍 IndexManager._initializeDefaults: Initialized with fields: ${Object.keys(this.index.data).join(', ')}`);
2590
- }
2591
- }
2592
- readColumnIndex(column) {
2593
- return new Set(this.index.data && this.index.data[column] ? Object.keys(this.index.data[column]) : []);
2651
+ this.indexLoaded = true;
2652
+ this.markIndexUsed();
2594
2653
  }
2595
2654
 
2596
- /**
2597
- * Convert index to JSON-serializable format for debugging and export
2598
- * This resolves the issue where Sets appear as empty objects in JSON.stringify
2655
+ /**
2656
+ * Convert index to JSON-serializable format for debugging and export
2657
+ * This resolves the issue where Sets appear as empty objects in JSON.stringify
2599
2658
  */
2600
2659
  toJSON() {
2601
2660
  const serializable = {
@@ -2661,9 +2720,9 @@ class IndexManager {
2661
2720
  return serializable;
2662
2721
  }
2663
2722
 
2664
- /**
2665
- * Get a JSON string representation of the index
2666
- * This properly handles Sets unlike the default JSON.stringify
2723
+ /**
2724
+ * Get a JSON string representation of the index
2725
+ * This properly handles Sets unlike the default JSON.stringify
2667
2726
  */
2668
2727
  toString() {
2669
2728
  return JSON.stringify(this.toJSON(), null, 2);
@@ -2671,8 +2730,8 @@ class IndexManager {
2671
2730
 
2672
2731
  // Simplified term mapping methods - just basic functionality
2673
2732
 
2674
- /**
2675
- * Rebuild index (stub for compatibility)
2733
+ /**
2734
+ * Rebuild index (stub for compatibility)
2676
2735
  */
2677
2736
  async rebuild() {
2678
2737
  // Stub implementation for compatibility
@@ -2680,9 +2739,9 @@ class IndexManager {
2680
2739
  }
2681
2740
  }
2682
2741
 
2683
- /**
2684
- * SchemaManager - Manages field schemas for optimized array-based serialization
2685
- * This replaces the need for repeating field names in JSON objects
2742
+ /**
2743
+ * SchemaManager - Manages field schemas for optimized array-based serialization
2744
+ * This replaces the need for repeating field names in JSON objects
2686
2745
  */
2687
2746
  class SchemaManager {
2688
2747
  constructor(opts = {}) {
@@ -2700,8 +2759,8 @@ class SchemaManager {
2700
2759
  this.isInitialized = false;
2701
2760
  }
2702
2761
 
2703
- /**
2704
- * Initialize schema from options or auto-detect from data
2762
+ /**
2763
+ * Initialize schema from options or auto-detect from data
2705
2764
  */
2706
2765
  initializeSchema(schemaOrData, autoDetect = false) {
2707
2766
  if (this.isInitialized && this.opts.strictSchema) {
@@ -2726,8 +2785,8 @@ class SchemaManager {
2726
2785
  }
2727
2786
  }
2728
2787
 
2729
- /**
2730
- * Set explicit schema
2788
+ /**
2789
+ * Set explicit schema
2731
2790
  */
2732
2791
  setSchema(fieldNames) {
2733
2792
  this.schema = [...fieldNames]; // Create copy
@@ -2742,8 +2801,8 @@ class SchemaManager {
2742
2801
  }
2743
2802
  }
2744
2803
 
2745
- /**
2746
- * Auto-detect schema from sample data
2804
+ /**
2805
+ * Auto-detect schema from sample data
2747
2806
  */
2748
2807
  autoDetectSchema(sampleData) {
2749
2808
  if (Array.isArray(sampleData)) {
@@ -2764,9 +2823,9 @@ class SchemaManager {
2764
2823
  }
2765
2824
  }
2766
2825
 
2767
- /**
2768
- * Initialize schema from database options
2769
- * Note: schema option is no longer supported, use fields instead
2826
+ /**
2827
+ * Initialize schema from database options
2828
+ * Note: schema option is no longer supported, use fields instead
2770
2829
  */
2771
2830
  initializeFromOptions(opts) {
2772
2831
  // Schema option is no longer supported - fields should be used instead
@@ -2774,8 +2833,8 @@ class SchemaManager {
2774
2833
  // Schema initialization is handled by Database.initializeSchema() using fields
2775
2834
  }
2776
2835
 
2777
- /**
2778
- * Add new field to schema (for schema evolution)
2836
+ /**
2837
+ * Add new field to schema (for schema evolution)
2779
2838
  */
2780
2839
  addField(fieldName) {
2781
2840
  if (this.fieldToIndex.has(fieldName)) {
@@ -2791,8 +2850,8 @@ class SchemaManager {
2791
2850
  return newIndex;
2792
2851
  }
2793
2852
 
2794
- /**
2795
- * Convert object to array using schema with strict field enforcement
2853
+ /**
2854
+ * Convert object to array using schema with strict field enforcement
2796
2855
  */
2797
2856
  objectToArray(obj) {
2798
2857
  if (!this.isInitialized || !this.opts.enableArraySerialization) {
@@ -2818,8 +2877,8 @@ class SchemaManager {
2818
2877
  return result;
2819
2878
  }
2820
2879
 
2821
- /**
2822
- * Convert array back to object using schema
2880
+ /**
2881
+ * Convert array back to object using schema
2823
2882
  */
2824
2883
  arrayToObject(arr) {
2825
2884
  if (!this.isInitialized || !this.opts.enableArraySerialization) {
@@ -2867,43 +2926,43 @@ class SchemaManager {
2867
2926
  return obj;
2868
2927
  }
2869
2928
 
2870
- /**
2871
- * Get field index by name
2929
+ /**
2930
+ * Get field index by name
2872
2931
  */
2873
2932
  getFieldIndex(fieldName) {
2874
2933
  return this.fieldToIndex.get(fieldName);
2875
2934
  }
2876
2935
 
2877
- /**
2878
- * Get field name by index
2936
+ /**
2937
+ * Get field name by index
2879
2938
  */
2880
2939
  getFieldName(index) {
2881
2940
  return this.indexToField.get(index);
2882
2941
  }
2883
2942
 
2884
- /**
2885
- * Check if field exists in schema
2943
+ /**
2944
+ * Check if field exists in schema
2886
2945
  */
2887
2946
  hasField(fieldName) {
2888
2947
  return this.fieldToIndex.has(fieldName);
2889
2948
  }
2890
2949
 
2891
- /**
2892
- * Get schema as array of field names
2950
+ /**
2951
+ * Get schema as array of field names
2893
2952
  */
2894
2953
  getSchema() {
2895
2954
  return [...this.schema]; // Return copy
2896
2955
  }
2897
2956
 
2898
- /**
2899
- * Get schema size
2957
+ /**
2958
+ * Get schema size
2900
2959
  */
2901
2960
  getSchemaSize() {
2902
2961
  return this.schema.length;
2903
2962
  }
2904
2963
 
2905
- /**
2906
- * Validate that object conforms to schema
2964
+ /**
2965
+ * Validate that object conforms to schema
2907
2966
  */
2908
2967
  validateObject(obj) {
2909
2968
  if (!this.isInitialized || !this.opts.strictSchema) {
@@ -2925,8 +2984,8 @@ class SchemaManager {
2925
2984
  return true;
2926
2985
  }
2927
2986
 
2928
- /**
2929
- * Get schema metadata for serialization
2987
+ /**
2988
+ * Get schema metadata for serialization
2930
2989
  */
2931
2990
  getSchemaMetadata() {
2932
2991
  return {
@@ -2937,8 +2996,8 @@ class SchemaManager {
2937
2996
  };
2938
2997
  }
2939
2998
 
2940
- /**
2941
- * Reset schema
2999
+ /**
3000
+ * Reset schema
2942
3001
  */
2943
3002
  reset() {
2944
3003
  this.schema = [];
@@ -2948,8 +3007,8 @@ class SchemaManager {
2948
3007
  this.schemaVersion++;
2949
3008
  }
2950
3009
 
2951
- /**
2952
- * Get performance statistics
3010
+ /**
3011
+ * Get performance statistics
2953
3012
  */
2954
3013
  getStats() {
2955
3014
  return {
@@ -2992,22 +3051,22 @@ class Serializer {
2992
3051
  };
2993
3052
  }
2994
3053
 
2995
- /**
2996
- * Initialize schema for array-based serialization
3054
+ /**
3055
+ * Initialize schema for array-based serialization
2997
3056
  */
2998
3057
  initializeSchema(schemaOrData, autoDetect = false) {
2999
3058
  this.schemaManager.initializeSchema(schemaOrData, autoDetect);
3000
3059
  }
3001
3060
 
3002
- /**
3003
- * Get current schema
3061
+ /**
3062
+ * Get current schema
3004
3063
  */
3005
3064
  getSchema() {
3006
3065
  return this.schemaManager.getSchema();
3007
3066
  }
3008
3067
 
3009
- /**
3010
- * Convert object to array format for optimized serialization
3068
+ /**
3069
+ * Convert object to array format for optimized serialization
3011
3070
  */
3012
3071
  convertToArrayFormat(obj) {
3013
3072
  if (!this.opts.enableArraySerialization) {
@@ -3016,8 +3075,8 @@ class Serializer {
3016
3075
  return this.schemaManager.objectToArray(obj);
3017
3076
  }
3018
3077
 
3019
- /**
3020
- * Convert array format back to object
3078
+ /**
3079
+ * Convert array format back to object
3021
3080
  */
3022
3081
  convertFromArrayFormat(arr) {
3023
3082
  if (!this.opts.enableArraySerialization) {
@@ -3044,8 +3103,8 @@ class Serializer {
3044
3103
  return obj;
3045
3104
  }
3046
3105
 
3047
- /**
3048
- * Advanced serialization with optimized JSON and buffer pooling
3106
+ /**
3107
+ * Advanced serialization with optimized JSON and buffer pooling
3049
3108
  */
3050
3109
  serialize(data, opts = {}) {
3051
3110
  this.serializationStats.totalSerializations++;
@@ -3072,8 +3131,8 @@ class Serializer {
3072
3131
  return this.serializeStandard(serializationData, addLinebreak);
3073
3132
  }
3074
3133
 
3075
- /**
3076
- * Advanced serialization with optimized JSON.stringify and buffer pooling
3134
+ /**
3135
+ * Advanced serialization with optimized JSON.stringify and buffer pooling
3077
3136
  */
3078
3137
  serializeAdvanced(data, addLinebreak) {
3079
3138
  // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
@@ -3098,9 +3157,9 @@ class Serializer {
3098
3157
  return result;
3099
3158
  }
3100
3159
 
3101
- /**
3102
- * Proper encoding normalization with UTF-8 validation
3103
- * Fixed to prevent double-encoding and data corruption
3160
+ /**
3161
+ * Proper encoding normalization with UTF-8 validation
3162
+ * Fixed to prevent double-encoding and data corruption
3104
3163
  */
3105
3164
  normalizeEncoding(str) {
3106
3165
  if (typeof str !== 'string') return str;
@@ -3112,8 +3171,8 @@ class Serializer {
3112
3171
  return this.safeConvertToUTF8(str);
3113
3172
  }
3114
3173
 
3115
- /**
3116
- * Check if string is valid UTF-8
3174
+ /**
3175
+ * Check if string is valid UTF-8
3117
3176
  */
3118
3177
  isValidUTF8(str) {
3119
3178
  try {
@@ -3126,8 +3185,8 @@ class Serializer {
3126
3185
  }
3127
3186
  }
3128
3187
 
3129
- /**
3130
- * Safe conversion to UTF-8 with proper encoding detection
3188
+ /**
3189
+ * Safe conversion to UTF-8 with proper encoding detection
3131
3190
  */
3132
3191
  safeConvertToUTF8(str) {
3133
3192
  // Try common encodings in order of likelihood
@@ -3151,9 +3210,9 @@ class Serializer {
3151
3210
  return str;
3152
3211
  }
3153
3212
 
3154
- /**
3155
- * Enhanced deep encoding normalization with UTF-8 validation
3156
- * Fixed to prevent double-encoding and data corruption
3213
+ /**
3214
+ * Enhanced deep encoding normalization with UTF-8 validation
3215
+ * Fixed to prevent double-encoding and data corruption
3157
3216
  */
3158
3217
  deepNormalizeEncoding(obj) {
3159
3218
  if (obj === null || obj === undefined) return obj;
@@ -3179,13 +3238,13 @@ class Serializer {
3179
3238
  return obj;
3180
3239
  }
3181
3240
 
3182
- /**
3183
- * Validate encoding before serialization
3241
+ /**
3242
+ * Validate encoding before serialization
3184
3243
  */
3185
- /**
3186
- * Sanitize data to remove problematic characters that break JSON parsing
3187
- * CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
3188
- * by removing control characters that cannot be safely represented in JSON
3244
+ /**
3245
+ * Sanitize data to remove problematic characters that break JSON parsing
3246
+ * CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
3247
+ * by removing control characters that cannot be safely represented in JSON
3189
3248
  */
3190
3249
  sanitizeDataForJSON(data) {
3191
3250
  const sanitizeString = str => {
@@ -3247,9 +3306,9 @@ class Serializer {
3247
3306
  return issues.length === 0;
3248
3307
  }
3249
3308
 
3250
- /**
3251
- * Optimized JSON.stringify with fast paths for common data structures
3252
- * Now includes deep encoding normalization for all string fields
3309
+ /**
3310
+ * Optimized JSON.stringify with fast paths for common data structures
3311
+ * Now includes deep encoding normalization for all string fields
3253
3312
  */
3254
3313
  optimizedStringify(obj) {
3255
3314
  // CRITICAL: Normalize encoding for all string fields before stringify
@@ -3310,8 +3369,8 @@ class Serializer {
3310
3369
  return result;
3311
3370
  }
3312
3371
 
3313
- /**
3314
- * Standard serialization (fallback)
3372
+ /**
3373
+ * Standard serialization (fallback)
3315
3374
  */
3316
3375
  serializeStandard(data, addLinebreak) {
3317
3376
  // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
@@ -3337,8 +3396,8 @@ class Serializer {
3337
3396
  return result;
3338
3397
  }
3339
3398
 
3340
- /**
3341
- * Advanced deserialization with fast paths
3399
+ /**
3400
+ * Advanced deserialization with fast paths
3342
3401
  */
3343
3402
  deserialize(data) {
3344
3403
  this.serializationStats.totalDeserializations++;
@@ -3522,8 +3581,8 @@ class Serializer {
3522
3581
  }
3523
3582
  }
3524
3583
 
3525
- /**
3526
- * Batch serialization for multiple records
3584
+ /**
3585
+ * Batch serialization for multiple records
3527
3586
  */
3528
3587
  serializeBatch(dataArray, opts = {}) {
3529
3588
  // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
@@ -3574,8 +3633,8 @@ class Serializer {
3574
3633
  }
3575
3634
  }
3576
3635
 
3577
- /**
3578
- * Batch deserialization for multiple records
3636
+ /**
3637
+ * Batch deserialization for multiple records
3579
3638
  */
3580
3639
  deserializeBatch(dataArray) {
3581
3640
  // Optimization: try to parse all entries as a single JSON array first
@@ -3617,16 +3676,16 @@ class Serializer {
3617
3676
  }
3618
3677
  }
3619
3678
 
3620
- /**
3621
- * Check if data appears to be binary (always false since we only use JSON now)
3679
+ /**
3680
+ * Check if data appears to be binary (always false since we only use JSON now)
3622
3681
  */
3623
3682
  isBinaryData(data) {
3624
3683
  // All data is now JSON format
3625
3684
  return false;
3626
3685
  }
3627
3686
 
3628
- /**
3629
- * Get comprehensive performance statistics
3687
+ /**
3688
+ * Get comprehensive performance statistics
3630
3689
  */
3631
3690
  getStats() {
3632
3691
  // NOTE: Buffer pool stats removed - buffer pool was causing more problems than benefits
@@ -3645,8 +3704,8 @@ class Serializer {
3645
3704
  };
3646
3705
  }
3647
3706
 
3648
- /**
3649
- * Cleanup resources
3707
+ /**
3708
+ * Cleanup resources
3650
3709
  */
3651
3710
  cleanup() {
3652
3711
  // NOTE: Buffer pool cleanup removed - buffer pool was causing more problems than benefits
@@ -4626,12 +4685,12 @@ class FileHandler {
4626
4685
  return groupedRanges;
4627
4686
  }
4628
4687
 
4629
- /**
4630
- * Ensure a line is complete by reading until newline if JSON appears truncated
4631
- * @param {string} line - The potentially incomplete line
4632
- * @param {number} fd - File descriptor
4633
- * @param {number} currentOffset - Current read offset
4634
- * @returns {string} Complete line
4688
+ /**
4689
+ * Ensure a line is complete by reading until newline if JSON appears truncated
4690
+ * @param {string} line - The potentially incomplete line
4691
+ * @param {number} fd - File descriptor
4692
+ * @param {number} currentOffset - Current read offset
4693
+ * @returns {string} Complete line
4635
4694
  */
4636
4695
  async ensureCompleteLine(line, fd, currentOffset) {
4637
4696
  // Fast check: if line already ends with newline, it's likely complete
@@ -4707,12 +4766,12 @@ class FileHandler {
4707
4766
  }
4708
4767
  }
4709
4768
 
4710
- /**
4711
- * Split content into complete JSON lines, handling special characters and escaped quotes
4712
- * CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors by ensuring
4713
- * each line is a complete, valid JSON object/array, even when containing special characters
4714
- * @param {string} content - Raw content containing multiple JSON lines
4715
- * @returns {string[]} Array of complete JSON lines
4769
+ /**
4770
+ * Split content into complete JSON lines, handling special characters and escaped quotes
4771
+ * CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors by ensuring
4772
+ * each line is a complete, valid JSON object/array, even when containing special characters
4773
+ * @param {string} content - Raw content containing multiple JSON lines
4774
+ * @returns {string[]} Array of complete JSON lines
4716
4775
  */
4717
4776
  splitJsonLines(content) {
4718
4777
  const lines = [];
@@ -5149,16 +5208,16 @@ class FileHandler {
5149
5208
  await fs.promises.appendFile(this.file, data);
5150
5209
  }
5151
5210
 
5152
- /**
5153
- * Check if data appears to be binary (always false since we only use JSON now)
5211
+ /**
5212
+ * Check if data appears to be binary (always false since we only use JSON now)
5154
5213
  */
5155
5214
  isBinaryData(data) {
5156
5215
  // All data is now JSON format
5157
5216
  return false;
5158
5217
  }
5159
5218
 
5160
- /**
5161
- * Check if file is binary (always false since we only use JSON now)
5219
+ /**
5220
+ * Check if file is binary (always false since we only use JSON now)
5162
5221
  */
5163
5222
  async isBinaryFile() {
5164
5223
  // All files are now JSON format
@@ -5215,12 +5274,12 @@ class FileHandler {
5215
5274
  });
5216
5275
  }
5217
5276
 
5218
- /**
5219
- * Read records with streaming using readline
5220
- * @param {Object} criteria - Filter criteria
5221
- * @param {Object} options - Options (limit, skip)
5222
- * @param {Function} matchesCriteria - Function to check if record matches criteria
5223
- * @returns {Promise<Array>} - Array of records
5277
+ /**
5278
+ * Read records with streaming using readline
5279
+ * @param {Object} criteria - Filter criteria
5280
+ * @param {Object} options - Options (limit, skip)
5281
+ * @param {Function} matchesCriteria - Function to check if record matches criteria
5282
+ * @returns {Promise<Array>} - Array of records
5224
5283
  */
5225
5284
  async readWithStreaming(criteria, options = {}, matchesCriteria, serializer = null) {
5226
5285
  // CRITICAL: Always use file mutex to prevent concurrent file operations
@@ -5389,12 +5448,12 @@ class FileHandler {
5389
5448
  }
5390
5449
  }
5391
5450
 
5392
- /**
5393
- * Count records with streaming
5394
- * @param {Object} criteria - Filter criteria
5395
- * @param {Object} options - Options (limit)
5396
- * @param {Function} matchesCriteria - Function to check if record matches criteria
5397
- * @returns {Promise<number>} - Number of records
5451
+ /**
5452
+ * Count records with streaming
5453
+ * @param {Object} criteria - Filter criteria
5454
+ * @param {Object} options - Options (limit)
5455
+ * @param {Function} matchesCriteria - Function to check if record matches criteria
5456
+ * @returns {Promise<number>} - Number of records
5398
5457
  */
5399
5458
  async countWithStreaming(criteria, options = {}, matchesCriteria, serializer = null) {
5400
5459
  const {
@@ -5459,9 +5518,9 @@ class FileHandler {
5459
5518
  }
5460
5519
  }
5461
5520
 
5462
- /**
5463
- * Get file statistics
5464
- * @returns {Promise<Object>} - File statistics
5521
+ /**
5522
+ * Get file statistics
5523
+ * @returns {Promise<Object>} - File statistics
5465
5524
  */
5466
5525
  async getFileStats() {
5467
5526
  try {
@@ -5478,9 +5537,9 @@ class FileHandler {
5478
5537
  }
5479
5538
  }
5480
5539
 
5481
- /**
5482
- * Count lines in file
5483
- * @returns {Promise<number>} - Number of lines
5540
+ /**
5541
+ * Count lines in file
5542
+ * @returns {Promise<number>} - Number of lines
5484
5543
  */
5485
5544
  async countLines() {
5486
5545
  let lineCount = 0;
@@ -5568,10 +5627,10 @@ class FileHandler {
5568
5627
  }
5569
5628
  }
5570
5629
 
5571
- /**
5572
- * Optimized batch write operation (OPTIMIZATION)
5573
- * @param {Array} dataChunks - Array of data chunks to write
5574
- * @param {boolean} append - Whether to append or overwrite
5630
+ /**
5631
+ * Optimized batch write operation (OPTIMIZATION)
5632
+ * @param {Array} dataChunks - Array of data chunks to write
5633
+ * @param {boolean} append - Whether to append or overwrite
5575
5634
  */
5576
5635
  async writeBatch(dataChunks, append = false) {
5577
5636
  if (!dataChunks || !dataChunks.length) return;
@@ -5594,10 +5653,10 @@ class FileHandler {
5594
5653
  }
5595
5654
  }
5596
5655
 
5597
- /**
5598
- * OPTIMIZATION: Streaming write for multiple chunks
5599
- * @param {Array} dataChunks - Array of data chunks to write
5600
- * @param {boolean} append - Whether to append or overwrite
5656
+ /**
5657
+ * OPTIMIZATION: Streaming write for multiple chunks
5658
+ * @param {Array} dataChunks - Array of data chunks to write
5659
+ * @param {boolean} append - Whether to append or overwrite
5601
5660
  */
5602
5661
  async _writeBatchStreaming(dataChunks, append = false) {
5603
5662
  // OPTIMIZATION: Use createWriteStream for better performance
@@ -5628,9 +5687,9 @@ class FileHandler {
5628
5687
  });
5629
5688
  }
5630
5689
 
5631
- /**
5632
- * Optimized append operation for single data chunk (OPTIMIZATION)
5633
- * @param {string|Buffer} data - Data to append
5690
+ /**
5691
+ * Optimized append operation for single data chunk (OPTIMIZATION)
5692
+ * @param {string|Buffer} data - Data to append
5634
5693
  */
5635
5694
  async appendOptimized(data) {
5636
5695
  const release = this.fileMutex ? await this.fileMutex.acquire() : () => {};
@@ -5642,9 +5701,9 @@ class FileHandler {
5642
5701
  }
5643
5702
  }
5644
5703
 
5645
- /**
5646
- * Windows-specific retry logic for fs.promises.writeFile operations
5647
- * Based on node-graceful-fs workarounds for EPERM issues
5704
+ /**
5705
+ * Windows-specific retry logic for fs.promises.writeFile operations
5706
+ * Based on node-graceful-fs workarounds for EPERM issues
5648
5707
  */
5649
5708
  async _writeFileWithRetry(filePath, data, maxRetries = 3) {
5650
5709
  const isWindows = process.platform === 'win32';
@@ -5680,9 +5739,9 @@ class FileHandler {
5680
5739
  }
5681
5740
  }
5682
5741
 
5683
- /**
5684
- * Windows-specific retry logic for file operations
5685
- * Based on node-graceful-fs workarounds for EPERM issues
5742
+ /**
5743
+ * Windows-specific retry logic for file operations
5744
+ * Based on node-graceful-fs workarounds for EPERM issues
5686
5745
  */
5687
5746
  async _writeWithRetry(data, maxRetries = 3) {
5688
5747
  const isWindows = process.platform === 'win32';
@@ -5748,11 +5807,11 @@ class FileHandler {
5748
5807
  }
5749
5808
  }
5750
5809
 
5751
- /**
5752
- * Read specific lines from the file using line numbers
5753
- * This is optimized for partial reads when using indexed queries
5754
- * @param {number[]} lineNumbers - Array of line numbers to read (1-based)
5755
- * @returns {Promise<string>} - Content of the specified lines
5810
+ /**
5811
+ * Read specific lines from the file using line numbers
5812
+ * This is optimized for partial reads when using indexed queries
5813
+ * @param {number[]} lineNumbers - Array of line numbers to read (1-based)
5814
+ * @returns {Promise<string>} - Content of the specified lines
5756
5815
  */
5757
5816
  async readSpecificLines(lineNumbers) {
5758
5817
  if (!lineNumbers || lineNumbers.length === 0) {
@@ -5822,15 +5881,15 @@ class FileHandler {
5822
5881
  }
5823
5882
  }
5824
5883
 
5825
- /**
5826
- * QueryManager - Handles all query operations and strategies
5827
- *
5828
- * Responsibilities:
5829
- * - find(), findOne(), count(), query()
5830
- * - findWithStreaming(), findWithIndexed()
5831
- * - matchesCriteria(), extractQueryFields()
5832
- * - Query strategies (INDEXED vs STREAMING)
5833
- * - Result estimation
5884
+ /**
5885
+ * QueryManager - Handles all query operations and strategies
5886
+ *
5887
+ * Responsibilities:
5888
+ * - find(), findOne(), count(), query()
5889
+ * - findWithStreaming(), findWithIndexed()
5890
+ * - matchesCriteria(), extractQueryFields()
5891
+ * - Query strategies (INDEXED vs STREAMING)
5892
+ * - Result estimation
5834
5893
  */
5835
5894
 
5836
5895
  class QueryManager {
@@ -5849,11 +5908,11 @@ class QueryManager {
5849
5908
  };
5850
5909
  }
5851
5910
 
5852
- /**
5853
- * Main find method with strategy selection
5854
- * @param {Object} criteria - Query criteria
5855
- * @param {Object} options - Query options
5856
- * @returns {Promise<Array>} - Query results
5911
+ /**
5912
+ * Main find method with strategy selection
5913
+ * @param {Object} criteria - Query criteria
5914
+ * @param {Object} options - Query options
5915
+ * @returns {Promise<Array>} - Query results
5857
5916
  */
5858
5917
  async find(criteria, options = {}) {
5859
5918
  if (this.database.destroyed) throw new Error('Database is destroyed');
@@ -5902,11 +5961,11 @@ class QueryManager {
5902
5961
  }
5903
5962
  }
5904
5963
 
5905
- /**
5906
- * Find one record
5907
- * @param {Object} criteria - Query criteria
5908
- * @param {Object} options - Query options
5909
- * @returns {Promise<Object|null>} - First matching record or null
5964
+ /**
5965
+ * Find one record
5966
+ * @param {Object} criteria - Query criteria
5967
+ * @param {Object} options - Query options
5968
+ * @returns {Promise<Object|null>} - First matching record or null
5910
5969
  */
5911
5970
  async findOne(criteria, options = {}) {
5912
5971
  if (this.database.destroyed) throw new Error('Database is destroyed');
@@ -5957,11 +6016,11 @@ class QueryManager {
5957
6016
  }
5958
6017
  }
5959
6018
 
5960
- /**
5961
- * Count records matching criteria
5962
- * @param {Object} criteria - Query criteria
5963
- * @param {Object} options - Query options
5964
- * @returns {Promise<number>} - Count of matching records
6019
+ /**
6020
+ * Count records matching criteria
6021
+ * @param {Object} criteria - Query criteria
6022
+ * @param {Object} options - Query options
6023
+ * @returns {Promise<number>} - Count of matching records
5965
6024
  */
5966
6025
  async count(criteria, options = {}) {
5967
6026
  if (this.database.destroyed) throw new Error('Database is destroyed');
@@ -5985,7 +6044,18 @@ class QueryManager {
5985
6044
  const results = await this.findWithStreaming(criteria, options);
5986
6045
  count = results.length;
5987
6046
  } else {
5988
- // OPTIMIZATION: For indexed strategy, use indexManager.query().size directly
6047
+ try {
6048
+ await this.database._ensureLazyIndexLoaded();
6049
+ } catch (error) {
6050
+ if (this.opts.debugMode) {
6051
+ console.log('⚠️ _ensureLazyIndexLoaded failed in count, falling back to streaming', error.message || error);
6052
+ }
6053
+ const streamingResults = await this.findWithStreaming(criteria, {
6054
+ ...options,
6055
+ forceFullScan: true
6056
+ });
6057
+ return streamingResults.length;
6058
+ }
5989
6059
  // This avoids reading actual records from the file - much faster!
5990
6060
  const lineNumbers = this.indexManager.query(criteria, options);
5991
6061
  if (lineNumbers.size === 0) {
@@ -6038,21 +6108,21 @@ class QueryManager {
6038
6108
  return count;
6039
6109
  }
6040
6110
 
6041
- /**
6042
- * Compatibility method that redirects to find
6043
- * @param {Object} criteria - Query criteria
6044
- * @param {Object} options - Query options
6045
- * @returns {Promise<Array>} - Query results
6111
+ /**
6112
+ * Compatibility method that redirects to find
6113
+ * @param {Object} criteria - Query criteria
6114
+ * @param {Object} options - Query options
6115
+ * @returns {Promise<Array>} - Query results
6046
6116
  */
6047
6117
  async query(criteria, options = {}) {
6048
6118
  return this.find(criteria, options);
6049
6119
  }
6050
6120
 
6051
- /**
6052
- * Find using streaming strategy with pre-filtering optimization
6053
- * @param {Object} criteria - Query criteria
6054
- * @param {Object} options - Query options
6055
- * @returns {Promise<Array>} - Query results
6121
+ /**
6122
+ * Find using streaming strategy with pre-filtering optimization
6123
+ * @param {Object} criteria - Query criteria
6124
+ * @param {Object} options - Query options
6125
+ * @returns {Promise<Array>} - Query results
6056
6126
  */
6057
6127
  async findWithStreaming(criteria, options = {}) {
6058
6128
  const streamingOptions = {
@@ -6071,6 +6141,21 @@ class QueryManager {
6071
6141
  // OPTIMIZATION: Try to use indices for pre-filtering when possible
6072
6142
  const indexableFields = this._getIndexableFields(criteria);
6073
6143
  if (indexableFields.length > 0) {
6144
+ try {
6145
+ await this.database._ensureLazyIndexLoaded();
6146
+ } catch (error) {
6147
+ if (this.opts.debugMode) {
6148
+ console.log('⚠️ _ensureLazyIndexLoaded failed, falling back to full streaming', error.message || error);
6149
+ }
6150
+ return this._streamAllRecords(criteria, streamingOptions);
6151
+ }
6152
+ const usableIndexableFields = indexableFields.filter(field => this.indexManager.hasUsableIndexData(field));
6153
+ if (usableIndexableFields.length !== indexableFields.length) {
6154
+ if (this.opts.debugMode) {
6155
+ console.log('🌊 Falling back to full streaming because some indexed fields lack usable index data');
6156
+ }
6157
+ return this._streamAllRecords(criteria, streamingOptions);
6158
+ }
6074
6159
  if (this.opts.debugMode) {
6075
6160
  console.log(`🌊 Using pre-filtered streaming with ${indexableFields.length} indexable fields`);
6076
6161
  }
@@ -6090,10 +6175,10 @@ class QueryManager {
6090
6175
  return this._streamAllRecords(criteria, streamingOptions);
6091
6176
  }
6092
6177
 
6093
- /**
6094
- * Get indexable fields from criteria
6095
- * @param {Object} criteria - Query criteria
6096
- * @returns {Array} - Array of indexable field names
6178
+ /**
6179
+ * Get indexable fields from criteria
6180
+ * @param {Object} criteria - Query criteria
6181
+ * @returns {Array} - Array of indexable field names
6097
6182
  */
6098
6183
  _getIndexableFields(criteria) {
6099
6184
  const indexableFields = [];
@@ -6123,10 +6208,10 @@ class QueryManager {
6123
6208
  return [...new Set(indexableFields)]; // Remove duplicates
6124
6209
  }
6125
6210
 
6126
- /**
6127
- * Extract indexable criteria for pre-filtering
6128
- * @param {Object} criteria - Full query criteria
6129
- * @returns {Object} - Criteria with only indexable fields
6211
+ /**
6212
+ * Extract indexable criteria for pre-filtering
6213
+ * @param {Object} criteria - Full query criteria
6214
+ * @returns {Object} - Criteria with only indexable fields
6130
6215
  */
6131
6216
  _extractIndexableCriteria(criteria) {
6132
6217
  if (!criteria || typeof criteria !== 'object') {
@@ -6171,9 +6256,9 @@ class QueryManager {
6171
6256
  return indexableCriteria;
6172
6257
  }
6173
6258
 
6174
- /**
6175
- * Determine whether the database currently has any records (persisted or pending)
6176
- * @returns {boolean}
6259
+ /**
6260
+ * Determine whether the database currently has any records (persisted or pending)
6261
+ * @returns {boolean}
6177
6262
  */
6178
6263
  _hasAnyRecords() {
6179
6264
  if (!this.database) {
@@ -6191,11 +6276,11 @@ class QueryManager {
6191
6276
  return false;
6192
6277
  }
6193
6278
 
6194
- /**
6195
- * Extract all indexed fields referenced in the criteria
6196
- * @param {Object} criteria
6197
- * @param {Set<string>} accumulator
6198
- * @returns {Array<string>}
6279
+ /**
6280
+ * Extract all indexed fields referenced in the criteria
6281
+ * @param {Object} criteria
6282
+ * @param {Set<string>} accumulator
6283
+ * @returns {Array<string>}
6199
6284
  */
6200
6285
  _extractIndexedFields(criteria, accumulator = new Set()) {
6201
6286
  if (!criteria) {
@@ -6225,10 +6310,10 @@ class QueryManager {
6225
6310
  return Array.from(accumulator);
6226
6311
  }
6227
6312
 
6228
- /**
6229
- * Identify indexed fields present in criteria whose index data is missing
6230
- * @param {Object} criteria
6231
- * @returns {Array<string>}
6313
+ /**
6314
+ * Identify indexed fields present in criteria whose index data is missing
6315
+ * @param {Object} criteria
6316
+ * @returns {Array<string>}
6232
6317
  */
6233
6318
  _getIndexedFieldsWithMissingData(criteria) {
6234
6319
  if (!this.indexManager || !criteria) {
@@ -6247,12 +6332,12 @@ class QueryManager {
6247
6332
  return missing;
6248
6333
  }
6249
6334
 
6250
- /**
6251
- * OPTIMIZATION 4: Stream pre-filtered records using line numbers from indices with partial index optimization
6252
- * @param {Set} preFilteredLines - Line numbers from index query
6253
- * @param {Object} criteria - Full query criteria
6254
- * @param {Object} options - Query options
6255
- * @returns {Promise<Array>} - Query results
6335
+ /**
6336
+ * OPTIMIZATION 4: Stream pre-filtered records using line numbers from indices with partial index optimization
6337
+ * @param {Set} preFilteredLines - Line numbers from index query
6338
+ * @param {Object} criteria - Full query criteria
6339
+ * @param {Object} options - Query options
6340
+ * @returns {Promise<Array>} - Query results
6256
6341
  */
6257
6342
  async _streamPreFilteredRecords(preFilteredLines, criteria, options = {}) {
6258
6343
  if (preFilteredLines.size === 0) {
@@ -6270,71 +6355,95 @@ class QueryManager {
6270
6355
  for (let i = 0; i < lineNumbers.length; i += batchSize) {
6271
6356
  batches.push(lineNumbers.slice(i, i + batchSize));
6272
6357
  }
6273
- for (const batch of batches) {
6274
- // OPTIMIZATION: Use ranges instead of reading entire file
6275
- const ranges = this.database.getRanges(batch);
6276
- const groupedRanges = await this.fileHandler.groupedRanges(ranges);
6277
- const fd = await fs.promises.open(this.fileHandler.file, 'r');
6278
- try {
6279
- for (const groupedRange of groupedRanges) {
6280
- var _iteratorAbruptCompletion = false;
6281
- var _didIteratorError = false;
6282
- var _iteratorError;
6283
- try {
6284
- for (var _iterator = _asyncIterator(this.fileHandler.readGroupedRange(groupedRange, fd)), _step; _iteratorAbruptCompletion = !(_step = await _iterator.next()).done; _iteratorAbruptCompletion = false) {
6285
- const row = _step.value;
6286
- {
6287
- if (row.line && row.line.trim()) {
6288
- try {
6289
- // CRITICAL FIX: Use serializer.deserialize instead of JSON.parse to handle array format
6290
- const record = this.database.serializer.deserialize(row.line);
6291
-
6292
- // OPTIMIZATION 4: Use optimized criteria matching for pre-filtered records
6293
- if (this._matchesCriteriaOptimized(record, criteria, options)) {
6294
- // SPACE OPTIMIZATION: Restore term IDs to terms for user (unless disabled)
6295
- const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
6296
- results.push(recordWithTerms);
6297
-
6298
- // Check limit
6299
- if (options.limit && results.length >= options.limit) {
6300
- return this._applyOrdering(results, options);
6358
+ const persistedCount = Array.isArray(this.database.offsets) ? this.database.offsets.length : 0;
6359
+ const fileLineNumbers = [];
6360
+ const writeBufferLineNumbers = [];
6361
+ for (const lineNumber of lineNumbers) {
6362
+ if (lineNumber >= persistedCount) {
6363
+ writeBufferLineNumbers.push(lineNumber);
6364
+ } else {
6365
+ fileLineNumbers.push(lineNumber);
6366
+ }
6367
+ }
6368
+ if (fileLineNumbers.length > 0) {
6369
+ const fileBatches = [];
6370
+ for (let i = 0; i < fileLineNumbers.length; i += batchSize) {
6371
+ fileBatches.push(fileLineNumbers.slice(i, i + batchSize));
6372
+ }
6373
+ for (const batch of fileBatches) {
6374
+ // OPTIMIZATION: Use ranges instead of reading entire file
6375
+ const ranges = this.database.getRanges(batch);
6376
+ const groupedRanges = await this.fileHandler.groupedRanges(ranges);
6377
+ const fd = await fs.promises.open(this.fileHandler.file, 'r');
6378
+ try {
6379
+ for (const groupedRange of groupedRanges) {
6380
+ var _iteratorAbruptCompletion = false;
6381
+ var _didIteratorError = false;
6382
+ var _iteratorError;
6383
+ try {
6384
+ for (var _iterator = _asyncIterator(this.fileHandler.readGroupedRange(groupedRange, fd)), _step; _iteratorAbruptCompletion = !(_step = await _iterator.next()).done; _iteratorAbruptCompletion = false) {
6385
+ const row = _step.value;
6386
+ {
6387
+ if (row.line && row.line.trim()) {
6388
+ try {
6389
+ const record = this.database.serializer.deserialize(row.line);
6390
+ if (this._matchesCriteriaOptimized(record, criteria, options)) {
6391
+ const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
6392
+ results.push(recordWithTerms);
6393
+ if (options.limit && results.length >= options.limit) {
6394
+ return this._applyOrdering(results, options);
6395
+ }
6301
6396
  }
6397
+ } catch (error) {
6398
+ continue;
6302
6399
  }
6303
- } catch (error) {
6304
- // Skip invalid lines
6305
- continue;
6306
6400
  }
6307
6401
  }
6308
6402
  }
6309
- }
6310
- } catch (err) {
6311
- _didIteratorError = true;
6312
- _iteratorError = err;
6313
- } finally {
6314
- try {
6315
- if (_iteratorAbruptCompletion && _iterator.return != null) {
6316
- await _iterator.return();
6317
- }
6403
+ } catch (err) {
6404
+ _didIteratorError = true;
6405
+ _iteratorError = err;
6318
6406
  } finally {
6319
- if (_didIteratorError) {
6320
- throw _iteratorError;
6407
+ try {
6408
+ if (_iteratorAbruptCompletion && _iterator.return != null) {
6409
+ await _iterator.return();
6410
+ }
6411
+ } finally {
6412
+ if (_didIteratorError) {
6413
+ throw _iteratorError;
6414
+ }
6321
6415
  }
6322
6416
  }
6323
6417
  }
6418
+ } finally {
6419
+ await fd.close();
6420
+ }
6421
+ }
6422
+ }
6423
+ if (writeBufferLineNumbers.length > 0) {
6424
+ writeBufferLineNumbers.sort((a, b) => a - b);
6425
+ for (const lineNumber of writeBufferLineNumbers) {
6426
+ const writeBufferIndex = lineNumber - persistedCount;
6427
+ const record = this.database.writeBuffer[writeBufferIndex];
6428
+ if (!record) continue;
6429
+ if (this._matchesCriteriaOptimized(record, criteria, options)) {
6430
+ const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
6431
+ results.push(recordWithTerms);
6432
+ if (options.limit && results.length >= options.limit) {
6433
+ return this._applyOrdering(results, options);
6434
+ }
6324
6435
  }
6325
- } finally {
6326
- await fd.close();
6327
6436
  }
6328
6437
  }
6329
6438
  return this._applyOrdering(results, options);
6330
6439
  }
6331
6440
 
6332
- /**
6333
- * OPTIMIZATION 4: Optimized criteria matching for pre-filtered records
6334
- * @param {Object} record - Record to check
6335
- * @param {Object} criteria - Filter criteria
6336
- * @param {Object} options - Query options
6337
- * @returns {boolean} - True if matches
6441
+ /**
6442
+ * OPTIMIZATION 4: Optimized criteria matching for pre-filtered records
6443
+ * @param {Object} record - Record to check
6444
+ * @param {Object} criteria - Filter criteria
6445
+ * @param {Object} options - Query options
6446
+ * @returns {boolean} - True if matches
6338
6447
  */
6339
6448
  _matchesCriteriaOptimized(record, criteria, options = {}) {
6340
6449
  if (!criteria || Object.keys(criteria).length === 0) {
@@ -6393,11 +6502,11 @@ class QueryManager {
6393
6502
  return true;
6394
6503
  }
6395
6504
 
6396
- /**
6397
- * OPTIMIZATION 4: Apply ordering to results
6398
- * @param {Array} results - Results to order
6399
- * @param {Object} options - Query options
6400
- * @returns {Array} - Ordered results
6505
+ /**
6506
+ * OPTIMIZATION 4: Apply ordering to results
6507
+ * @param {Array} results - Results to order
6508
+ * @param {Object} options - Query options
6509
+ * @returns {Array} - Ordered results
6401
6510
  */
6402
6511
  _applyOrdering(results, options) {
6403
6512
  if (options.orderBy) {
@@ -6411,11 +6520,11 @@ class QueryManager {
6411
6520
  return results;
6412
6521
  }
6413
6522
 
6414
- /**
6415
- * Stream all records (fallback method)
6416
- * @param {Object} criteria - Query criteria
6417
- * @param {Object} options - Query options
6418
- * @returns {Promise<Array>} - Query results
6523
+ /**
6524
+ * Stream all records (fallback method)
6525
+ * @param {Object} criteria - Query criteria
6526
+ * @param {Object} options - Query options
6527
+ * @returns {Promise<Array>} - Query results
6419
6528
  */
6420
6529
  async _streamAllRecords(criteria, options = {}) {
6421
6530
  const memoryLimit = options.limit || undefined;
@@ -6442,16 +6551,27 @@ class QueryManager {
6442
6551
  return resultsWithTerms;
6443
6552
  }
6444
6553
 
6445
- /**
6446
- * Find using indexed search strategy with real streaming
6447
- * @param {Object} criteria - Query criteria
6448
- * @param {Object} options - Query options
6449
- * @returns {Promise<Array>} - Query results
6554
+ /**
6555
+ * Find using indexed search strategy with real streaming
6556
+ * @param {Object} criteria - Query criteria
6557
+ * @param {Object} options - Query options
6558
+ * @returns {Promise<Array>} - Query results
6450
6559
  */
6451
6560
  async findWithIndexed(criteria, options = {}) {
6452
6561
  if (this.opts.debugMode) {
6453
6562
  console.log('📊 Using indexed strategy with real streaming');
6454
6563
  }
6564
+ try {
6565
+ await this.database._ensureLazyIndexLoaded();
6566
+ } catch (error) {
6567
+ if (this.opts.debugMode) {
6568
+ console.log('⚠️ _ensureLazyIndexLoaded failed, falling back to full streaming', error.message || error);
6569
+ }
6570
+ return this.findWithStreaming(criteria, {
6571
+ ...options,
6572
+ forceFullScan: true
6573
+ });
6574
+ }
6455
6575
  let results = [];
6456
6576
  const limit = options.limit; // No default limit - return all results unless explicitly limited
6457
6577
 
@@ -6598,12 +6718,12 @@ class QueryManager {
6598
6718
  return results;
6599
6719
  }
6600
6720
 
6601
- /**
6602
- * Check if a record matches criteria
6603
- * @param {Object} record - Record to check
6604
- * @param {Object} criteria - Filter criteria
6605
- * @param {Object} options - Query options (for caseInsensitive, etc.)
6606
- * @returns {boolean} - True if matches
6721
+ /**
6722
+ * Check if a record matches criteria
6723
+ * @param {Object} record - Record to check
6724
+ * @param {Object} criteria - Filter criteria
6725
+ * @param {Object} options - Query options (for caseInsensitive, etc.)
6726
+ * @returns {boolean} - True if matches
6607
6727
  */
6608
6728
  matchesCriteria(record, criteria, options = {}) {
6609
6729
  if (!criteria || Object.keys(criteria).length === 0) {
@@ -6672,13 +6792,13 @@ class QueryManager {
6672
6792
  return true;
6673
6793
  }
6674
6794
 
6675
- /**
6676
- * Check if a field matches a condition
6677
- * @param {Object} record - Record to check
6678
- * @param {string} field - Field name
6679
- * @param {*} condition - Condition to match
6680
- * @param {Object} options - Query options
6681
- * @returns {boolean} - True if matches
6795
+ /**
6796
+ * Check if a field matches a condition
6797
+ * @param {Object} record - Record to check
6798
+ * @param {string} field - Field name
6799
+ * @param {*} condition - Condition to match
6800
+ * @param {Object} options - Query options
6801
+ * @returns {boolean} - True if matches
6682
6802
  */
6683
6803
  matchesFieldCondition(record, field, condition, options = {}) {
6684
6804
  const value = record[field];
@@ -6753,13 +6873,13 @@ class QueryManager {
6753
6873
  return value === condition;
6754
6874
  }
6755
6875
 
6756
- /**
6757
- * Check if a value matches an operator condition
6758
- * @param {*} value - Value to check
6759
- * @param {string} operator - Operator
6760
- * @param {*} operatorValue - Operator value
6761
- * @param {Object} options - Query options
6762
- * @returns {boolean} - True if matches
6876
+ /**
6877
+ * Check if a value matches an operator condition
6878
+ * @param {*} value - Value to check
6879
+ * @param {string} operator - Operator
6880
+ * @param {*} operatorValue - Operator value
6881
+ * @param {Object} options - Query options
6882
+ * @returns {boolean} - True if matches
6763
6883
  */
6764
6884
  matchesOperator(value, operator, operatorValue, options = {}) {
6765
6885
  switch (operator) {
@@ -6825,10 +6945,10 @@ class QueryManager {
6825
6945
  }
6826
6946
  }
6827
6947
 
6828
- /**
6829
- * Preprocess query to handle array field syntax automatically
6830
- * @param {Object} criteria - Query criteria
6831
- * @returns {Object} - Processed criteria
6948
+ /**
6949
+ * Preprocess query to handle array field syntax automatically
6950
+ * @param {Object} criteria - Query criteria
6951
+ * @returns {Object} - Processed criteria
6832
6952
  */
6833
6953
  preprocessQuery(criteria) {
6834
6954
  if (!criteria || typeof criteria !== 'object') {
@@ -6903,11 +7023,11 @@ class QueryManager {
6903
7023
  return processed;
6904
7024
  }
6905
7025
 
6906
- /**
6907
- * Determine which query strategy to use
6908
- * @param {Object} criteria - Query criteria
6909
- * @param {Object} options - Query options
6910
- * @returns {string} - 'streaming' or 'indexed'
7026
+ /**
7027
+ * Determine which query strategy to use
7028
+ * @param {Object} criteria - Query criteria
7029
+ * @param {Object} options - Query options
7030
+ * @returns {string} - 'streaming' or 'indexed'
6911
7031
  */
6912
7032
  shouldUseStreaming(criteria, options = {}) {
6913
7033
  const {
@@ -7062,7 +7182,7 @@ class QueryManager {
7062
7182
 
7063
7183
  // OPTIMIZATION 2: Hybrid strategy - use pre-filtered streaming when index is empty
7064
7184
  const indexData = this.indexManager.index.data || {};
7065
- const hasIndexData = Object.keys(indexData).length > 0;
7185
+ const hasIndexData = Object.keys(indexData).some(field => this.indexManager.hasUsableIndexData(field));
7066
7186
  if (!hasIndexData) {
7067
7187
  // Check if we can use pre-filtered streaming with term mapping
7068
7188
  if (this.opts.termMapping && this._canUsePreFilteredStreaming(criteria)) {
@@ -7092,11 +7212,11 @@ class QueryManager {
7092
7212
  return 'indexed';
7093
7213
  }
7094
7214
 
7095
- /**
7096
- * Estimate number of results for a query
7097
- * @param {Object} criteria - Query criteria
7098
- * @param {number} totalRecords - Total records in database
7099
- * @returns {number} - Estimated results
7215
+ /**
7216
+ * Estimate number of results for a query
7217
+ * @param {Object} criteria - Query criteria
7218
+ * @param {number} totalRecords - Total records in database
7219
+ * @returns {number} - Estimated results
7100
7220
  */
7101
7221
  estimateQueryResults(criteria, totalRecords) {
7102
7222
  // If database is empty, return 0
@@ -7152,11 +7272,11 @@ class QueryManager {
7152
7272
  return minResults === Infinity ? 0 : minResults;
7153
7273
  }
7154
7274
 
7155
- /**
7156
- * Estimate results for $all operator
7157
- * @param {Object} fieldIndex - Field index
7158
- * @param {Array} values - Values to match
7159
- * @returns {number} - Estimated results
7275
+ /**
7276
+ * Estimate results for $all operator
7277
+ * @param {Object} fieldIndex - Field index
7278
+ * @param {Array} values - Values to match
7279
+ * @returns {number} - Estimated results
7160
7280
  */
7161
7281
  estimateAllOperator(fieldIndex, values) {
7162
7282
  if (!Array.isArray(values) || values.length === 0) {
@@ -7171,13 +7291,13 @@ class QueryManager {
7171
7291
  return minCount === Infinity ? 0 : minCount;
7172
7292
  }
7173
7293
 
7174
- /**
7175
- * Estimate results for operators
7176
- * @param {Object} fieldIndex - Field index
7177
- * @param {string} operator - Operator
7178
- * @param {*} value - Value
7179
- * @param {number} totalRecords - Total records
7180
- * @returns {number} - Estimated results
7294
+ /**
7295
+ * Estimate results for operators
7296
+ * @param {Object} fieldIndex - Field index
7297
+ * @param {string} operator - Operator
7298
+ * @param {*} value - Value
7299
+ * @param {number} totalRecords - Total records
7300
+ * @returns {number} - Estimated results
7181
7301
  */
7182
7302
  estimateOperatorResults(fieldIndex, operator, value, totalRecords) {
7183
7303
  // This is a simplified estimation - in practice, you might want more sophisticated logic
@@ -7208,10 +7328,10 @@ class QueryManager {
7208
7328
  return 0;
7209
7329
  }
7210
7330
 
7211
- /**
7212
- * Validate strict query mode
7213
- * @param {Object} criteria - Query criteria
7214
- * @param {Object} options - Query options
7331
+ /**
7332
+ * Validate strict query mode
7333
+ * @param {Object} criteria - Query criteria
7334
+ * @param {Object} options - Query options
7215
7335
  */
7216
7336
  validateStrictQuery(criteria, options = {}) {
7217
7337
  // Allow bypassing strict mode validation with allowNonIndexed option
@@ -7278,10 +7398,10 @@ class QueryManager {
7278
7398
  }
7279
7399
  }
7280
7400
 
7281
- /**
7282
- * Update average time for performance tracking
7283
- * @param {string} type - Type of operation ('streaming' or 'indexed')
7284
- * @param {number} time - Time taken
7401
+ /**
7402
+ * Update average time for performance tracking
7403
+ * @param {string} type - Type of operation ('streaming' or 'indexed')
7404
+ * @param {number} time - Time taken
7285
7405
  */
7286
7406
  updateAverageTime(type, time) {
7287
7407
  if (!this.usageStats[`${type}AverageTime`]) {
@@ -7294,10 +7414,10 @@ class QueryManager {
7294
7414
  this.usageStats[`${type}AverageTime`] = (currentAverage * (count - 1) + time) / count;
7295
7415
  }
7296
7416
 
7297
- /**
7298
- * OPTIMIZATION 2: Check if we can use pre-filtered streaming with term mapping
7299
- * @param {Object} criteria - Query criteria
7300
- * @returns {boolean} - True if pre-filtered streaming can be used
7417
+ /**
7418
+ * OPTIMIZATION 2: Check if we can use pre-filtered streaming with term mapping
7419
+ * @param {Object} criteria - Query criteria
7420
+ * @returns {boolean} - True if pre-filtered streaming can be used
7301
7421
  */
7302
7422
  _canUsePreFilteredStreaming(criteria) {
7303
7423
  if (!criteria || typeof criteria !== 'object') {
@@ -8213,14 +8333,14 @@ class StreamingProcessor extends events.EventEmitter {
8213
8333
  }
8214
8334
  }
8215
8335
 
8216
- /**
8217
- * TermManager - Manages term-to-ID mapping for efficient storage
8218
- *
8219
- * Responsibilities:
8220
- * - Map terms to numeric IDs for space efficiency
8221
- * - Track term usage counts for cleanup
8222
- * - Load/save terms from/to index file
8223
- * - Clean up orphaned terms
8336
+ /**
8337
+ * TermManager - Manages term-to-ID mapping for efficient storage
8338
+ *
8339
+ * Responsibilities:
8340
+ * - Map terms to numeric IDs for space efficiency
8341
+ * - Track term usage counts for cleanup
8342
+ * - Load/save terms from/to index file
8343
+ * - Clean up orphaned terms
8224
8344
  */
8225
8345
  class TermManager {
8226
8346
  constructor() {
@@ -8230,10 +8350,10 @@ class TermManager {
8230
8350
  this.nextId = 1;
8231
8351
  }
8232
8352
 
8233
- /**
8234
- * Get ID for a term (create if doesn't exist)
8235
- * @param {string} term - Term to get ID for
8236
- * @returns {number} - Numeric ID for the term
8353
+ /**
8354
+ * Get ID for a term (create if doesn't exist)
8355
+ * @param {string} term - Term to get ID for
8356
+ * @returns {number} - Numeric ID for the term
8237
8357
  */
8238
8358
  getTermId(term) {
8239
8359
  if (this.termToId.has(term)) {
@@ -8248,12 +8368,12 @@ class TermManager {
8248
8368
  return id;
8249
8369
  }
8250
8370
 
8251
- /**
8252
- * Get term ID without incrementing count (for IndexManager use)
8253
- * @param {string} term - Term to get ID for
8254
- * @returns {number|undefined} - Numeric ID for the term, or undefined if not found
8255
- * CRITICAL: Does NOT create new IDs - only returns existing ones
8256
- * This prevents creating invalid term IDs during queries when terms haven't been loaded yet
8371
+ /**
8372
+ * Get term ID without incrementing count (for IndexManager use)
8373
+ * @param {string} term - Term to get ID for
8374
+ * @returns {number|undefined} - Numeric ID for the term, or undefined if not found
8375
+ * CRITICAL: Does NOT create new IDs - only returns existing ones
8376
+ * This prevents creating invalid term IDs during queries when terms haven't been loaded yet
8257
8377
  */
8258
8378
  getTermIdWithoutIncrement(term) {
8259
8379
  if (this.termToId.has(term)) {
@@ -8266,19 +8386,19 @@ class TermManager {
8266
8386
  return undefined;
8267
8387
  }
8268
8388
 
8269
- /**
8270
- * Get term by ID
8271
- * @param {number} id - Numeric ID
8272
- * @returns {string|null} - Term or null if not found
8389
+ /**
8390
+ * Get term by ID
8391
+ * @param {number} id - Numeric ID
8392
+ * @returns {string|null} - Term or null if not found
8273
8393
  */
8274
8394
  getTerm(id) {
8275
8395
  return this.idToTerm.get(id) || null;
8276
8396
  }
8277
8397
 
8278
- /**
8279
- * Bulk get term IDs for multiple terms (optimized for performance)
8280
- * @param {string[]} terms - Array of terms to get IDs for
8281
- * @returns {number[]} - Array of term IDs in the same order
8398
+ /**
8399
+ * Bulk get term IDs for multiple terms (optimized for performance)
8400
+ * @param {string[]} terms - Array of terms to get IDs for
8401
+ * @returns {number[]} - Array of term IDs in the same order
8282
8402
  */
8283
8403
  bulkGetTermIds(terms) {
8284
8404
  if (!Array.isArray(terms) || terms.length === 0) {
@@ -8304,9 +8424,9 @@ class TermManager {
8304
8424
  return termIds;
8305
8425
  }
8306
8426
 
8307
- /**
8308
- * Load terms from file data
8309
- * @param {Object} termsData - Terms data from file
8427
+ /**
8428
+ * Load terms from file data
8429
+ * @param {Object} termsData - Terms data from file
8310
8430
  */
8311
8431
  loadTerms(termsData) {
8312
8432
  if (!termsData || typeof termsData !== 'object') {
@@ -8324,9 +8444,9 @@ class TermManager {
8324
8444
  }
8325
8445
  }
8326
8446
 
8327
- /**
8328
- * Save terms to file format
8329
- * @returns {Object} - Terms data for file
8447
+ /**
8448
+ * Save terms to file format
8449
+ * @returns {Object} - Terms data for file
8330
8450
  */
8331
8451
  saveTerms() {
8332
8452
  const termsData = {};
@@ -8336,11 +8456,11 @@ class TermManager {
8336
8456
  return termsData;
8337
8457
  }
8338
8458
 
8339
- /**
8340
- * Clean up orphaned terms (terms with count 0)
8341
- * @param {boolean} forceCleanup - Force cleanup even if conditions not met
8342
- * @param {Object} options - Cleanup options
8343
- * @returns {number} - Number of orphaned terms removed
8459
+ /**
8460
+ * Clean up orphaned terms (terms with count 0)
8461
+ * @param {boolean} forceCleanup - Force cleanup even if conditions not met
8462
+ * @param {Object} options - Cleanup options
8463
+ * @returns {number} - Number of orphaned terms removed
8344
8464
  */
8345
8465
  cleanupOrphanedTerms(forceCleanup = false, options = {}) {
8346
8466
  const {
@@ -8398,9 +8518,9 @@ class TermManager {
8398
8518
  return orphanedIds.length;
8399
8519
  }
8400
8520
 
8401
- /**
8402
- * Check if system is safe for cleanup operations
8403
- * @returns {boolean} - True if system is safe for cleanup
8521
+ /**
8522
+ * Check if system is safe for cleanup operations
8523
+ * @returns {boolean} - True if system is safe for cleanup
8404
8524
  */
8405
8525
  isSystemSafe() {
8406
8526
  // This method should be overridden by the database instance
@@ -8408,10 +8528,10 @@ class TermManager {
8408
8528
  return true; // Default to safe for backward compatibility
8409
8529
  }
8410
8530
 
8411
- /**
8412
- * Perform intelligent automatic cleanup
8413
- * @param {Object} options - Cleanup options
8414
- * @returns {number} - Number of orphaned terms removed
8531
+ /**
8532
+ * Perform intelligent automatic cleanup
8533
+ * @param {Object} options - Cleanup options
8534
+ * @returns {number} - Number of orphaned terms removed
8415
8535
  */
8416
8536
  performIntelligentCleanup(options = {}) {
8417
8537
  return this.cleanupOrphanedTerms(false, {
@@ -8425,27 +8545,27 @@ class TermManager {
8425
8545
  });
8426
8546
  }
8427
8547
 
8428
- /**
8429
- * Decrement term count (when term is removed from index)
8430
- * @param {number} termId - Term ID to decrement
8548
+ /**
8549
+ * Decrement term count (when term is removed from index)
8550
+ * @param {number} termId - Term ID to decrement
8431
8551
  */
8432
8552
  decrementTermCount(termId) {
8433
8553
  const count = this.termCounts.get(termId) || 0;
8434
8554
  this.termCounts.set(termId, Math.max(0, count - 1));
8435
8555
  }
8436
8556
 
8437
- /**
8438
- * Increment term count (when term is added to index)
8439
- * @param {number} termId - Term ID to increment
8557
+ /**
8558
+ * Increment term count (when term is added to index)
8559
+ * @param {number} termId - Term ID to increment
8440
8560
  */
8441
8561
  incrementTermCount(termId) {
8442
8562
  const count = this.termCounts.get(termId) || 0;
8443
8563
  this.termCounts.set(termId, count + 1);
8444
8564
  }
8445
8565
 
8446
- /**
8447
- * Get statistics about terms
8448
- * @returns {Object} - Term statistics
8566
+ /**
8567
+ * Get statistics about terms
8568
+ * @returns {Object} - Term statistics
8449
8569
  */
8450
8570
  getStats() {
8451
8571
  return {
@@ -8455,34 +8575,34 @@ class TermManager {
8455
8575
  };
8456
8576
  }
8457
8577
 
8458
- /**
8459
- * Check if a term exists
8460
- * @param {string} term - Term to check
8461
- * @returns {boolean} - True if term exists
8578
+ /**
8579
+ * Check if a term exists
8580
+ * @param {string} term - Term to check
8581
+ * @returns {boolean} - True if term exists
8462
8582
  */
8463
8583
  hasTerm(term) {
8464
8584
  return this.termToId.has(term);
8465
8585
  }
8466
8586
 
8467
- /**
8468
- * Get all terms
8469
- * @returns {Array} - Array of all terms
8587
+ /**
8588
+ * Get all terms
8589
+ * @returns {Array} - Array of all terms
8470
8590
  */
8471
8591
  getAllTerms() {
8472
8592
  return Array.from(this.termToId.keys());
8473
8593
  }
8474
8594
 
8475
- /**
8476
- * Get all term IDs
8477
- * @returns {Array} - Array of all term IDs
8595
+ /**
8596
+ * Get all term IDs
8597
+ * @returns {Array} - Array of all term IDs
8478
8598
  */
8479
8599
  getAllTermIds() {
8480
8600
  return Array.from(this.idToTerm.keys());
8481
8601
  }
8482
8602
 
8483
- /**
8484
- * Get statistics about term mapping
8485
- * @returns {Object} - Statistics object
8603
+ /**
8604
+ * Get statistics about term mapping
8605
+ * @returns {Object} - Statistics object
8486
8606
  */
8487
8607
  getStatistics() {
8488
8608
  return {
@@ -8494,9 +8614,9 @@ class TermManager {
8494
8614
  }
8495
8615
  }
8496
8616
 
8497
- /**
8498
- * IterateEntry class for intuitive API with automatic change detection
8499
- * Uses native JavaScript setters for maximum performance
8617
+ /**
8618
+ * IterateEntry class for intuitive API with automatic change detection
8619
+ * Uses native JavaScript setters for maximum performance
8500
8620
  */
8501
8621
  class IterateEntry {
8502
8622
  constructor(entry, originalRecord) {
@@ -8549,8 +8669,8 @@ class IterateEntry {
8549
8669
  }
8550
8670
  }
8551
8671
 
8552
- /**
8553
- * InsertSession - Simple batch insertion without memory duplication
8672
+ /**
8673
+ * InsertSession - Simple batch insertion without memory duplication
8554
8674
  */
8555
8675
  class InsertSession {
8556
8676
  constructor(database, sessionOptions = {}) {
@@ -8731,8 +8851,8 @@ class InsertSession {
8731
8851
  return insertedCount;
8732
8852
  }
8733
8853
 
8734
- /**
8735
- * Wait for this session's operations to complete
8854
+ /**
8855
+ * Wait for this session's operations to complete
8736
8856
  */
8737
8857
  async waitForOperations(maxWaitTime = null) {
8738
8858
  const startTime = Date.now();
@@ -8750,15 +8870,15 @@ class InsertSession {
8750
8870
  return true;
8751
8871
  }
8752
8872
 
8753
- /**
8754
- * Check if this session has pending operations
8873
+ /**
8874
+ * Check if this session has pending operations
8755
8875
  */
8756
8876
  hasPendingOperations() {
8757
8877
  return this.pendingAutoFlushes.size > 0 || this.flushing || this.batches.length > 0 || this.currentBatch.length > 0;
8758
8878
  }
8759
8879
 
8760
- /**
8761
- * Destroy this session and unregister it
8880
+ /**
8881
+ * Destroy this session and unregister it
8762
8882
  */
8763
8883
  destroy() {
8764
8884
  // Unregister from database
@@ -8773,16 +8893,16 @@ class InsertSession {
8773
8893
  }
8774
8894
  }
8775
8895
 
8776
- /**
8777
- * JexiDB - A high-performance, in-memory database with persistence
8778
- *
8779
- * Features:
8780
- * - In-memory storage with optional persistence
8781
- * - Advanced indexing and querying
8782
- * - Transaction support
8783
- * - Manual save functionality
8784
- * - Recovery mechanisms
8785
- * - Performance optimizations
8896
+ /**
8897
+ * JexiDB - A high-performance, in-memory database with persistence
8898
+ *
8899
+ * Features:
8900
+ * - In-memory storage with optional persistence
8901
+ * - Advanced indexing and querying
8902
+ * - Transaction support
8903
+ * - Manual save functionality
8904
+ * - Recovery mechanisms
8905
+ * - Performance optimizations
8786
8906
  */
8787
8907
  class Database extends events.EventEmitter {
8788
8908
  constructor(file, opts = {}) {
@@ -8906,8 +9026,8 @@ class Database extends events.EventEmitter {
8906
9026
  // Note: Validation will be done after configuration conversion in initializeManagers()
8907
9027
  }
8908
9028
 
8909
- /**
8910
- * Validate field and index configuration
9029
+ /**
9030
+ * Validate field and index configuration
8911
9031
  */
8912
9032
  validateIndexConfiguration() {
8913
9033
  // Validate fields configuration
@@ -8937,8 +9057,8 @@ class Database extends events.EventEmitter {
8937
9057
  }
8938
9058
  }
8939
9059
 
8940
- /**
8941
- * Validate field types
9060
+ /**
9061
+ * Validate field types
8942
9062
  */
8943
9063
  validateFieldTypes(fields, configType) {
8944
9064
  const supportedTypes = ['string', 'number', 'boolean', 'array:string', 'array:number', 'array:boolean', 'array', 'object', 'auto'];
@@ -8970,8 +9090,8 @@ class Database extends events.EventEmitter {
8970
9090
  }
8971
9091
  }
8972
9092
 
8973
- /**
8974
- * Validate index fields array
9093
+ /**
9094
+ * Validate index fields array
8975
9095
  */
8976
9096
  validateIndexFields(indexFields) {
8977
9097
  if (!this.opts.fields) {
@@ -8989,8 +9109,8 @@ class Database extends events.EventEmitter {
8989
9109
  }
8990
9110
  }
8991
9111
 
8992
- /**
8993
- * Prepare index configuration for IndexManager
9112
+ /**
9113
+ * Prepare index configuration for IndexManager
8994
9114
  */
8995
9115
  prepareIndexConfiguration() {
8996
9116
  if (Array.isArray(this.opts.indexes)) {
@@ -9013,8 +9133,8 @@ class Database extends events.EventEmitter {
9013
9133
  // Legacy format (indexes as object) is already compatible
9014
9134
  }
9015
9135
 
9016
- /**
9017
- * Initialize all managers
9136
+ /**
9137
+ * Initialize all managers
9018
9138
  */
9019
9139
  initializeManagers() {
9020
9140
  // CRITICAL FIX: Prevent double initialization which corrupts term mappings
@@ -9120,9 +9240,9 @@ class Database extends events.EventEmitter {
9120
9240
  this.streamingProcessor = new StreamingProcessor(this.opts);
9121
9241
  }
9122
9242
 
9123
- /**
9124
- * Get term mapping fields from configuration or indexes (auto-detected)
9125
- * @returns {string[]} Array of field names that use term mapping
9243
+ /**
9244
+ * Get term mapping fields from configuration or indexes (auto-detected)
9245
+ * @returns {string[]} Array of field names that use term mapping
9126
9246
  */
9127
9247
  getTermMappingFields() {
9128
9248
  // If termMappingFields is explicitly configured, use it
@@ -9142,9 +9262,9 @@ class Database extends events.EventEmitter {
9142
9262
  return termMappingFields;
9143
9263
  }
9144
9264
 
9145
- /**
9146
- * CRITICAL FIX: Validate database state before critical operations
9147
- * Prevents crashes from undefined methods and invalid states
9265
+ /**
9266
+ * CRITICAL FIX: Validate database state before critical operations
9267
+ * Prevents crashes from undefined methods and invalid states
9148
9268
  */
9149
9269
  validateState() {
9150
9270
  if (this.destroyed) {
@@ -9171,9 +9291,9 @@ class Database extends events.EventEmitter {
9171
9291
  return true;
9172
9292
  }
9173
9293
 
9174
- /**
9175
- * CRITICAL FIX: Ensure file path is valid and accessible
9176
- * Prevents file path loss issues mentioned in crash report
9294
+ /**
9295
+ * CRITICAL FIX: Ensure file path is valid and accessible
9296
+ * Prevents file path loss issues mentioned in crash report
9177
9297
  */
9178
9298
  ensureFilePath() {
9179
9299
  if (!this.normalizedFile) {
@@ -9182,16 +9302,16 @@ class Database extends events.EventEmitter {
9182
9302
  return this.normalizedFile;
9183
9303
  }
9184
9304
 
9185
- /**
9186
- * Normalize file path to ensure it ends with .jdb
9305
+ /**
9306
+ * Normalize file path to ensure it ends with .jdb
9187
9307
  */
9188
9308
  normalizeFilePath(file) {
9189
9309
  if (!file) return null;
9190
9310
  return file.endsWith('.jdb') ? file : `${file}.jdb`;
9191
9311
  }
9192
9312
 
9193
- /**
9194
- * Initialize the database
9313
+ /**
9314
+ * Initialize the database
9195
9315
  */
9196
9316
  async initialize() {
9197
9317
  // Check if database is destroyed first (before checking initialized)
@@ -9281,10 +9401,10 @@ class Database extends events.EventEmitter {
9281
9401
  }
9282
9402
  }
9283
9403
 
9284
- /**
9285
- * Validate that the database is initialized before performing operations
9286
- * @param {string} operation - The operation being attempted
9287
- * @throws {Error} If database is not initialized
9404
+ /**
9405
+ * Validate that the database is initialized before performing operations
9406
+ * @param {string} operation - The operation being attempted
9407
+ * @throws {Error} If database is not initialized
9288
9408
  */
9289
9409
  _validateInitialization(operation) {
9290
9410
  if (this.destroyed) {
@@ -9299,8 +9419,8 @@ class Database extends events.EventEmitter {
9299
9419
  }
9300
9420
  }
9301
9421
 
9302
- /**
9303
- * Clear existing database files (.jdb and .idx.jdb)
9422
+ /**
9423
+ * Clear existing database files (.jdb and .idx.jdb)
9304
9424
  */
9305
9425
  async clearExistingFiles() {
9306
9426
  if (!this.normalizedFile) return;
@@ -9341,8 +9461,8 @@ class Database extends events.EventEmitter {
9341
9461
  }
9342
9462
  }
9343
9463
 
9344
- /**
9345
- * Load data from file
9464
+ /**
9465
+ * Load data from file
9346
9466
  */
9347
9467
  async load() {
9348
9468
  if (!this.normalizedFile) return;
@@ -9627,9 +9747,9 @@ class Database extends events.EventEmitter {
9627
9747
  }
9628
9748
  }
9629
9749
 
9630
- /**
9631
- * Save data to file
9632
- * @param {boolean} inQueue - Whether to execute within the operation queue (default: false)
9750
+ /**
9751
+ * Save data to file
9752
+ * @param {boolean} inQueue - Whether to execute within the operation queue (default: false)
9633
9753
  */
9634
9754
  async save(inQueue = false) {
9635
9755
  this._validateInitialization('save');
@@ -9706,8 +9826,8 @@ class Database extends events.EventEmitter {
9706
9826
  }
9707
9827
  }
9708
9828
 
9709
- /**
9710
- * Internal save implementation (without queue)
9829
+ /**
9830
+ * Internal save implementation (without queue)
9711
9831
  */
9712
9832
  async _doSave() {
9713
9833
  // CRITICAL FIX: Check if database is destroyed
@@ -9758,6 +9878,7 @@ class Database extends events.EventEmitter {
9758
9878
  const startLineNumber = this.pendingIndexUpdates[0].lineNumber;
9759
9879
 
9760
9880
  // Process index updates in batch
9881
+ await this._ensureLazyIndexLoaded();
9761
9882
  await this.indexManager.addBatch(records, startLineNumber);
9762
9883
 
9763
9884
  // Clear pending updates
@@ -10282,11 +10403,11 @@ class Database extends events.EventEmitter {
10282
10403
  }
10283
10404
  }
10284
10405
 
10285
- /**
10286
- * Process term mapping for a record
10287
- * @param {Object} record - Record to process
10288
- * @param {boolean} isUpdate - Whether this is an update operation
10289
- * @param {Object} oldRecord - Original record (for updates)
10406
+ /**
10407
+ * Process term mapping for a record
10408
+ * @param {Object} record - Record to process
10409
+ * @param {boolean} isUpdate - Whether this is an update operation
10410
+ * @param {Object} oldRecord - Original record (for updates)
10290
10411
  */
10291
10412
  processTermMapping(record, isUpdate = false, oldRecord = null) {
10292
10413
  const termMappingFields = this.getTermMappingFields();
@@ -10347,10 +10468,10 @@ class Database extends events.EventEmitter {
10347
10468
  }
10348
10469
  }
10349
10470
 
10350
- /**
10351
- * Convert terms to term IDs for serialization (SPACE OPTIMIZATION)
10352
- * @param {Object} record - Record to process
10353
- * @returns {Object} - Record with terms converted to term IDs
10471
+ /**
10472
+ * Convert terms to term IDs for serialization (SPACE OPTIMIZATION)
10473
+ * @param {Object} record - Record to process
10474
+ * @returns {Object} - Record with terms converted to term IDs
10354
10475
  */
10355
10476
  removeTermIdsForSerialization(record) {
10356
10477
  const termMappingFields = this.getTermMappingFields();
@@ -10376,10 +10497,10 @@ class Database extends events.EventEmitter {
10376
10497
  return optimizedRecord;
10377
10498
  }
10378
10499
 
10379
- /**
10380
- * Convert term IDs back to terms after deserialization (SPACE OPTIMIZATION)
10381
- * @param {Object} record - Record with term IDs
10382
- * @returns {Object} - Record with terms restored
10500
+ /**
10501
+ * Convert term IDs back to terms after deserialization (SPACE OPTIMIZATION)
10502
+ * @param {Object} record - Record with term IDs
10503
+ * @returns {Object} - Record with terms restored
10383
10504
  */
10384
10505
  restoreTermIdsAfterDeserialization(record) {
10385
10506
  const termMappingFields = this.getTermMappingFields();
@@ -10409,9 +10530,9 @@ class Database extends events.EventEmitter {
10409
10530
  return restoredRecord;
10410
10531
  }
10411
10532
 
10412
- /**
10413
- * Remove term mapping for a record
10414
- * @param {Object} record - Record to process
10533
+ /**
10534
+ * Remove term mapping for a record
10535
+ * @param {Object} record - Record to process
10415
10536
  */
10416
10537
  removeTermMapping(record) {
10417
10538
  const termMappingFields = this.getTermMappingFields();
@@ -10433,10 +10554,10 @@ class Database extends events.EventEmitter {
10433
10554
  }
10434
10555
  }
10435
10556
 
10436
- /**
10437
- * Process term mapping for multiple records in batch (OPTIMIZATION)
10438
- * @param {Array} records - Records to process
10439
- * @returns {Array} - Processed records with term mappings
10557
+ /**
10558
+ * Process term mapping for multiple records in batch (OPTIMIZATION)
10559
+ * @param {Array} records - Records to process
10560
+ * @returns {Array} - Processed records with term mappings
10440
10561
  */
10441
10562
  processTermMappingBatch(records) {
10442
10563
  const termMappingFields = this.getTermMappingFields();
@@ -10488,10 +10609,10 @@ class Database extends events.EventEmitter {
10488
10609
  return records;
10489
10610
  }
10490
10611
 
10491
- /**
10492
- * Calculate total size of serialized records (OPTIMIZATION)
10493
- * @param {Array} records - Records to calculate size for
10494
- * @returns {number} - Total size in bytes
10612
+ /**
10613
+ * Calculate total size of serialized records (OPTIMIZATION)
10614
+ * @param {Array} records - Records to calculate size for
10615
+ * @returns {number} - Total size in bytes
10495
10616
  */
10496
10617
  calculateBatchSize(records) {
10497
10618
  if (!records || !records.length) return 0;
@@ -10506,10 +10627,10 @@ class Database extends events.EventEmitter {
10506
10627
  return totalSize;
10507
10628
  }
10508
10629
 
10509
- /**
10510
- * Begin an insert session for batch operations
10511
- * @param {Object} sessionOptions - Options for the insert session
10512
- * @returns {InsertSession} - The insert session instance
10630
+ /**
10631
+ * Begin an insert session for batch operations
10632
+ * @param {Object} sessionOptions - Options for the insert session
10633
+ * @returns {InsertSession} - The insert session instance
10513
10634
  */
10514
10635
  beginInsertSession(sessionOptions = {}) {
10515
10636
  if (this.destroyed) {
@@ -10521,8 +10642,8 @@ class Database extends events.EventEmitter {
10521
10642
  return new InsertSession(this, sessionOptions);
10522
10643
  }
10523
10644
 
10524
- /**
10525
- * Insert a new record
10645
+ /**
10646
+ * Insert a new record
10526
10647
  */
10527
10648
  async insert(data) {
10528
10649
  this._validateInitialization('insert');
@@ -10615,8 +10736,8 @@ class Database extends events.EventEmitter {
10615
10736
  });
10616
10737
  }
10617
10738
 
10618
- /**
10619
- * Insert multiple records in batch (OPTIMIZATION)
10739
+ /**
10740
+ * Insert multiple records in batch (OPTIMIZATION)
10620
10741
  */
10621
10742
  async insertBatch(dataArray) {
10622
10743
  this._validateInitialization('insertBatch');
@@ -10641,8 +10762,8 @@ class Database extends events.EventEmitter {
10641
10762
  });
10642
10763
  }
10643
10764
 
10644
- /**
10645
- * Internal implementation for insertBatch to allow inline execution when already inside the queue
10765
+ /**
10766
+ * Internal implementation for insertBatch to allow inline execution when already inside the queue
10646
10767
  */
10647
10768
  async _insertBatchInternal(dataArray) {
10648
10769
  // CRITICAL FIX: Validate state before insert operation
@@ -10733,8 +10854,8 @@ class Database extends events.EventEmitter {
10733
10854
  return processedRecords;
10734
10855
  }
10735
10856
 
10736
- /**
10737
- * Find records matching criteria
10857
+ /**
10858
+ * Find records matching criteria
10738
10859
  */
10739
10860
  async find(criteria = {}, options = {}) {
10740
10861
  this._validateInitialization('find');
@@ -10842,11 +10963,11 @@ class Database extends events.EventEmitter {
10842
10963
  }
10843
10964
  }
10844
10965
 
10845
- /**
10846
- * Validate indexed query mode for strict mode
10847
- * @private
10848
- * @param {Object} criteria - Query criteria
10849
- * @param {Object} options - Query options
10966
+ /**
10967
+ * Validate indexed query mode for strict mode
10968
+ * @private
10969
+ * @param {Object} criteria - Query criteria
10970
+ * @param {Object} options - Query options
10850
10971
  */
10851
10972
  _validateIndexedQuery(criteria, options = {}) {
10852
10973
  // Allow bypassing strict mode validation with allowNonIndexed option
@@ -10872,10 +10993,10 @@ class Database extends events.EventEmitter {
10872
10993
  }
10873
10994
  }
10874
10995
 
10875
- /**
10876
- * Create a shallow copy of a record for change detection
10877
- * Optimized for known field types: number, string, null, or single-level arrays
10878
- * @private
10996
+ /**
10997
+ * Create a shallow copy of a record for change detection
10998
+ * Optimized for known field types: number, string, null, or single-level arrays
10999
+ * @private
10879
11000
  */
10880
11001
  _createShallowCopy(record) {
10881
11002
  const copy = {};
@@ -10900,10 +11021,10 @@ class Database extends events.EventEmitter {
10900
11021
  return copy;
10901
11022
  }
10902
11023
 
10903
- /**
10904
- * Create an intuitive API wrapper using a class with Proxy
10905
- * Combines the benefits of classes with the flexibility of Proxy
10906
- * @private
11024
+ /**
11025
+ * Create an intuitive API wrapper using a class with Proxy
11026
+ * Combines the benefits of classes with the flexibility of Proxy
11027
+ * @private
10907
11028
  */
10908
11029
  _createEntryProxy(entry, originalRecord) {
10909
11030
  // Create a class instance that wraps the entry
@@ -10938,9 +11059,9 @@ class Database extends events.EventEmitter {
10938
11059
  });
10939
11060
  }
10940
11061
 
10941
- /**
10942
- * Create a high-performance wrapper for maximum speed
10943
- * @private
11062
+ /**
11063
+ * Create a high-performance wrapper for maximum speed
11064
+ * @private
10944
11065
  */
10945
11066
  _createHighPerformanceWrapper(entry, originalRecord) {
10946
11067
  // Create a simple wrapper object for high performance
@@ -10958,10 +11079,10 @@ class Database extends events.EventEmitter {
10958
11079
  return wrapper;
10959
11080
  }
10960
11081
 
10961
- /**
10962
- * Check if a record has changed using optimized comparison
10963
- * Optimized for known field types: number, string, null, or single-level arrays
10964
- * @private
11082
+ /**
11083
+ * Check if a record has changed using optimized comparison
11084
+ * Optimized for known field types: number, string, null, or single-level arrays
11085
+ * @private
10965
11086
  */
10966
11087
  _hasRecordChanged(current, original) {
10967
11088
  // Quick reference check first
@@ -11025,9 +11146,9 @@ class Database extends events.EventEmitter {
11025
11146
  return false;
11026
11147
  }
11027
11148
 
11028
- /**
11029
- * Extract field names from query criteria
11030
- * @private
11149
+ /**
11150
+ * Extract field names from query criteria
11151
+ * @private
11031
11152
  */
11032
11153
  _extractQueryFields(criteria) {
11033
11154
  const fields = new Set();
@@ -11054,8 +11175,8 @@ class Database extends events.EventEmitter {
11054
11175
  return Array.from(fields);
11055
11176
  }
11056
11177
 
11057
- /**
11058
- * Update records matching criteria
11178
+ /**
11179
+ * Update records matching criteria
11059
11180
  */
11060
11181
  async update(criteria, updateData) {
11061
11182
  this._validateInitialization('update');
@@ -11116,6 +11237,7 @@ class Database extends events.EventEmitter {
11116
11237
 
11117
11238
  // CRITICAL FIX: Remove old terms from index before adding new ones
11118
11239
  if (this.indexManager) {
11240
+ await this._ensureLazyIndexLoaded();
11119
11241
  await this.indexManager.remove(record);
11120
11242
  if (this.opts.debugMode) {
11121
11243
  console.log(`🔄 UPDATE: Removed old terms from index for record ${record.id}`);
@@ -11151,6 +11273,7 @@ class Database extends events.EventEmitter {
11151
11273
  }
11152
11274
  }
11153
11275
  const indexUpdateStart = Date.now();
11276
+ await this._ensureLazyIndexLoaded();
11154
11277
  await this.indexManager.update(record, updated, lineNumber);
11155
11278
  if (this.opts.debugMode) {
11156
11279
  console.log(`🔄 UPDATE: Index update completed in ${Date.now() - indexUpdateStart}ms`);
@@ -11173,8 +11296,8 @@ class Database extends events.EventEmitter {
11173
11296
  });
11174
11297
  }
11175
11298
 
11176
- /**
11177
- * Delete records matching criteria
11299
+ /**
11300
+ * Delete records matching criteria
11178
11301
  */
11179
11302
  async delete(criteria) {
11180
11303
  this._validateInitialization('delete');
@@ -11217,6 +11340,7 @@ class Database extends events.EventEmitter {
11217
11340
  for (const record of records) {
11218
11341
  // Remove term mapping
11219
11342
  this.removeTermMapping(record);
11343
+ await this._ensureLazyIndexLoaded();
11220
11344
  await this.indexManager.remove(record);
11221
11345
 
11222
11346
  // Remove record from writeBuffer or mark as deleted
@@ -11248,16 +11372,16 @@ class Database extends events.EventEmitter {
11248
11372
  });
11249
11373
  }
11250
11374
 
11251
- /**
11252
- * Generate a unique ID
11375
+ /**
11376
+ * Generate a unique ID
11253
11377
  */
11254
11378
  generateId() {
11255
11379
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
11256
11380
  }
11257
11381
 
11258
- /**
11259
- * Apply schema enforcement to a record
11260
- * Converts object to array and back to enforce schema (remove extra fields, add undefined for missing fields)
11382
+ /**
11383
+ * Apply schema enforcement to a record
11384
+ * Converts object to array and back to enforce schema (remove extra fields, add undefined for missing fields)
11261
11385
  */
11262
11386
  applySchemaEnforcement(record) {
11263
11387
  // Only apply schema enforcement if fields configuration is explicitly provided
@@ -11281,8 +11405,8 @@ class Database extends events.EventEmitter {
11281
11405
  return enforcedRecord;
11282
11406
  }
11283
11407
 
11284
- /**
11285
- * Initialize schema for array-based serialization
11408
+ /**
11409
+ * Initialize schema for array-based serialization
11286
11410
  */
11287
11411
  initializeSchema() {
11288
11412
  if (!this.serializer || !this.serializer.schemaManager) {
@@ -11314,8 +11438,8 @@ class Database extends events.EventEmitter {
11314
11438
  }
11315
11439
  }
11316
11440
 
11317
- /**
11318
- * Get database length (number of records)
11441
+ /**
11442
+ * Get database length (number of records)
11319
11443
  */
11320
11444
  get length() {
11321
11445
  // Return total records: writeBuffer + saved records
@@ -11378,15 +11502,15 @@ class Database extends events.EventEmitter {
11378
11502
  return writeBufferRecords + savedRecords;
11379
11503
  }
11380
11504
 
11381
- /**
11382
- * Calculate current writeBuffer size in bytes (similar to published v1.1.0)
11505
+ /**
11506
+ * Calculate current writeBuffer size in bytes (similar to published v1.1.0)
11383
11507
  */
11384
11508
  currentWriteBufferSize() {
11385
11509
  return this.writeBufferTotalSize || 0;
11386
11510
  }
11387
11511
 
11388
- /**
11389
- * Get database statistics
11512
+ /**
11513
+ * Get database statistics
11390
11514
  */
11391
11515
  getStats() {
11392
11516
  const stats = {
@@ -11406,16 +11530,16 @@ class Database extends events.EventEmitter {
11406
11530
  return stats;
11407
11531
  }
11408
11532
 
11409
- /**
11410
- * Initialize database (alias for initialize for backward compatibility)
11533
+ /**
11534
+ * Initialize database (alias for initialize for backward compatibility)
11411
11535
  */
11412
11536
  async init() {
11413
11537
  return this.initialize();
11414
11538
  }
11415
11539
 
11416
- /**
11417
- * Schedule index rebuild when index data is missing or corrupted
11418
- * @private
11540
+ /**
11541
+ * Schedule index rebuild when index data is missing or corrupted
11542
+ * @private
11419
11543
  */
11420
11544
  _scheduleIndexRebuild() {
11421
11545
  // Mark that rebuild is needed
@@ -11425,9 +11549,9 @@ class Database extends events.EventEmitter {
11425
11549
  // This avoids blocking init() but ensures index is available when needed
11426
11550
  }
11427
11551
 
11428
- /**
11429
- * Rebuild indexes from data file if needed
11430
- * @private
11552
+ /**
11553
+ * Rebuild indexes from data file if needed
11554
+ * @private
11431
11555
  */
11432
11556
  async _rebuildIndexesIfNeeded() {
11433
11557
  if (this.opts.debugMode) {
@@ -11489,9 +11613,9 @@ class Database extends events.EventEmitter {
11489
11613
  }
11490
11614
  }
11491
11615
 
11492
- /**
11493
- * Rebuild indexes with retry logic to handle I/O hangs
11494
- * @private
11616
+ /**
11617
+ * Rebuild indexes with retry logic to handle I/O hangs
11618
+ * @private
11495
11619
  */
11496
11620
  async _rebuildIndexesWithRetry() {
11497
11621
  // If no timeout configured, use original implementation without retry
@@ -11671,10 +11795,10 @@ class Database extends events.EventEmitter {
11671
11795
  return count;
11672
11796
  }
11673
11797
 
11674
- /**
11675
- * Destroy database - DESTRUCTIVE MODE
11676
- * Assumes save() has already been called by user
11677
- * If anything is still active, it indicates a bug - log error and force cleanup
11798
+ /**
11799
+ * Destroy database - DESTRUCTIVE MODE
11800
+ * Assumes save() has already been called by user
11801
+ * If anything is still active, it indicates a bug - log error and force cleanup
11678
11802
  */
11679
11803
  async destroy() {
11680
11804
  if (this.destroyed) return;
@@ -11732,8 +11856,8 @@ class Database extends events.EventEmitter {
11732
11856
  }
11733
11857
  }
11734
11858
 
11735
- /**
11736
- * Internal destroy implementation
11859
+ /**
11860
+ * Internal destroy implementation
11737
11861
  */
11738
11862
  async _performDestroy() {
11739
11863
  try {
@@ -11833,8 +11957,8 @@ class Database extends events.EventEmitter {
11833
11957
  }
11834
11958
  }
11835
11959
 
11836
- /**
11837
- * Find one record
11960
+ /**
11961
+ * Find one record
11838
11962
  */
11839
11963
  async findOne(criteria, options = {}) {
11840
11964
  this._validateInitialization('findOne');
@@ -11845,8 +11969,8 @@ class Database extends events.EventEmitter {
11845
11969
  return results.length > 0 ? results[0] : null;
11846
11970
  }
11847
11971
 
11848
- /**
11849
- * Count records
11972
+ /**
11973
+ * Count records
11850
11974
  */
11851
11975
  async count(criteria = {}, options = {}) {
11852
11976
  this._validateInitialization('count');
@@ -11860,23 +11984,23 @@ class Database extends events.EventEmitter {
11860
11984
  return fileCount + writeBufferCount;
11861
11985
  }
11862
11986
 
11863
- /**
11864
- * Check if any records exist matching the given criteria (ultra-fast when using indexed fields)
11865
- *
11866
- * @param {string|object} fieldName - Indexed field name (legacy) OR query criteria object (new)
11867
- * @param {string|Array<string>} terms - Single term or array of terms (when using legacy syntax)
11868
- * @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
11869
- * @returns {Promise<boolean>} - True if at least one match exists
11870
- *
11871
- * @example
11872
- * // Legacy syntax - ultra-fast index-only check
11873
- * const exists = await db.exists('nameTerms', 'tv');
11874
- * const existsAll = await db.exists('nameTerms', ['tv', 'globo'], { $all: true });
11875
- *
11876
- * @example
11877
- * // New syntax - full query criteria support
11878
- * const exists = await db.exists({ mediaType: 'live', status: 'active' });
11879
- * const existsOr = await db.exists({ mediaType: ['live', 'vod'] });
11987
+ /**
11988
+ * Check if any records exist matching the given criteria (ultra-fast when using indexed fields)
11989
+ *
11990
+ * @param {string|object} fieldName - Indexed field name (legacy) OR query criteria object (new)
11991
+ * @param {string|Array<string>} terms - Single term or array of terms (when using legacy syntax)
11992
+ * @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
11993
+ * @returns {Promise<boolean>} - True if at least one match exists
11994
+ *
11995
+ * @example
11996
+ * // Legacy syntax - ultra-fast index-only check
11997
+ * const exists = await db.exists('nameTerms', 'tv');
11998
+ * const existsAll = await db.exists('nameTerms', ['tv', 'globo'], { $all: true });
11999
+ *
12000
+ * @example
12001
+ * // New syntax - full query criteria support
12002
+ * const exists = await db.exists({ mediaType: 'live', status: 'active' });
12003
+ * const existsOr = await db.exists({ mediaType: ['live', 'vod'] });
11880
12004
  */
11881
12005
  async exists(fieldNameOrCriteria, terms, options = {}) {
11882
12006
  this._validateInitialization('exists');
@@ -11890,6 +12014,7 @@ class Database extends events.EventEmitter {
11890
12014
  // Legacy syntax: exists(fieldName, terms, options)
11891
12015
  // Also handle invalid inputs (null, array) for backward compatibility
11892
12016
  const fieldName = fieldNameOrCriteria;
12017
+ await this._ensureLazyIndexLoaded();
11893
12018
  return this.indexManager.exists(fieldName, terms, options);
11894
12019
  } else {
11895
12020
  // Invalid input type
@@ -11897,13 +12022,13 @@ class Database extends events.EventEmitter {
11897
12022
  }
11898
12023
  }
11899
12024
 
11900
- /**
11901
- * Run batched index-only existence checks using a single connection per field.
11902
- * Each entry must specify { field, terms, options?, id? } so the result map stays stable.
11903
- *
11904
- * @param {Array<Object>} criteriaArray
11905
- * @param {Object} opts
11906
- * @returns {Promise<Object<string, boolean>>}
12025
+ /**
12026
+ * Run batched index-only existence checks using a single connection per field.
12027
+ * Each entry must specify { field, terms, options?, id? } so the result map stays stable.
12028
+ *
12029
+ * @param {Array<Object>} criteriaArray
12030
+ * @param {Object} opts
12031
+ * @returns {Promise<Object<string, boolean>>}
11907
12032
  */
11908
12033
  async multiExists(criteriaArray, opts = {}) {
11909
12034
  this._validateInitialization('multiExists');
@@ -11913,6 +12038,7 @@ class Database extends events.EventEmitter {
11913
12038
  if (!this.indexManager) {
11914
12039
  return {};
11915
12040
  }
12041
+ await this._ensureLazyIndexLoaded();
11916
12042
  const results = {};
11917
12043
  const perField = new Map();
11918
12044
  for (let i = 0; i < criteriaArray.length; i++) {
@@ -11942,12 +12068,12 @@ class Database extends events.EventEmitter {
11942
12068
  return results;
11943
12069
  }
11944
12070
 
11945
- /**
11946
- * Check if any records exist using full query criteria
11947
- * Uses index intersection when possible for maximum performance
11948
- * @private
11949
- * @param {object} criteria - Query criteria object
11950
- * @returns {Promise<boolean>} - True if at least one match exists
12071
+ /**
12072
+ * Check if any records exist using full query criteria
12073
+ * Uses index intersection when possible for maximum performance
12074
+ * @private
12075
+ * @param {object} criteria - Query criteria object
12076
+ * @returns {Promise<boolean>} - True if at least one match exists
11951
12077
  */
11952
12078
 
11953
12079
  async _existsWithCriteria(criteria) {
@@ -11974,7 +12100,7 @@ class Database extends events.EventEmitter {
11974
12100
  if (this.opts.debugMode) {
11975
12101
  console.log(`⚡ exists() using INDEX-ONLY optimization for: ${JSON.stringify(criteria)}`);
11976
12102
  }
11977
- return this._existsIndexOnly(criteria);
12103
+ return await this._existsIndexOnly(criteria);
11978
12104
  }
11979
12105
 
11980
12106
  // 🎯 FALLBACK: Use the same find() logic for complex criteria or non-indexed fields
@@ -11990,11 +12116,11 @@ class Database extends events.EventEmitter {
11990
12116
  }
11991
12117
  }
11992
12118
 
11993
- /**
11994
- * Check if criteria can use index-only existence check
11995
- * @private
11996
- * @param {object} criteria - Query criteria
11997
- * @returns {boolean} - True if can use index-only
12119
+ /**
12120
+ * Check if criteria can use index-only existence check
12121
+ * @private
12122
+ * @param {object} criteria - Query criteria
12123
+ * @returns {boolean} - True if can use index-only
11998
12124
  */
11999
12125
  _canUseIndexOnlyForExists(criteria) {
12000
12126
  // Must have indexes configured
@@ -12025,13 +12151,14 @@ class Database extends events.EventEmitter {
12025
12151
  return true;
12026
12152
  }
12027
12153
 
12028
- /**
12029
- * Perform index-only existence check (ultra-fast, no disk I/O)
12030
- * @private
12031
- * @param {object} criteria - Simple criteria with only indexed fields
12032
- * @returns {boolean} - True if any records match the criteria
12154
+ /**
12155
+ * Perform index-only existence check (ultra-fast, no disk I/O)
12156
+ * @private
12157
+ * @param {object} criteria - Simple criteria with only indexed fields
12158
+ * @returns {boolean} - True if any records match the criteria
12033
12159
  */
12034
- _existsIndexOnly(criteria) {
12160
+ async _existsIndexOnly(criteria) {
12161
+ await this._ensureLazyIndexLoaded();
12035
12162
  const criteriaEntries = Object.entries(criteria);
12036
12163
 
12037
12164
  // For single field criteria, use direct indexManager.exists()
@@ -12066,12 +12193,12 @@ class Database extends events.EventEmitter {
12066
12193
  return intersection.size > 0;
12067
12194
  }
12068
12195
 
12069
- /**
12070
- * Get line numbers for a field/value combination using index-only lookup
12071
- * @private
12072
- * @param {string} field - Field name
12073
- * @param {*} value - Field value (string, number, or array)
12074
- * @returns {Array<number>} Array of line numbers
12196
+ /**
12197
+ * Get line numbers for a field/value combination using index-only lookup
12198
+ * @private
12199
+ * @param {string} field - Field name
12200
+ * @param {*} value - Field value (string, number, or array)
12201
+ * @returns {Array<number>} Array of line numbers
12075
12202
  */
12076
12203
  _getIndexLinesForFieldValue(field, value) {
12077
12204
  // For arrays (OR logic), check each value
@@ -12088,12 +12215,12 @@ class Database extends events.EventEmitter {
12088
12215
  return this._getSingleFieldLines(field, value);
12089
12216
  }
12090
12217
 
12091
- /**
12092
- * Get line numbers for a single field/value using index lookup
12093
- * @private
12094
- * @param {string} field - Field name
12095
- * @param {*} value - Single field value
12096
- * @returns {Array<number>} Array of line numbers
12218
+ /**
12219
+ * Get line numbers for a single field/value using index lookup
12220
+ * @private
12221
+ * @param {string} field - Field name
12222
+ * @param {*} value - Single field value
12223
+ * @returns {Array<number>} Array of line numbers
12097
12224
  */
12098
12225
  _getSingleFieldLines(field, value) {
12099
12226
  // Use indexManager.exists() logic but return line numbers instead of boolean
@@ -12136,13 +12263,13 @@ class Database extends events.EventEmitter {
12136
12263
  return Array.from(lines);
12137
12264
  }
12138
12265
 
12139
- /**
12140
- * Calculate coverage for grouped include/exclude term sets
12141
- * @param {string} fieldName - Name of the indexed field
12142
- * @param {Array<object>} groups - Array of { terms, excludes } objects
12143
- * @param {object} filterCriteria - Optional filter criteria
12144
- * @param {object} options - Optional settings
12145
- * @returns {Promise<number>} Coverage percentage between 0 and 100
12266
+ /**
12267
+ * Calculate coverage for grouped include/exclude term sets
12268
+ * @param {string} fieldName - Name of the indexed field
12269
+ * @param {Array<object>} groups - Array of { terms, excludes } objects
12270
+ * @param {object} filterCriteria - Optional filter criteria
12271
+ * @param {object} options - Optional settings
12272
+ * @returns {Promise<number>} Coverage percentage between 0 and 100
12146
12273
  */
12147
12274
  async coverage(fieldName, groups, filterCriteria = null, options = {}) {
12148
12275
  this._validateInitialization('coverage');
@@ -12308,12 +12435,12 @@ class Database extends events.EventEmitter {
12308
12435
  return Number(coverageValue.toFixed(precision));
12309
12436
  }
12310
12437
 
12311
- /**
12312
- * Score records based on weighted terms in an indexed array:string field
12313
- * @param {string} fieldName - Name of indexed array:string field
12314
- * @param {object} scores - Map of terms to numeric weights
12315
- * @param {object} options - Query options
12316
- * @returns {Promise<Array>} Records with scores, sorted by score
12438
+ /**
12439
+ * Score records based on weighted terms in an indexed array:string field
12440
+ * @param {string} fieldName - Name of indexed array:string field
12441
+ * @param {object} scores - Map of terms to numeric weights
12442
+ * @param {object} options - Query options
12443
+ * @returns {Promise<Array>} Records with scores, sorted by score
12317
12444
  */
12318
12445
  async score(fieldName, scores, options = {}) {
12319
12446
  // Validate initialization
@@ -12367,12 +12494,23 @@ class Database extends events.EventEmitter {
12367
12494
  // Check if this is a term-mapped field
12368
12495
  const isTermMapped = this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.includes(fieldName);
12369
12496
 
12497
+ // CRITICAL FIX: Ensure index is loaded before accessing data
12498
+ // After idle unload, index.data[fieldName] becomes {} (truthy but empty)
12499
+ // Use _ensureLazyIndexLoaded() to reload from disk instead of _rebuildIndexesIfNeeded()
12500
+ // which requires allowIndexRebuild=true and rebuilds from scratch
12501
+ if (!this.indexManager.indexLoaded) {
12502
+ await this._ensureLazyIndexLoaded();
12503
+ }
12504
+
12370
12505
  // Access the index for this field
12371
12506
  const fieldIndex = this.indexManager.index.data[fieldName];
12372
- if (!fieldIndex) {
12507
+ if (!fieldIndex || Object.keys(fieldIndex).length === 0) {
12373
12508
  return [];
12374
12509
  }
12375
12510
 
12511
+ // Reset idle unload timer since we're using the index
12512
+ this.indexManager.markIndexUsed();
12513
+
12376
12514
  // Accumulate scores for each line number
12377
12515
  const scoreMap = new Map();
12378
12516
  const countMap = opts.mode === 'avg' ? new Map() : null;
@@ -12538,9 +12676,9 @@ class Database extends events.EventEmitter {
12538
12676
  return results;
12539
12677
  }
12540
12678
 
12541
- /**
12542
- * Read a grouped range with retry logic to handle I/O hangs
12543
- * @private
12679
+ /**
12680
+ * Read a grouped range with retry logic to handle I/O hangs
12681
+ * @private
12544
12682
  */
12545
12683
  async _readGroupedRangeWithRetry(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts) {
12546
12684
  // If no timeout configured, use original implementation without retry
@@ -12635,9 +12773,9 @@ class Database extends events.EventEmitter {
12635
12773
  return results;
12636
12774
  }
12637
12775
 
12638
- /**
12639
- * Original read grouped range logic without retry (for backward compatibility)
12640
- * @private
12776
+ /**
12777
+ * Original read grouped range logic without retry (for backward compatibility)
12778
+ * @private
12641
12779
  */
12642
12780
  async _readGroupedRangeOriginal(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts) {
12643
12781
  const results = [];
@@ -12695,9 +12833,9 @@ class Database extends events.EventEmitter {
12695
12833
  return results;
12696
12834
  }
12697
12835
 
12698
- /**
12699
- * Original rebuild indexes logic without retry (for backward compatibility)
12700
- * @private
12836
+ /**
12837
+ * Original rebuild indexes logic without retry (for backward compatibility)
12838
+ * @private
12701
12839
  */
12702
12840
  async _rebuildIndexesOriginal() {
12703
12841
  let count = 0;
@@ -12832,8 +12970,8 @@ class Database extends events.EventEmitter {
12832
12970
  return count;
12833
12971
  }
12834
12972
 
12835
- /**
12836
- * Wait for all pending operations to complete
12973
+ /**
12974
+ * Wait for all pending operations to complete
12837
12975
  */
12838
12976
  async _waitForPendingOperations() {
12839
12977
  if (this.operationQueue && this.operationQueue.getQueueLength() > 0) {
@@ -12851,8 +12989,8 @@ class Database extends events.EventEmitter {
12851
12989
  }
12852
12990
  }
12853
12991
 
12854
- /**
12855
- * Flush write buffer completely with smart detection of ongoing insertions
12992
+ /**
12993
+ * Flush write buffer completely with smart detection of ongoing insertions
12856
12994
  */
12857
12995
  async _flushWriteBufferCompletely() {
12858
12996
  // Force complete flush of write buffer with intelligent detection
@@ -12912,9 +13050,9 @@ class Database extends events.EventEmitter {
12912
13050
  }
12913
13051
  }
12914
13052
 
12915
- /**
12916
- * Get a lightweight sample of the write buffer for comparison
12917
- * @returns {Object} - Sample data for comparison
13053
+ /**
13054
+ * Get a lightweight sample of the write buffer for comparison
13055
+ * @returns {Object} - Sample data for comparison
12918
13056
  */
12919
13057
  _getBufferSample() {
12920
13058
  if (!this.writeBuffer || this.writeBuffer.length === 0) {
@@ -12953,11 +13091,11 @@ class Database extends events.EventEmitter {
12953
13091
  return sample;
12954
13092
  }
12955
13093
 
12956
- /**
12957
- * Check if two buffer samples are identical (indicating stuck flush)
12958
- * @param {Object} sample1 - First sample
12959
- * @param {Object} sample2 - Second sample
12960
- * @returns {boolean} - True if samples are identical
13094
+ /**
13095
+ * Check if two buffer samples are identical (indicating stuck flush)
13096
+ * @param {Object} sample1 - First sample
13097
+ * @param {Object} sample2 - Second sample
13098
+ * @returns {boolean} - True if samples are identical
12961
13099
  */
12962
13100
  _isBufferSampleIdentical(sample1, sample2) {
12963
13101
  if (!sample1 || !sample2) {
@@ -12991,8 +13129,8 @@ class Database extends events.EventEmitter {
12991
13129
  return true;
12992
13130
  }
12993
13131
 
12994
- /**
12995
- * Process write buffer items
13132
+ /**
13133
+ * Process write buffer items
12996
13134
  */
12997
13135
  async _processWriteBuffer() {
12998
13136
  // Process write buffer items without loading entire file
@@ -13070,8 +13208,8 @@ class Database extends events.EventEmitter {
13070
13208
  }
13071
13209
  }
13072
13210
 
13073
- /**
13074
- * Process individual buffer item
13211
+ /**
13212
+ * Process individual buffer item
13075
13213
  */
13076
13214
  async _processBufferItem(buffer) {
13077
13215
  // Process buffer item without loading entire file
@@ -13082,8 +13220,8 @@ class Database extends events.EventEmitter {
13082
13220
  }
13083
13221
  }
13084
13222
 
13085
- /**
13086
- * Process individual object item
13223
+ /**
13224
+ * Process individual object item
13087
13225
  */
13088
13226
  async _processObjectItem(obj) {
13089
13227
  // Process object item without loading entire file
@@ -13096,8 +13234,8 @@ class Database extends events.EventEmitter {
13096
13234
  }
13097
13235
  }
13098
13236
 
13099
- /**
13100
- * Process multiple object items in a single batch write operation
13237
+ /**
13238
+ * Process multiple object items in a single batch write operation
13101
13239
  */
13102
13240
  async _processObjectItemsBatch(objects) {
13103
13241
  if (!this.fileHandler || objects.length === 0) return;
@@ -13115,8 +13253,8 @@ class Database extends events.EventEmitter {
13115
13253
  await this.fileHandler.writeDataAsync(buffer);
13116
13254
  }
13117
13255
 
13118
- /**
13119
- * Wait for all I/O operations to complete
13256
+ /**
13257
+ * Wait for all I/O operations to complete
13120
13258
  */
13121
13259
  async _waitForIOCompletion() {
13122
13260
  // Wait for all file operations to complete
@@ -13128,9 +13266,9 @@ class Database extends events.EventEmitter {
13128
13266
  }
13129
13267
  }
13130
13268
 
13131
- /**
13132
- * CRITICAL FIX: Safe fallback method to load existing records when _streamExistingRecords fails
13133
- * This prevents data loss by attempting alternative methods to preserve existing data
13269
+ /**
13270
+ * CRITICAL FIX: Safe fallback method to load existing records when _streamExistingRecords fails
13271
+ * This prevents data loss by attempting alternative methods to preserve existing data
13134
13272
  */
13135
13273
  async _loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot) {
13136
13274
  const existingRecords = [];
@@ -13176,9 +13314,9 @@ class Database extends events.EventEmitter {
13176
13314
  }
13177
13315
  }
13178
13316
 
13179
- /**
13180
- * Stream existing records without loading entire file into memory
13181
- * Optimized with group ranging and reduced JSON parsing
13317
+ /**
13318
+ * Stream existing records without loading entire file into memory
13319
+ * Optimized with group ranging and reduced JSON parsing
13182
13320
  */
13183
13321
  async _streamExistingRecords(deletedIdsSnapshot, writeBufferSnapshot) {
13184
13322
  const existingRecords = [];
@@ -13650,8 +13788,8 @@ class Database extends events.EventEmitter {
13650
13788
  return existingRecords;
13651
13789
  }
13652
13790
 
13653
- /**
13654
- * Flush write buffer
13791
+ /**
13792
+ * Flush write buffer
13655
13793
  */
13656
13794
  async flush() {
13657
13795
  return this.operationQueue.enqueue(async () => {
@@ -13668,8 +13806,8 @@ class Database extends events.EventEmitter {
13668
13806
  });
13669
13807
  }
13670
13808
 
13671
- /**
13672
- * Flush insertion buffer (backward compatibility)
13809
+ /**
13810
+ * Flush insertion buffer (backward compatibility)
13673
13811
  */
13674
13812
  async flushInsertionBuffer() {
13675
13813
  // Flush insertion buffer implementation - save any pending data
@@ -13677,8 +13815,8 @@ class Database extends events.EventEmitter {
13677
13815
  return this.flush();
13678
13816
  }
13679
13817
 
13680
- /**
13681
- * Get memory usage
13818
+ /**
13819
+ * Get memory usage
13682
13820
  */
13683
13821
  getMemoryUsage() {
13684
13822
  return {
@@ -13690,7 +13828,7 @@ class Database extends events.EventEmitter {
13690
13828
  };
13691
13829
  }
13692
13830
  _hasActualIndexData() {
13693
- if (!this.indexManager) return false;
13831
+ if (!this.indexManager || this.indexManager.indexLoaded === false) return false;
13694
13832
  const data = this.indexManager.index.data;
13695
13833
  for (const field in data) {
13696
13834
  const fieldData = data[field];
@@ -13703,11 +13841,53 @@ class Database extends events.EventEmitter {
13703
13841
  }
13704
13842
  return false;
13705
13843
  }
13844
+ async _loadIndexDataFromFile() {
13845
+ const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
13846
+ try {
13847
+ await fs.promises.access(idxPath);
13848
+ } catch (error) {
13849
+ return null;
13850
+ }
13851
+ try {
13852
+ const raw = await fs.promises.readFile(idxPath, 'utf8');
13853
+ const parsed = JSON.parse(raw);
13854
+ return parsed?.index ?? null;
13855
+ } catch (error) {
13856
+ if (this.opts.debugMode) {
13857
+ console.error(`⚠️ _loadIndexDataFromFile: Failed to read or parse ${idxPath}`, error);
13858
+ }
13859
+ return null;
13860
+ }
13861
+ }
13862
+ async _ensureLazyIndexLoaded() {
13863
+ if (!this.indexManager || this.indexManager.indexLoaded) return;
13864
+ if (!this.indexManager.indexedFields || this.indexManager.indexedFields.length === 0) {
13865
+ this.indexManager.indexLoaded = true;
13866
+ return;
13867
+ }
13868
+ if (this._lazyIndexLoadPromise) {
13869
+ return this._lazyIndexLoadPromise;
13870
+ }
13871
+ this._lazyIndexLoadPromise = (async () => {
13872
+ const indexData = await this._loadIndexDataFromFile();
13873
+ if (indexData) {
13874
+ this.indexManager.load(indexData);
13875
+ } else {
13876
+ if (this.opts.debugMode) {
13877
+ console.log('🔍 _ensureLazyIndexLoaded: No index file found or invalid index data, continuing with empty index');
13878
+ }
13879
+ this.indexManager.indexLoaded = true;
13880
+ this.indexManager.markIndexUsed();
13881
+ }
13882
+ this._lazyIndexLoadPromise = null;
13883
+ })();
13884
+ return this._lazyIndexLoadPromise;
13885
+ }
13706
13886
 
13707
- /**
13708
- * Locate a record by line number and return its byte range
13709
- * @param {number} n - Line number
13710
- * @returns {Array} - [start, end] byte range or undefined
13887
+ /**
13888
+ * Locate a record by line number and return its byte range
13889
+ * @param {number} n - Line number
13890
+ * @returns {Array} - [start, end] byte range or undefined
13711
13891
  */
13712
13892
  locate(n) {
13713
13893
  if (this.offsets[n] === undefined) {
@@ -13726,10 +13906,10 @@ class Database extends events.EventEmitter {
13726
13906
  return [this.offsets[n], end];
13727
13907
  }
13728
13908
 
13729
- /**
13730
- * Get ranges for streaming based on line numbers
13731
- * @param {Array|Set} map - Line numbers to get ranges for
13732
- * @returns {Array} - Array of range objects {start, end, index}
13909
+ /**
13910
+ * Get ranges for streaming based on line numbers
13911
+ * @param {Array|Set} map - Line numbers to get ranges for
13912
+ * @returns {Array} - Array of range objects {start, end, index}
13733
13913
  */
13734
13914
  getRanges(map) {
13735
13915
  return (map || Array.from(this.offsets.keys())).map(n => {
@@ -13742,19 +13922,19 @@ class Database extends events.EventEmitter {
13742
13922
  }).filter(n => n !== undefined);
13743
13923
  }
13744
13924
 
13745
- /**
13746
- * Get the base line number for writeBuffer entries (number of persisted records)
13747
- * @private
13925
+ /**
13926
+ * Get the base line number for writeBuffer entries (number of persisted records)
13927
+ * @private
13748
13928
  */
13749
13929
  _getWriteBufferBaseLineNumber() {
13750
13930
  return Array.isArray(this.offsets) ? this.offsets.length : 0;
13751
13931
  }
13752
13932
 
13753
- /**
13754
- * Convert a writeBuffer index into an absolute line number
13755
- * @param {number} writeBufferIndex - Index inside writeBuffer (0-based)
13756
- * @returns {number} Absolute line number (0-based)
13757
- * @private
13933
+ /**
13934
+ * Convert a writeBuffer index into an absolute line number
13935
+ * @param {number} writeBufferIndex - Index inside writeBuffer (0-based)
13936
+ * @returns {number} Absolute line number (0-based)
13937
+ * @private
13758
13938
  */
13759
13939
  _getAbsoluteLineNumber(writeBufferIndex) {
13760
13940
  if (typeof writeBufferIndex !== 'number' || writeBufferIndex < 0) {
@@ -13763,10 +13943,10 @@ class Database extends events.EventEmitter {
13763
13943
  return this._getWriteBufferBaseLineNumber() + writeBufferIndex;
13764
13944
  }
13765
13945
 
13766
- /**
13767
- * Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
13768
- * @param {string} line - The corrupted line to recover
13769
- * @returns {string|null} - The recovered line or null if recovery is not possible
13946
+ /**
13947
+ * Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
13948
+ * @param {string} line - The corrupted line to recover
13949
+ * @returns {string|null} - The recovered line or null if recovery is not possible
13770
13950
  */
13771
13951
  _tryRecoverLine(line) {
13772
13952
  if (!line || typeof line !== 'string') {
@@ -14016,8 +14196,8 @@ class Database extends events.EventEmitter {
14016
14196
  }).apply(this, arguments);
14017
14197
  }
14018
14198
 
14019
- /**
14020
- * Walk through records using streaming (real implementation)
14199
+ /**
14200
+ * Walk through records using streaming (real implementation)
14021
14201
  */
14022
14202
  walk(_x3) {
14023
14203
  var _this2 = this;
@@ -14474,18 +14654,18 @@ class Database extends events.EventEmitter {
14474
14654
  }).apply(this, arguments);
14475
14655
  }
14476
14656
 
14477
- /**
14478
- * Iterate through records with bulk update capabilities
14479
- * Allows in-place modifications and deletions with optimized performance
14480
- *
14481
- * @param {Object} criteria - Query criteria
14482
- * @param {Object} options - Iteration options
14483
- * @param {number} options.chunkSize - Batch size for processing (default: 1000)
14484
- * @param {string} options.strategy - Processing strategy: 'streaming' (always uses walk() method)
14485
- * @param {boolean} options.autoSave - Auto-save after each chunk (default: false)
14486
- * @param {Function} options.progressCallback - Progress callback function
14487
- * @param {boolean} options.detectChanges - Auto-detect changes (default: true)
14488
- * @returns {AsyncGenerator} Generator yielding records for modification
14657
+ /**
14658
+ * Iterate through records with bulk update capabilities
14659
+ * Allows in-place modifications and deletions with optimized performance
14660
+ *
14661
+ * @param {Object} criteria - Query criteria
14662
+ * @param {Object} options - Iteration options
14663
+ * @param {number} options.chunkSize - Batch size for processing (default: 1000)
14664
+ * @param {string} options.strategy - Processing strategy: 'streaming' (always uses walk() method)
14665
+ * @param {boolean} options.autoSave - Auto-save after each chunk (default: false)
14666
+ * @param {Function} options.progressCallback - Progress callback function
14667
+ * @param {boolean} options.detectChanges - Auto-detect changes (default: true)
14668
+ * @returns {AsyncGenerator} Generator yielding records for modification
14489
14669
  */
14490
14670
  iterate(_x4) {
14491
14671
  var _this3 = this;
@@ -14620,9 +14800,9 @@ class Database extends events.EventEmitter {
14620
14800
  }).apply(this, arguments);
14621
14801
  }
14622
14802
 
14623
- /**
14624
- * Process a batch of updates and deletes from iterate operation
14625
- * @private
14803
+ /**
14804
+ * Process a batch of updates and deletes from iterate operation
14805
+ * @private
14626
14806
  */
14627
14807
  async _processIterateBatch(updateBuffer, deleteBuffer, options) {
14628
14808
  if (updateBuffer.length === 0 && deleteBuffer.size === 0) return;
@@ -14649,6 +14829,7 @@ class Database extends events.EventEmitter {
14649
14829
 
14650
14830
  // Update index
14651
14831
  const absoluteLineNumber = this._getAbsoluteLineNumber(targetIndex);
14832
+ await this._ensureLazyIndexLoaded();
14652
14833
  await this.indexManager.update(record, record, absoluteLineNumber);
14653
14834
  }
14654
14835
  if (this.opts.debugMode) {
@@ -14668,6 +14849,7 @@ class Database extends events.EventEmitter {
14668
14849
  this.removeTermMapping(record);
14669
14850
 
14670
14851
  // Remove from index
14852
+ await this._ensureLazyIndexLoaded();
14671
14853
  await this.indexManager.remove(record);
14672
14854
 
14673
14855
  // Remove from writeBuffer or mark as deleted
@@ -14697,8 +14879,8 @@ class Database extends events.EventEmitter {
14697
14879
  }
14698
14880
  }
14699
14881
 
14700
- /**
14701
- * Close the database
14882
+ /**
14883
+ * Close the database
14702
14884
  */
14703
14885
  async close() {
14704
14886
  if (this.destroyed || this.closed) return;
@@ -14742,11 +14924,14 @@ class Database extends events.EventEmitter {
14742
14924
  }
14743
14925
  }
14744
14926
 
14745
- // 2. Mark as closed (but not destroyed) to allow reopening
14927
+ // 2. Cancel any pending idle unload timers before shutdown
14928
+ this.indexManager?.cancelIdleUnloadTimer?.();
14929
+
14930
+ // 3. Mark as closed (but not destroyed) to allow reopening
14746
14931
  this.closed = true;
14747
14932
  this.initialized = false;
14748
14933
 
14749
- // 3. Clear any remaining state for clean reopening
14934
+ // 4. Clear any remaining state for clean reopening
14750
14935
  this.writeBuffer = [];
14751
14936
  this.writeBufferOffsets = [];
14752
14937
  this.writeBufferSizes = [];
@@ -14765,14 +14950,20 @@ class Database extends events.EventEmitter {
14765
14950
  }
14766
14951
  }
14767
14952
 
14768
- /**
14769
- * Save index data to .idx.jdb file
14770
- * @private
14953
+ /**
14954
+ * Save index data to .idx.jdb file
14955
+ * @private
14771
14956
  */
14772
14957
  async _saveIndexDataToFile() {
14773
14958
  if (this.indexManager) {
14774
14959
  try {
14775
14960
  const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
14961
+ if (this.indexManager.indexLoaded === false && fs.existsSync(idxPath)) {
14962
+ if (this.opts.debugMode) {
14963
+ console.log(`⚠️ _saveIndexDataToFile: Index unloaded in memory, preserving existing index file ${idxPath}`);
14964
+ }
14965
+ return;
14966
+ }
14776
14967
  const indexJSON = this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {};
14777
14968
 
14778
14969
  // Check if index is empty
@@ -14843,8 +15034,8 @@ class Database extends events.EventEmitter {
14843
15034
  }
14844
15035
  }
14845
15036
 
14846
- /**
14847
- * Get operation queue statistics
15037
+ /**
15038
+ * Get operation queue statistics
14848
15039
  */
14849
15040
  getQueueStats() {
14850
15041
  if (!this.operationQueue) {
@@ -14862,11 +15053,11 @@ class Database extends events.EventEmitter {
14862
15053
  return this.operationQueue.getStats();
14863
15054
  }
14864
15055
 
14865
- /**
14866
- * Wait for all pending operations to complete
14867
- * This includes operation queue AND active insert sessions
14868
- * If called with no arguments, interpret as waitForOperations(null).
14869
- * If argument provided (maxWaitTime), pass that on.
15056
+ /**
15057
+ * Wait for all pending operations to complete
15058
+ * This includes operation queue AND active insert sessions
15059
+ * If called with no arguments, interpret as waitForOperations(null).
15060
+ * If argument provided (maxWaitTime), pass that on.
14870
15061
  */
14871
15062
  async waitForOperations(maxWaitTime = null) {
14872
15063
  // Accept any falsy/undefined/empty call as "wait for all"