jexidb 2.1.5 → 2.1.7

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.
@@ -2,6 +2,7 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import readline from 'readline'
4
4
  import pLimit from 'p-limit'
5
+ import pRetry from 'p-retry'
5
6
 
6
7
  export default class FileHandler {
7
8
  constructor(file, fileMutex = null, opts = {}) {
@@ -835,15 +836,55 @@ export default class FileHandler {
835
836
  // Add a small delay to ensure any pending operations complete
836
837
  await new Promise(resolve => setTimeout(resolve, 5));
837
838
  // Use global read limiter to prevent file descriptor exhaustion
838
- return this.readLimiter(() => this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer));
839
+ return this.readLimiter(() => this._readWithStreamingRetry(criteria, options, matchesCriteria, serializer));
839
840
  });
840
841
  } else {
841
842
  // Use global read limiter to prevent file descriptor exhaustion
842
- return this.readLimiter(() => this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer));
843
+ return this.readLimiter(() => this._readWithStreamingRetry(criteria, options, matchesCriteria, serializer));
843
844
  }
844
845
  }
845
846
 
846
- async _readWithStreamingInternal(criteria, options = {}, matchesCriteria, serializer = null) {
847
+ async _readWithStreamingRetry(criteria, options = {}, matchesCriteria, serializer = null) {
848
+ // If no timeout configured, use original implementation without retry
849
+ if (!options.ioTimeoutMs) {
850
+ return this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer);
851
+ }
852
+
853
+ const timeoutMs = options.ioTimeoutMs || 5000; // Default 5s timeout per attempt
854
+ const maxRetries = options.maxRetries || 3;
855
+
856
+ return pRetry(async (attempt) => {
857
+ const controller = new AbortController();
858
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
859
+
860
+ try {
861
+ const results = await this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer, controller.signal);
862
+ return results;
863
+ } catch (error) {
864
+ if (error.name === 'AbortError' || error.code === 'ETIMEDOUT') {
865
+ if (this.opts.debugMode) {
866
+ console.log(`⚠️ Streaming read attempt ${attempt} timed out, retrying...`);
867
+ }
868
+ throw error; // p-retry will retry
869
+ }
870
+ // For other errors, don't retry
871
+ throw new pRetry.AbortError(error);
872
+ } finally {
873
+ clearTimeout(timeout);
874
+ }
875
+ }, {
876
+ retries: maxRetries,
877
+ minTimeout: 100,
878
+ maxTimeout: 1000,
879
+ onFailedAttempt: (error) => {
880
+ if (this.opts.debugMode) {
881
+ console.log(`Streaming read failed (attempt ${error.attemptNumber}), ${error.retriesLeft} retries left`);
882
+ }
883
+ }
884
+ });
885
+ }
886
+
887
+ async _readWithStreamingInternal(criteria, options = {}, matchesCriteria, serializer = null, signal = null) {
847
888
  const { limit, skip = 0 } = options; // No default limit
848
889
  const results = [];
849
890
  let lineNumber = 0;
@@ -870,42 +911,57 @@ export default class FileHandler {
870
911
  crlfDelay: Infinity // Better performance
871
912
  });
872
913
 
914
+ // Handle abort signal
915
+ if (signal) {
916
+ signal.addEventListener('abort', () => {
917
+ stream.destroy();
918
+ rl.close();
919
+ });
920
+ }
921
+
873
922
  // Process line by line
