jexidb 2.1.0 → 2.1.2
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 +9253 -437
- package/package.json +9 -2
- package/src/Database.mjs +1572 -212
- package/src/FileHandler.mjs +83 -44
- package/src/OperationQueue.mjs +23 -23
- package/src/SchemaManager.mjs +325 -268
- package/src/Serializer.mjs +234 -24
- package/src/managers/IndexManager.mjs +778 -87
- package/src/managers/QueryManager.mjs +340 -67
- package/src/managers/TermManager.mjs +7 -7
- package/src/utils/operatorNormalizer.mjs +116 -0
- package/.babelrc +0 -13
- package/.gitattributes +0 -2
- package/CHANGELOG.md +0 -140
- package/babel.config.json +0 -5
- package/docs/API.md +0 -1051
- package/docs/EXAMPLES.md +0 -701
- package/docs/README.md +0 -194
- package/examples/iterate-usage-example.js +0 -157
- package/examples/simple-iterate-example.js +0 -115
- package/jest.config.js +0 -24
- package/scripts/README.md +0 -47
- package/scripts/clean-test-files.js +0 -75
- package/scripts/prepare.js +0 -31
- package/scripts/run-tests.js +0 -80
- package/test/$not-operator-with-and.test.js +0 -282
- package/test/README.md +0 -8
- package/test/close-init-cycle.test.js +0 -256
- package/test/critical-bugs-fixes.test.js +0 -1069
- package/test/index-persistence.test.js +0 -306
- package/test/index-serialization.test.js +0 -314
- package/test/indexed-query-mode.test.js +0 -360
- package/test/iterate-method.test.js +0 -272
- package/test/query-operators.test.js +0 -238
- package/test/regex-array-fields.test.js +0 -129
- package/test/score-method.test.js +0 -238
- package/test/setup.js +0 -17
- package/test/term-mapping-minimal.test.js +0 -154
- package/test/term-mapping-simple.test.js +0 -257
- package/test/term-mapping.test.js +0 -514
- package/test/writebuffer-flush-resilience.test.js +0 -204
package/src/FileHandler.mjs
CHANGED
|
@@ -6,10 +6,12 @@ import pLimit from 'p-limit'
|
|
|
6
6
|
export default class FileHandler {
|
|
7
7
|
constructor(file, fileMutex = null, opts = {}) {
|
|
8
8
|
this.file = file
|
|
9
|
-
this.indexFile = file.replace(/\.jdb$/, '.idx.jdb')
|
|
9
|
+
this.indexFile = file ? file.replace(/\.jdb$/, '.idx.jdb') : null
|
|
10
10
|
this.fileMutex = fileMutex
|
|
11
11
|
this.opts = opts
|
|
12
12
|
this.maxBufferSize = opts.maxBufferSize || 4 * 1024 * 1024 // 4MB default
|
|
13
|
+
// Global I/O limiter to prevent file descriptor exhaustion in concurrent operations
|
|
14
|
+
this.readLimiter = pLimit(opts.maxConcurrentReads || 4)
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
async truncate(offset) {
|
|
@@ -174,7 +176,7 @@ export default class FileHandler {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
async readRanges(ranges, mapper) {
|
|
177
|
-
const lines = {}
|
|
179
|
+
const lines = {}
|
|
178
180
|
|
|
179
181
|
// Check if file exists before trying to read it
|
|
180
182
|
if (!await this.exists()) {
|
|
@@ -185,7 +187,7 @@ export default class FileHandler {
|
|
|
185
187
|
const groupedRanges = await this.groupedRanges(ranges)
|
|
186
188
|
try {
|
|
187
189
|
await Promise.allSettled(groupedRanges.map(async (groupedRange) => {
|
|
188
|
-
await
|
|
190
|
+
await this.readLimiter(async () => {
|
|
189
191
|
for await (const row of this.readGroupedRange(groupedRange, fd)) {
|
|
190
192
|
lines[row.start] = mapper ? (await mapper(row.line, { start: row.start, end: row.start + row.line.length })) : row.line
|
|
191
193
|
}
|
|
@@ -255,6 +257,10 @@ export default class FileHandler {
|
|
|
255
257
|
lineString = actualBuffer.toString('utf8', { replacement: '?' })
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
// CRITICAL FIX: Remove trailing newlines and whitespace for single range too
|
|
261
|
+
// Optimized: Use trimEnd() which efficiently removes all trailing whitespace (faster than manual checks)
|
|
262
|
+
lineString = lineString.trimEnd()
|
|
263
|
+
|
|
258
264
|
yield {
|
|
259
265
|
line: lineString,
|
|
260
266
|
start: range.start,
|
|
@@ -301,14 +307,42 @@ export default class FileHandler {
|
|
|
301
307
|
}
|
|
302
308
|
}
|
|
303
309
|
} else {
|
|
304
|
-
//
|
|
310
|
+
// CRITICAL FIX: For non-adjacent ranges, use the range.end directly
|
|
311
|
+
// because range.end already excludes the newline (calculated as offsets[n+1] - 1)
|
|
312
|
+
// We just need to find the line start (beginning of the line in the buffer)
|
|
305
313
|
for (let i = 0; i < groupedRange.length; i++) {
|
|
306
314
|
const range = groupedRange[i]
|
|
307
315
|
const relativeStart = range.start - firstRange.start
|
|
308
316
|
const relativeEnd = range.end - firstRange.start
|
|
309
317
|
|
|
310
|
-
//
|
|
311
|
-
|
|
318
|
+
// OPTIMIZATION 2: Find line start only if necessary
|
|
319
|
+
// Check if we're already at a line boundary to avoid unnecessary backwards search
|
|
320
|
+
let lineStart = relativeStart
|
|
321
|
+
if (relativeStart > 0 && content[relativeStart - 1] !== '\n') {
|
|
322
|
+
// Only search backwards if we're not already at a line boundary
|
|
323
|
+
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
|
|
324
|
+
lineStart--
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// OPTIMIZATION 3: Use slice() instead of substring() for better performance
|
|
329
|
+
// CRITICAL FIX: range.end = offsets[n+1] - 1 points to the newline character
|
|
330
|
+
// slice(start, end) includes characters from start to end-1 (end is exclusive)
|
|
331
|
+
// So if relativeEnd points to the newline, slice will include it
|
|
332
|
+
let rangeContent = content.slice(lineStart, relativeEnd)
|
|
333
|
+
|
|
334
|
+
// OPTIMIZATION 4: Direct character check instead of regex/trimEnd
|
|
335
|
+
// Remove trailing newlines and whitespace efficiently
|
|
336
|
+
// trimEnd() is actually optimized in V8, but we can check if there's anything to trim first
|
|
337
|
+
const len = rangeContent.length
|
|
338
|
+
if (len > 0) {
|
|
339
|
+
// Quick check: if last char is not whitespace, skip trimEnd
|
|
340
|
+
const lastChar = rangeContent[len - 1]
|
|
341
|
+
if (lastChar === '\n' || lastChar === '\r' || lastChar === ' ' || lastChar === '\t') {
|
|
342
|
+
// Only call trimEnd if we detected trailing whitespace
|
|
343
|
+
rangeContent = rangeContent.trimEnd()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
312
346
|
|
|
313
347
|
if (rangeContent.length === 0) continue
|
|
314
348
|
|
|
@@ -543,45 +577,48 @@ export default class FileHandler {
|
|
|
543
577
|
}
|
|
544
578
|
|
|
545
579
|
async readLastLine() {
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
580
|
+
// Use global read limiter to prevent file descriptor exhaustion
|
|
581
|
+
return this.readLimiter(async () => {
|
|
582
|
+
// Check if file exists before trying to read it
|
|
583
|
+
if (!await this.exists()) {
|
|
584
|
+
return null // Return null if file doesn't exist
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const reader = await fs.promises.open(this.file, 'r')
|
|
588
|
+
try {
|
|
589
|
+
const { size } = await reader.stat()
|
|
590
|
+
if (size < 1) throw 'empty file'
|
|
591
|
+
this.size = size
|
|
592
|
+
const bufferSize = 16384
|
|
593
|
+
let buffer, isFirstRead = true, lastReadSize, readPosition = Math.max(size - bufferSize, 0)
|
|
594
|
+
while (readPosition >= 0) {
|
|
595
|
+
const readSize = Math.min(bufferSize, size - readPosition)
|
|
596
|
+
if (readSize !== lastReadSize) {
|
|
597
|
+
lastReadSize = readSize
|
|
598
|
+
buffer = Buffer.alloc(readSize)
|
|
599
|
+
}
|
|
600
|
+
const { bytesRead } = await reader.read(buffer, 0, isFirstRead ? (readSize - 1) : readSize, readPosition)
|
|
601
|
+
if (isFirstRead) isFirstRead = false
|
|
602
|
+
if (bytesRead === 0) break
|
|
603
|
+
const newlineIndex = buffer.lastIndexOf(10)
|
|
604
|
+
const start = readPosition + newlineIndex + 1
|
|
605
|
+
if (newlineIndex !== -1) {
|
|
606
|
+
const lastLine = Buffer.alloc(size - start)
|
|
607
|
+
await reader.read(lastLine, 0, size - start, start)
|
|
608
|
+
if (!lastLine || !lastLine.length) {
|
|
609
|
+
throw 'no metadata or empty file'
|
|
610
|
+
}
|
|
611
|
+
return lastLine
|
|
612
|
+
} else {
|
|
613
|
+
readPosition -= bufferSize
|
|
574
614
|
}
|
|
575
|
-
return lastLine
|
|
576
|
-
} else {
|
|
577
|
-
readPosition -= bufferSize
|
|
578
615
|
}
|
|
616
|
+
} catch (e) {
|
|
617
|
+
String(e).includes('empty file') || console.error('Error reading last line:', e)
|
|
618
|
+
} finally {
|
|
619
|
+
reader.close()
|
|
579
620
|
}
|
|
580
|
-
}
|
|
581
|
-
String(e).includes('empty file') || console.error('Error reading last line:', e)
|
|
582
|
-
} finally {
|
|
583
|
-
reader.close()
|
|
584
|
-
}
|
|
621
|
+
})
|
|
585
622
|
}
|
|
586
623
|
|
|
587
624
|
/**
|
|
@@ -597,10 +634,12 @@ export default class FileHandler {
|
|
|
597
634
|
return this.fileMutex.runExclusive(async () => {
|
|
598
635
|
// Add a small delay to ensure any pending operations complete
|
|
599
636
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
600
|
-
|
|
637
|
+
// Use global read limiter to prevent file descriptor exhaustion
|
|
638
|
+
return this.readLimiter(() => this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer));
|
|
601
639
|
});
|
|
602
640
|
} else {
|
|
603
|
-
|
|
641
|
+
// Use global read limiter to prevent file descriptor exhaustion
|
|
642
|
+
return this.readLimiter(() => this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer));
|
|
604
643
|
}
|
|
605
644
|
}
|
|
606
645
|
|
package/src/OperationQueue.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OperationQueue -
|
|
3
|
-
*
|
|
2
|
+
* OperationQueue - Queue system for database operations
|
|
3
|
+
* Resolves race conditions between concurrent operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class OperationQueue {
|
|
@@ -20,9 +20,9 @@ export class OperationQueue {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* @param {Function} operation -
|
|
25
|
-
* @returns {Promise} - Promise
|
|
23
|
+
* Adds an operation to the queue
|
|
24
|
+
* @param {Function} operation - Asynchronous function to be executed
|
|
25
|
+
* @returns {Promise} - Promise that resolves when the operation is completed
|
|
26
26
|
*/
|
|
27
27
|
async enqueue(operation) {
|
|
28
28
|
const id = ++this.operationId
|
|
@@ -48,13 +48,13 @@ export class OperationQueue {
|
|
|
48
48
|
startTime: Date.now()
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// Process immediately if not already processing
|
|
52
52
|
this.process().catch(reject)
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Processes all operations in the queue sequentially
|
|
58
58
|
*/
|
|
59
59
|
async process() {
|
|
60
60
|
if (this.processing || this.queue.length === 0) {
|
|
@@ -116,9 +116,9 @@ export class OperationQueue {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
*
|
|
120
|
-
* @param {number|null} maxWaitTime -
|
|
121
|
-
* @returns {Promise<boolean>} - true
|
|
119
|
+
* Waits for all pending operations to be processed
|
|
120
|
+
* @param {number|null} maxWaitTime - Maximum wait time in ms (null = wait indefinitely)
|
|
121
|
+
* @returns {Promise<boolean>} - true if all operations were processed, false if a timeout occurred
|
|
122
122
|
*/
|
|
123
123
|
async waitForCompletion(maxWaitTime = 5000) {
|
|
124
124
|
const startTime = Date.now()
|
|
@@ -167,21 +167,21 @@ export class OperationQueue {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
*
|
|
170
|
+
* Returns the current queue length
|
|
171
171
|
*/
|
|
172
172
|
getQueueLength() {
|
|
173
173
|
return this.queue.length
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
*
|
|
177
|
+
* Checks whether operations are currently being processed
|
|
178
178
|
*/
|
|
179
179
|
isProcessing() {
|
|
180
180
|
return this.processing
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
|
-
*
|
|
184
|
+
* Returns queue statistics
|
|
185
185
|
*/
|
|
186
186
|
getStats() {
|
|
187
187
|
return {
|
|
@@ -194,7 +194,7 @@ export class OperationQueue {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
|
-
*
|
|
197
|
+
* Clears the queue (for emergency situations)
|
|
198
198
|
*/
|
|
199
199
|
clear() {
|
|
200
200
|
const clearedCount = this.queue.length
|
|
@@ -208,9 +208,9 @@ export class OperationQueue {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/**
|
|
211
|
-
*
|
|
212
|
-
* @param {number} stuckThreshold -
|
|
213
|
-
* @returns {Array} -
|
|
211
|
+
* Detects stuck operations and returns detailed information
|
|
212
|
+
* @param {number} stuckThreshold - Time in ms to consider an operation stuck
|
|
213
|
+
* @returns {Array} - List of stuck operations with stack traces
|
|
214
214
|
*/
|
|
215
215
|
detectStuckOperations(stuckThreshold = 10000) {
|
|
216
216
|
const now = Date.now()
|
|
@@ -225,15 +225,15 @@ export class OperationQueue {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
*
|
|
229
|
-
* @param {number} stuckThreshold -
|
|
230
|
-
* @returns {number} -
|
|
228
|
+
* Force-cleans stuck operations (last resort)
|
|
229
|
+
* @param {number} stuckThreshold - Time in ms to consider an operation stuck
|
|
230
|
+
* @returns {number} - Number of operations removed
|
|
231
231
|
*/
|
|
232
232
|
forceCleanupStuckOperations(stuckThreshold = 10000) {
|
|
233
233
|
const stuckOps = this.detectStuckOperations(stuckThreshold)
|
|
234
234
|
|
|
235
235
|
if (stuckOps.length > 0) {
|
|
236
|
-
//
|
|
236
|
+
// Reject all stuck operations
|
|
237
237
|
stuckOps.forEach(stuckOp => {
|
|
238
238
|
const opIndex = this.queue.findIndex(op => op.id === stuckOp.id)
|
|
239
239
|
if (opIndex !== -1) {
|
|
@@ -255,14 +255,14 @@ export class OperationQueue {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
*
|
|
258
|
+
* Checks whether the queue is empty
|
|
259
259
|
*/
|
|
260
260
|
isEmpty() {
|
|
261
261
|
return this.queue.length === 0
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
|
-
*
|
|
265
|
+
* Returns information about the next operation in the queue
|
|
266
266
|
*/
|
|
267
267
|
peekNext() {
|
|
268
268
|
if (this.queue.length === 0) {
|