jexidb 2.0.2 → 2.0.3

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.
@@ -33,13 +33,41 @@ class JSONLDatabase extends _events.EventEmitter {
33
33
  } else {
34
34
  this.filePath = filePath;
35
35
  }
36
+
37
+ // Enhanced configuration with intelligent defaults
36
38
  this.options = {
37
- batchSize: 100,
38
- // Batch size for inserts
39
+ // Original options
40
+ batchSize: 50,
41
+ // Reduced from 100 for faster response
39
42
  create: true,
40
43
  // Create database if it doesn't exist (default: true)
41
44
  clear: false,
42
45
  // Clear database on load if not empty (default: false)
46
+
47
+ // Auto-save intelligent configuration
48
+ autoSave: true,
49
+ // Enable auto-save by default
50
+ autoSaveThreshold: 50,
51
+ // Flush when buffer reaches 50 records
52
+ autoSaveInterval: 5000,
53
+ // Flush every 5 seconds
54
+ forceSaveOnClose: true,
55
+ // Always save when closing
56
+
57
+ // Performance configuration
58
+ adaptiveBatchSize: true,
59
+ // Adjust batch size based on usage
60
+ minBatchSize: 10,
61
+ // Minimum batch size for flush
62
+ maxBatchSize: 200,
63
+ // Maximum batch size for performance
64
+
65
+ // Memory management
66
+ maxMemoryUsage: 'auto',
67
+ // Calculate automatically or use fixed value
68
+ maxFlushChunkBytes: 8 * 1024 * 1024,
69
+ // 8MB default
70
+
43
71
  ...options
44
72
  };
45
73
 