874
923
  for await (const line of rl) {
875
- if (lineNumber >= skip) {
876
- try {
877
- let record;
878
- if (serializer && typeof serializer.deserialize === 'function') {
879
- // Use serializer for deserialization
880
- record = serializer.deserialize(line);
881
- } else {
882
- // Fallback to JSON.parse for backward compatibility
883
- record = JSON.parse(line);
884
- }
924
+ if (signal && signal.aborted) {
925
+ break; // Stop if aborted
926
+ }
927
+
928
+ lineNumber++;
929
+
930
+ // Skip lines that were already processed in previous attempts
931
+ if (lineNumber <= skip) {
932
+ skipped++;
933
+ continue;
934
+ }
935
+
936
+ try {
937
+ let record;
938
+ if (serializer && typeof serializer.deserialize === 'function') {
939
+ // Use serializer for deserialization
940
+ record = serializer.deserialize(line);
941
+ } else {
942
+ // Fallback to JSON.parse for backward compatibility
943
+ record = JSON.parse(line);
944
+ }
945
+
946
+ if (record && matchesCriteria(record, criteria)) {
947
+ // Return raw data - term mapping will be handled by Database layer
948
+ results.push({ ...record, _: lineNumber });
949
+ matched++;
885
950
 
886
- if (record && matchesCriteria(record, criteria)) {
887
- // Return raw data - term mapping will be handled by Database layer
888
- results.push({ ...record, _: lineNumber });
889
- matched++;
890
-
891
- // Check if we've reached the limit
892
- if (results.length >= limit) {
893
- break;
894
- }
895
- }
896
- } catch (error) {
897
- // CRITICAL FIX: Only log errors if they're not expected during concurrent operations
898
- // Don't log JSON parsing errors that occur during file writes
899
- if (this.opts && this.opts.debugMode && !error.message.includes('Unexpected')) {
900
- console.log(`Error reading line ${lineNumber}:`, error.message);
951
+ // Check if we've reached the limit
952
+ if (results.length >= limit) {
953
+ break;
901
954
  }
902
- // Ignore invalid lines - they may be partial writes
903
955
  }
904
- } else {
905
- skipped++;
956
+ } catch (error) {
957
+ // CRITICAL FIX: Only log errors if they're not expected during concurrent operations
958
+ // Don't log JSON parsing errors that occur during file writes
959
+ if (this.opts && this.opts.debugMode && !error.message.includes('Unexpected')) {
960
+ console.log(`Error reading line ${lineNumber}:`, error.message);
961
+ }
962
+ // Ignore invalid lines - they may be partial writes
906
963
  }
907
964
 
908
- lineNumber++;
909
965
  processed++;
910
966
  }
911
967
 
@@ -916,6 +972,10 @@ export default class FileHandler {
916
972
  return results;
917
973
 
918
974
  } catch (error) {
975
+ if (error.message === 'AbortError') {
976
+ // Return partial results if aborted
977
+ return results;
978
+ }
919
979
  console.error('Error in readWithStreaming:', error);
920
980
  throw error;
921
981
  }
@@ -1,4 +1,5 @@
1
1
  import { normalizeOperator } from '../utils/operatorNormalizer.mjs'
2
+ import { promises as fs } from 'fs'
2
3
 
3
4
  /**
4
5
  * QueryManager - Handles all query operations and strategies
@@ -492,8 +493,7 @@ export class QueryManager {
492
493
  const ranges = this.database.getRanges(batch);
493
494
  const groupedRanges = await this.fileHandler.groupedRanges(ranges);
494
495
 
495
- const fs = await import('fs');
496
- const fd = await fs.promises.open(this.fileHandler.file, 'r');
496
+ const fd = await fs.open(this.fileHandler.file, 'r');
497
497
 
498
498
  try {
499
499
  for (const groupedRange of groupedRanges) {
@@ -631,18 +631,23 @@ export class QueryManager {
631
631
  const results = await this.fileHandler.readWithStreaming(criteria, streamingOptions, (record, criteria) => {
632
632
  return this.matchesCriteria(record, criteria, options);
633
633
  }, this.serializer || null);
634
+
635
+ // SPACE OPTIMIZATION: Restore term IDs to terms for user (unless disabled)
636
+ const resultsWithTerms = options.restoreTerms !== false ?
637
+ results.map(record => this.database.restoreTermIdsAfterDeserialization(record)) :
638
+ results;
634
639
 
635
640
  // Apply ordering if specified
636
641
  if (options.orderBy) {
637
642
  const [field, direction = 'asc'] = options.orderBy.split(' ');
638
- results.sort((a, b) => {
643
+ resultsWithTerms.sort((a, b) => {
639
644
  if (a[field] > b[field]) return direction === 'asc' ? 1 : -1;
640
645
  if (a[field] < b[field]) return direction === 'asc' ? -1 : 1;
641
646
  return 0;
642
647
  });
643
648
  }
644
649
 
645
- return results;
650
+ return resultsWithTerms;
646
651
  }
647
652
 
648
653
  /**
@@ -729,8 +734,7 @@ export class QueryManager {
729
734
  if (ranges.length > 0) {
730
735
  const groupedRanges = await this.database.fileHandler.groupedRanges(ranges)
731
736
 
732
- const fs = await import('fs')
733
- const fd = await fs.promises.open(this.database.fileHandler.file, 'r')
737
+ const fd = await fs.open(this.database.fileHandler.file, 'r')
734
738
 
735
739
  try {
736
740
  for (const groupedRange of groupedRanges) {
@@ -740,6 +744,7 @@ export class QueryManager {
740
744
  const recordWithTerms = options.restoreTerms !== false ?
741
745
  this.database.restoreTermIdsAfterDeserialization(record) :
742
746
  record
747
+ recordWithTerms._ = row._
743
748
  results.push(recordWithTerms)
744
749
  if (limit && results.length >= limit) break
745
750
  } catch (error) {
@@ -766,6 +771,7 @@ export class QueryManager {
766
771
  const recordWithTerms = options.restoreTerms !== false ?
767
772
  this.database.restoreTermIdsAfterDeserialization(record) :
768
773
  record
774
+ recordWithTerms._ = lineNumber
769
775
  results.push(recordWithTerms)
770
776
  }
771
777
  }
@@ -1470,7 +1476,7 @@ export class QueryManager {
1470
1476
  if (typeof condition === 'object' && !Array.isArray(condition)) {
1471
1477
  const operators = Object.keys(condition);
1472
1478
  for (const op of operators) {
1473
- if (!['$in', '$nin', '$contains', '$all', '>', '>=', '<', '<=', '!=', 'contains', 'regex'].includes(op)) {
1479
+ if (!['$in', '$nin', '$contains', '$all', '$exists', '>', '>=', '<', '<=', '!=', 'contains', 'regex'].includes(op)) {
1474
1480
  throw new Error(`Operator '${op}' is not supported in strict mode for field '${field}'.`);
1475
1481
  }
1476
1482
  }
@@ -145,6 +145,13 @@ export default class TermManager {
145
145
  const stats = this.getStats()
146
146
  const orphanedCount = stats.orphanedTerms
147
147
  const totalTerms = stats.totalTerms
148
+
149
+ // SAFETY: If all terms are marked as orphaned, it likely means counts
150
+ // haven't been rebuilt after loading from disk. Skip cleanup to avoid
151
+ // wiping valid term mappings.
152
+ if (totalTerms > 0 && orphanedCount === totalTerms) {
153
+ return 0
154
+ }
148
155
 
149
156
  // Only cleanup if conditions are met
150
157
  const shouldCleanup = (