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.
Files changed (41) hide show
  1. package/dist/Database.cjs +9253 -437
  2. package/package.json +9 -2
  3. package/src/Database.mjs +1572 -212
  4. package/src/FileHandler.mjs +83 -44
  5. package/src/OperationQueue.mjs +23 -23
  6. package/src/SchemaManager.mjs +325 -268
  7. package/src/Serializer.mjs +234 -24
  8. package/src/managers/IndexManager.mjs +778 -87
  9. package/src/managers/QueryManager.mjs +340 -67
  10. package/src/managers/TermManager.mjs +7 -7
  11. package/src/utils/operatorNormalizer.mjs +116 -0
  12. package/.babelrc +0 -13
  13. package/.gitattributes +0 -2
  14. package/CHANGELOG.md +0 -140
  15. package/babel.config.json +0 -5
  16. package/docs/API.md +0 -1051
  17. package/docs/EXAMPLES.md +0 -701
  18. package/docs/README.md +0 -194
  19. package/examples/iterate-usage-example.js +0 -157
  20. package/examples/simple-iterate-example.js +0 -115
  21. package/jest.config.js +0 -24
  22. package/scripts/README.md +0 -47
  23. package/scripts/clean-test-files.js +0 -75
  24. package/scripts/prepare.js +0 -31
  25. package/scripts/run-tests.js +0 -80
  26. package/test/$not-operator-with-and.test.js +0 -282
  27. package/test/README.md +0 -8
  28. package/test/close-init-cycle.test.js +0 -256
  29. package/test/critical-bugs-fixes.test.js +0 -1069
  30. package/test/index-persistence.test.js +0 -306
  31. package/test/index-serialization.test.js +0 -314
  32. package/test/indexed-query-mode.test.js +0 -360
  33. package/test/iterate-method.test.js +0 -272
  34. package/test/query-operators.test.js +0 -238
  35. package/test/regex-array-fields.test.js +0 -129
  36. package/test/score-method.test.js +0 -238
  37. package/test/setup.js +0 -17
  38. package/test/term-mapping-minimal.test.js +0 -154
  39. package/test/term-mapping-simple.test.js +0 -257
  40. package/test/term-mapping.test.js +0 -514
  41. package/test/writebuffer-flush-resilience.test.js +0 -204
@@ -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 = {}, limit = pLimit(4)
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 limit(async () => {
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
- // Original logic for non-adjacent ranges
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
- // Extract the specific range content
311
- const rangeContent = content.substring(relativeStart, relativeEnd)
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
- // Check if file exists before trying to read it
547
- if (!await this.exists()) {
548
- return null // Return null if file doesn't exist
549
- }
550
-
551
- const reader = await fs.promises.open(this.file, 'r')
552
- try {
553
- const { size } = await reader.stat()
554
- if (size < 1) throw 'empty file'
555
- this.size = size
556
- const bufferSize = 16384
557
- let buffer, isFirstRead = true, lastReadSize, readPosition = Math.max(size - bufferSize, 0)
558
- while (readPosition >= 0) {
559
- const readSize = Math.min(bufferSize, size - readPosition)
560
- if (readSize !== lastReadSize) {
561
- lastReadSize = readSize
562
- buffer = Buffer.alloc(readSize)
563
- }
564
- const { bytesRead } = await reader.read(buffer, 0, isFirstRead ? (readSize - 1) : readSize, readPosition)
565
- if (isFirstRead) isFirstRead = false
566
- if (bytesRead === 0) break
567
- const newlineIndex = buffer.lastIndexOf(10)
568
- const start = readPosition + newlineIndex + 1
569
- if (newlineIndex !== -1) {
570
- const lastLine = Buffer.alloc(size - start)
571
- await reader.read(lastLine, 0, size - start, start)
572
- if (!lastLine || !lastLine.length) {
573
- throw 'no metadata or empty file'
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
- } catch (e) {
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
- return this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer);
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
- return this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer);
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
 
@@ -1,6 +1,6 @@
1
1
  /**
2
- * OperationQueue - Sistema de fila para operações do banco de dados
3
- * Resolve race conditions entre operações concorrentes
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
- * Adiciona uma operação à fila
24
- * @param {Function} operation - Função assíncrona a ser executada
25
- * @returns {Promise} - Promise que resolve quando a operação é concluída
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
- // Processar imediatamente se não estiver processando
51
+ // Process immediately if not already processing
52
52
  this.process().catch(reject)
53
53
  })
54
54
  }
55
55
 
56
56
  /**
57
- * Processa todas as operações na fila sequencialmente
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
- * Aguarda todas as operações pendentes serem processadas
120
- * @param {number|null} maxWaitTime - Tempo máximo de espera em ms (null = wait indefinitely)
121
- * @returns {Promise<boolean>} - true se todas foram processadas, false se timeout
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
- * Retorna o tamanho atual da fila
170
+ * Returns the current queue length
171
171
  */
172
172
  getQueueLength() {
173
173
  return this.queue.length
174
174
  }
175
175
 
176
176
  /**
177
- * Verifica se está processando operações
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
- * Retorna estatísticas da fila
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
- * Limpa a fila (para casos de emergência)
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
- * Detecta operações travadas e retorna informações detalhadas
212
- * @param {number} stuckThreshold - Tempo em ms para considerar uma operação travada
213
- * @returns {Array} - Lista de operações travadas com stack traces
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
- * Força a limpeza de operações travadas (último recurso)
229
- * @param {number} stuckThreshold - Tempo em ms para considerar uma operação travada
230
- * @returns {number} - Número de operações removidas
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
- // Rejeitar todas as operações travadas
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
- * Verifica se a fila está vazia
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
- * Retorna informações sobre a próxima operação na fila
265
+ * Returns information about the next operation in the queue
266
266
  */
267
267
  peekNext() {
268
268
  if (this.queue.length === 0) {