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.
- package/README.md +460 -449
- package/dist/Database.cjs +1321 -259
- package/package.json +3 -2
- package/src/Database.mjs +674 -136
- package/src/FileHandler.mjs +92 -32
- package/src/managers/QueryManager.mjs +13 -7
- package/src/managers/TermManager.mjs +7 -0
package/src/FileHandler.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
843
|
+
return this.readLimiter(() => this._readWithStreamingRetry(criteria, options, matchesCriteria, serializer));
|
|
843
844
|
}
|
|
844
845
|
}
|
|
845
846
|
|
|
846
|
-
async
|
|
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 (
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
}
|
|
905
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = (
|