@@ -47,6 +75,11 @@ class JSONLDatabase extends _events.EventEmitter {
47
75
  if (this.options.clear === true) {
48
76
  this.options.create = true;
49
77
  }
78
+
79
+ // Auto-save timer and state
80
+ this.autoSaveTimer = null;
81
+ this.lastFlushTime = null;
82
+ this.lastAutoSaveTime = Date.now();
50
83
  this.isInitialized = false;
51
84
  this.offsets = [];
52
85
  this.indexOffset = 0;
@@ -278,11 +311,29 @@ class JSONLDatabase extends _events.EventEmitter {
278
311
 
279
312
  // Convert back to Map objects
280
313
  for (const [field, indexMap] of Object.entries(savedIndexes)) {
281
- if (this.indexes[field]) {
314
+ // Initialize index if it doesn't exist
315
+ if (!this.indexes[field]) {
282
316
  this.indexes[field] = new Map();
283
- for (const [value, indices] of Object.entries(indexMap)) {
284
- this.indexes[field].set(value, new Set(indices));
317
+ }
318
+ this.indexes[field] = new Map();
319
+ for (const [value, indices] of Object.entries(indexMap)) {
320
+ // Convert value back to original type based on field configuration
321
+ let convertedValue = value;
322
+ if (this.indexes[field] && this.indexes[field].constructor === Map) {
323
+ // Try to convert based on field type
324
+ if (field === 'id' || field.includes('id') || field.includes('Id')) {
325
+ convertedValue = parseInt(value, 10);
326
+ } else if (typeof value === 'string' && !isNaN(parseFloat(value))) {
327
+ // Try to convert numeric strings back to numbers
328
+ const num = parseFloat(value);
329
+ if (Number.isInteger(num)) {
330
+ convertedValue = parseInt(value, 10);
331
+ } else {
332
+ convertedValue = num;
333
+ }
334
+ }
285
335
  }
336
+ this.indexes[field].set(convertedValue, new Set(indices));
286
337
  }
287
338
  }
288
339
  return true;
@@ -340,7 +391,7 @@ class JSONLDatabase extends _events.EventEmitter {
340
391
  }
341
392
  }
342
393
 
343
- // ORIGINAL STRATEGY: Buffer in memory + batch write
394
+ // ORIGINAL STRATEGY: Buffer in memory + batch write with intelligent auto-save
344
395
  async insert(data) {
345
396
  if (!this.isInitialized) {
346
397
  throw new Error('Database not initialized');
@@ -363,23 +414,76 @@ class JSONLDatabase extends _events.EventEmitter {
363
414
  // Add to index immediately for searchability
364
415
  this.addToIndex(record, this.recordCount - 1);
365
416
 
366
- // Flush buffer if it's full (BATCH WRITE) or if autoSave is enabled
367
- if (this.insertionBuffer.length >= this.insertionStats.batchSize || this.options.autoSave) {
368
- await this.flushInsertionBuffer();
369
- }
370
- this.shouldSave = true;
417
+ // Intelligent auto-save logic
418
+ if (this.options.autoSave) {
419
+ // Auto-save based on threshold
420
+ if (this.insertionBuffer.length >= this.options.autoSaveThreshold) {
421
+ await this.flush();
422
+ this.emit('buffer-full');
423
+ }
371
424
 
372
- // Save immediately if autoSave is enabled
373
- if (this.options.autoSave && this.shouldSave) {
374
- await this.save();
425
+ // Auto-save based on time interval
426
+ if (!this.autoSaveTimer) {
427
+ this.autoSaveTimer = setTimeout(async () => {
428
+ if (this.insertionBuffer.length > 0) {
429
+ await this.flush();
430
+ this.emit('auto-save-timer');
431
+ }
432
+ this.autoSaveTimer = null;
433
+ }, this.options.autoSaveInterval);
434
+ }
435
+ } else {
436
+ // Manual mode: flush only when buffer is full
437
+ if (this.insertionBuffer.length >= this.insertionStats.batchSize) {
438
+ await this.flushInsertionBuffer();
439
+ }
375
440
  }
441
+ this.shouldSave = true;
376
442
 
377
443
  // Emit insert event
378
444
  this.emit('insert', record, this.recordCount - 1);
379
445
  return record; // Return immediately (ORIGINAL STRATEGY)
380
446
  }
381
447
 
382
- // ULTRA-OPTIMIZED STRATEGY: Bulk flush with minimal I/O
448
+ // PUBLIC METHOD: Flush buffer to disk
449
+ async flush() {
450
+ if (!this.isInitialized) {
451
+ throw new Error('Database not initialized');
452
+ }
453
+ if (this.insertionBuffer.length > 0) {
454
+ const flushCount = this.insertionBuffer.length;
455
+ await this.flushInsertionBuffer();
456
+ this.lastFlushTime = Date.now();
457
+ this.emit('buffer-flush', flushCount);
458
+ return flushCount;
459
+ }
460
+ return 0;
461
+ }
462
+
463
+ // PUBLIC METHOD: Force save - always saves regardless of buffer size
464
+ async forceSave() {
465
+ if (!this.isInitialized) {
466
+ throw new Error('Database not initialized');
467
+ }
468
+ await this.flush();
469
+ await this.save();
470
+ this.emit('save-complete');
471
+ }
472
+
473
+ // PUBLIC METHOD: Get buffer status information
474
+ getBufferStatus() {
475
+ return {
476
+ pendingCount: this.insertionBuffer.length,
477
+ bufferSize: this.options.batchSize,
478
+ lastFlush: this.lastFlushTime,
479
+ lastAutoSave: this.lastAutoSaveTime,
480
+ shouldFlush: this.insertionBuffer.length >= this.options.autoSaveThreshold,
481
+ autoSaveEnabled: this.options.autoSave,
482
+ autoSaveTimer: this.autoSaveTimer ? 'active' : 'inactive'
483
+ };
484
+ }
485
+
486
+ // ULTRA-OPTIMIZED STRATEGY: Bulk flush with minimal I/O (chunked to avoid OOM)
383
487
  async flushInsertionBuffer() {
384
488
  if (this.insertionBuffer.length === 0) {
385
489
  return;
@@ -395,44 +499,50 @@ class JSONLDatabase extends _events.EventEmitter {
395
499
  currentOffset = 0;
396
500
  }
397
501
 
398
- // Pre-allocate arrays for better performance
399
- const offsets = new Array(this.insertionBuffer.length);
400
- const lines = new Array(this.insertionBuffer.length);
502
+ // Write in chunks to avoid allocating a huge buffer/string at once
503
+ const maxChunkBytes = this.options.maxFlushChunkBytes || 8 * 1024 * 1024; // 8MB default
504
+ let chunkParts = [];
505
+ let chunkBytes = 0;
401
506
 
402
- // Batch process all records
507
+ // We'll push offsets directly to avoid creating a separate large array
508
+ const pendingOffsets = [];
403
509
  for (let i = 0; i < this.insertionBuffer.length; i++) {
404
510
  const record = this.insertionBuffer[i];
405
-
406
- // Records are already indexed in insert/insertMany methods
407
- // No need to index again here
408
-
409
- // Serialize record (batch operation)
410
511
  const line = JSON.stringify(record) + '\n';
411
- lines[i] = line;
512
+ const lineBytes = Buffer.byteLength(line, 'utf8');
513
+
514
+ // Track offset for this record
515
+ pendingOffsets.push(currentOffset);
516
+ currentOffset += lineBytes;
517
+
518
+ // If one line is larger than chunk size, write the current chunk and then this line alone
519
+ if (lineBytes > maxChunkBytes) {
520
+ if (chunkParts.length > 0) {
521
+ await _fs.promises.appendFile(this.filePath, chunkParts.join(''));
522
+ chunkParts.length = 0;
523
+ chunkBytes = 0;
524
+ }
525
+ await _fs.promises.appendFile(this.filePath, line);
526
+ continue;
527
+ }
412
528
 
413
- // Calculate accurate offset (batch operation)
414
- offsets[i] = currentOffset;
415
- currentOffset += Buffer.byteLength(line, 'utf8');
529
+ // If adding this line would exceed the chunk size, flush current chunk first
530
+ if (chunkBytes + lineBytes > maxChunkBytes) {
531
+ await _fs.promises.appendFile(this.filePath, chunkParts.join(''));
532
+ chunkParts.length = 0;
533
+ chunkBytes = 0;
534
+ }
535
+ chunkParts.push(line);
536
+ chunkBytes += lineBytes;
537
+ }
538
+ if (chunkParts.length > 0) {
539
+ await _fs.promises.appendFile(this.filePath, chunkParts.join(''));
416
540
  }
417
541
 
418
- // Single string concatenation (much faster than Buffer.concat)
419
- const batchString = lines.join('');
420
- const batchBuffer = Buffer.from(batchString, 'utf8');
421
-
422
- // Single file write operation
423
- await _fs.promises.appendFile(this.filePath, batchBuffer);
424
-
425
- // Batch update offsets
426
- this.offsets.push(...offsets);
427
-
428
- // Record count is already updated in insert/insertMany methods
429
- // No need to update it again here
430
-
431
- // Clear the insertion buffer
542
+ // Update offsets and clear buffer
543
+ this.offsets.push(...pendingOffsets);
432
544
  this.insertionBuffer.length = 0;
433
-
434
- // Mark that we need to save (offset line will be added by save() method)
435
- this.shouldSave = true;
545
+ this.shouldSave = true; // Mark that we need to save (offset line will be added by save())
436
546
  } catch (error) {
437
547
  console.error('Error flushing insertion buffer:', error);
438
548
  throw new Error(`Failed to flush insertion buffer: ${error.message}`);
@@ -459,20 +569,21 @@ class JSONLDatabase extends _events.EventEmitter {
459
569
  matchingIndices = this.queryIndex(indexedCriteria);
460
570
  }
461
571
 
462
- // If no indexed fields or no matches found, start with all records
463
- if (matchingIndices.length === 0) {
572
+ // If no indexed fields, start with all records
573
+ if (indexedFields.length === 0) {
464
574
  matchingIndices = Array.from({
465
575
  length: this.recordCount
466
576
  }, (_, i) => i);
577
+ } else if (matchingIndices.length === 0) {
578
+ // If we have indexed fields but no matches, return empty array
579
+ return [];
467
580
  }
468
581
  if (matchingIndices.length === 0) {
469
582
  return [];
470
583
  }
471
584
 
472
- // Step 2: Collect results from both disk and buffer
585
+ // Step 2: Collect results from disk (existing records)
473
586
  const results = [];
474
-
475
- // First, get results from disk (existing records)
476
587
  for (const index of matchingIndices) {
477
588
  if (index < this.offsets.length) {
478
589
  const offset = this.offsets[index];
@@ -489,54 +600,16 @@ class JSONLDatabase extends _events.EventEmitter {
489
600
  }
490
601
  }
491
602
 
492
- // Then, get results from buffer (new records) - only include records that match the indexed criteria
493
- const bufferIndices = new Set();
494
- if (indexedFields.length > 0) {
495
- // Use the same queryIndex logic for buffer records
496
- for (const [field, fieldCriteria] of Object.entries(indexedFields.reduce((acc, field) => {
497
- acc[field] = criteria[field];
498
- return acc;
499
- }, {}))) {
500
- const indexMap = this.indexes[field];
501
- if (indexMap) {
502
- if (typeof fieldCriteria === 'object' && !Array.isArray(fieldCriteria)) {
503
- // Handle operators like 'in'
504
- for (const [operator, operatorValue] of Object.entries(fieldCriteria)) {
505
- if (operator === 'in' && Array.isArray(operatorValue)) {
506
- for (const searchValue of operatorValue) {
507
- const indexSet = indexMap.get(searchValue);
508
- if (indexSet) {
509
- for (const index of indexSet) {
510
- if (index >= this.recordCount - this.insertionBuffer.length) {
511
- bufferIndices.add(index);
512
- }
513
- }
514
- }
515
- }
516
- }
517
- }
518
- }
519
- }
520
- }
521
- } else {
522
- // No indexed fields, include all buffer records
603
+ // Step 3: Add results from buffer (new records) if buffer is not empty
604
+ if (this.insertionBuffer.length > 0) {
605
+ // Check each buffer record against criteria
523
606
  for (let i = 0; i < this.insertionBuffer.length; i++) {
524
- bufferIndices.add(this.recordCount - this.insertionBuffer.length + i);
525
- }
526
- }
527
-
528
- // Add matching buffer records
529
- for (const bufferIndex of bufferIndices) {
530
- const bufferOffset = bufferIndex - (this.recordCount - this.insertionBuffer.length);
531
- if (bufferOffset >= 0 && bufferOffset < this.insertionBuffer.length) {
532
- const record = this.insertionBuffer[bufferOffset];
533
-
534
- // Check non-indexed fields
535
- if (nonIndexedFields.length === 0 || this.matchesCriteria(record, nonIndexedFields.reduce((acc, field) => {
536
- acc[field] = criteria[field];
537
- return acc;
538
- }, {}))) {
539
- results.push(record);
607
+ const record = this.insertionBuffer[i];
608
+ if (record && !record._deleted) {
609
+ // Check if record matches all criteria
610
+ if (this.matchesCriteria(record, criteria)) {
611
+ results.push(record);
612
+ }
540
613
  }
541
614
  }
542
615
  }
@@ -830,11 +903,22 @@ class JSONLDatabase extends _events.EventEmitter {
830
903
  this.shouldSave = false;
831
904
  }
832
905
  async close() {
906
+ // Clear auto-save timer
907
+ if (this.autoSaveTimer) {
908
+ clearTimeout(this.autoSaveTimer);
909
+ this.autoSaveTimer = null;
910
+ }
911
+
833
912
  // Flush any pending inserts first
834
913
  if (this.insertionBuffer.length > 0) {
835
- await this.flushInsertionBuffer();
914
+ await this.flush();
836
915
  }
837
- if (this.shouldSave) {
916
+
917
+ // Force save on close if enabled
918
+ if (this.options.forceSaveOnClose && this.shouldSave) {
919
+ await this.save();
920
+ this.emit('close-save-complete');
921
+ } else if (this.shouldSave) {
838
922
  await this.save();
839
923
  }
840
924
  if (this.fileHandle) {
@@ -842,6 +926,7 @@ class JSONLDatabase extends _events.EventEmitter {
842
926
  this.fileHandle = null;
843
927
  }
844
928
  this.isInitialized = false;
929
+ this.emit('close');
845
930
  }
846
931
  get length() {
847
932
  return this.recordCount;
@@ -857,7 +942,16 @@ class JSONLDatabase extends _events.EventEmitter {
857
942
  // No buffer in memory - on-demand reading
858
943
  fileHandle: this.fileHandle ? 'open' : 'closed',
859
944
  insertionBufferSize: this.insertionBuffer.length,
860
- batchSize: this.insertionStats.batchSize
945
+ batchSize: this.insertionStats.batchSize,
946
+ // Auto-save information
947
+ autoSave: {
948
+ enabled: this.options.autoSave,
949
+ threshold: this.options.autoSaveThreshold,
950
+ interval: this.options.autoSaveInterval,
951
+ timerActive: this.autoSaveTimer ? true : false,
952
+ lastFlush: this.lastFlushTime,
953
+ lastAutoSave: this.lastAutoSaveTime
954
+ }
861
955
  };
862
956
  }
863
957
  get indexStats() {
@@ -867,6 +961,33 @@ class JSONLDatabase extends _events.EventEmitter {
867
961
  };
868
962
  }
869
963
 
964
+ // PUBLIC METHOD: Configure performance settings
965
+ configurePerformance(settings) {
966
+ if (settings.batchSize !== undefined) {
967
+ this.options.batchSize = Math.max(this.options.minBatchSize, Math.min(this.options.maxBatchSize, settings.batchSize));
968
+ this.insertionStats.batchSize = this.options.batchSize;
969
+ }
970
+ if (settings.autoSaveThreshold !== undefined) {
971
+ this.options.autoSaveThreshold = settings.autoSaveThreshold;
972
+ }
973
+ if (settings.autoSaveInterval !== undefined) {
974
+ this.options.autoSaveInterval = settings.autoSaveInterval;
975
+ }
976
+ this.emit('performance-configured', this.options);
977
+ }
978
+
979
+ // PUBLIC METHOD: Get performance configuration
980
+ getPerformanceConfig() {
981
+ return {
982
+ batchSize: this.options.batchSize,
983
+ autoSaveThreshold: this.options.autoSaveThreshold,
984
+ autoSaveInterval: this.options.autoSaveInterval,
985
+ adaptiveBatchSize: this.options.adaptiveBatchSize,
986
+ minBatchSize: this.options.minBatchSize,
987
+ maxBatchSize: this.options.maxBatchSize
988
+ };
989
+ }
990
+
870
991
  /**
871
992
  * Compatibility method: readColumnIndex - gets unique values from indexed columns only
872
993
  * Maintains compatibility with JexiDB v1 code
@@ -1002,10 +1123,28 @@ class JSONLDatabase extends _events.EventEmitter {
1002
1123
  }
1003
1124
  }
1004
1125
  async destroy() {
1126
+ // destroy() agora é equivalente a close() - fecha instância, mantém arquivo
1005
1127
  await this.close();
1006
- await _fs.promises.unlink(this.filePath);
1007
1128
  this.emit('destroy');
1008
1129
  }
1130
+ async deleteDatabase() {
1131
+ await this.close();
1132
+ await _fs.promises.unlink(this.filePath);
1133
+
1134
+ // Also remove index file if it exists
1135
+ try {
1136
+ const indexPath = this.filePath.replace('.jdb', '.idx.jdb');
1137
+ await _fs.promises.unlink(indexPath);
1138
+ } catch (e) {
1139
+ // Index file might not exist
1140
+ }
1141
+ this.emit('delete-database');
1142
+ }
1143
+
1144
+ // Alias for deleteDatabase
1145
+ async removeDatabase() {
1146
+ return this.deleteDatabase();
1147
+ }
1009
1148
  async findOne(criteria = {}) {
1010
1149
  const results = await this.find(criteria);
1011
1150
  return results.length > 0 ? results[0] : null;