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.
@@ -28,10 +28,28 @@ class JSONLDatabase extends EventEmitter {
28
28
  this.filePath = filePath;
29
29
  }
30
30
 
31
+ // Enhanced configuration with intelligent defaults
31
32
  this.options = {
32
- batchSize: 100, // Batch size for inserts
33
+ // Original options
34
+ batchSize: 50, // Reduced from 100 for faster response
33
35
  create: true, // Create database if it doesn't exist (default: true)
34
36
  clear: false, // Clear database on load if not empty (default: false)
37
+
38
+ // Auto-save intelligent configuration
39
+ autoSave: true, // Enable auto-save by default
40
+ autoSaveThreshold: 50, // Flush when buffer reaches 50 records
41
+ autoSaveInterval: 5000, // Flush every 5 seconds
42
+ forceSaveOnClose: true, // Always save when closing
43
+
44
+ // Performance configuration
45
+ adaptiveBatchSize: true, // Adjust batch size based on usage
46
+ minBatchSize: 10, // Minimum batch size for flush
47
+ maxBatchSize: 200, // Maximum batch size for performance
48
+
49
+ // Memory management
50
+ maxMemoryUsage: 'auto', // Calculate automatically or use fixed value
51
+ maxFlushChunkBytes: 8 * 1024 * 1024, // 8MB default
52
+
35
53
  ...options
36
54
  };
37
55
 
@@ -40,6 +58,11 @@ class JSONLDatabase extends EventEmitter {
40
58
  this.options.create = true;
41
59
  }
42
60
 
61
+ // Auto-save timer and state
62
+ this.autoSaveTimer = null;
63
+ this.lastFlushTime = null;
64
+ this.lastAutoSaveTime = Date.now();
65
+
43
66
  this.isInitialized = false;
44
67
  this.offsets = [];
45
68
  this.indexOffset = 0;
@@ -288,11 +311,30 @@ class JSONLDatabase extends EventEmitter {
288
311
 
289
312
  // Convert back to Map objects
290
313
  for (const [field, indexMap] of Object.entries(savedIndexes)) {
291
- if (this.indexes[field]) {
314
+ // Initialize index if it doesn't exist
315
+ if (!this.indexes[field]) {
292
316
  this.indexes[field] = new Map();
293
- for (const [value, indices] of Object.entries(indexMap)) {
294
- this.indexes[field].set(value, new Set(indices));
317
+ }
318
+
319
+ this.indexes[field] = new Map();
320
+ for (const [value, indices] of Object.entries(indexMap)) {
321
+ // Convert value back to original type based on field configuration
322
+ let convertedValue = value;
323
+ if (this.indexes[field] && this.indexes[field].constructor === Map) {
324
+ // Try to convert based on field type
325
+ if (field === 'id' || field.includes('id') || field.includes('Id')) {
326
+ convertedValue = parseInt(value, 10);
327
+ } else if (typeof value === 'string' && !isNaN(parseFloat(value))) {
328
+ // Try to convert numeric strings back to numbers
329
+ const num = parseFloat(value);
330
+ if (Number.isInteger(num)) {
331
+ convertedValue = parseInt(value, 10);
332
+ } else {
333
+ convertedValue = num;
334
+ }
335
+ }
295
336
  }
337
+ this.indexes[field].set(convertedValue, new Set(indices));
296
338
  }
297
339
  }
298
340
 
@@ -354,7 +396,7 @@ class JSONLDatabase extends EventEmitter {
354
396
  }
355
397
  }
356
398
 
357
- // ORIGINAL STRATEGY: Buffer in memory + batch write
399
+ // ORIGINAL STRATEGY: Buffer in memory + batch write with intelligent auto-save
358
400
  async insert(data) {
359
401
  if (!this.isInitialized) {
360
402
  throw new Error('Database not initialized');
@@ -378,25 +420,80 @@ class JSONLDatabase extends EventEmitter {
378
420
  // Add to index immediately for searchability
379
421
  this.addToIndex(record, this.recordCount - 1);
380
422
 
381
- // Flush buffer if it's full (BATCH WRITE) or if autoSave is enabled
382
- if (this.insertionBuffer.length >= this.insertionStats.batchSize || this.options.autoSave) {
383
- await this.flushInsertionBuffer();
423
+ // Intelligent auto-save logic
424
+ if (this.options.autoSave) {
425
+ // Auto-save based on threshold
426
+ if (this.insertionBuffer.length >= this.options.autoSaveThreshold) {
427
+ await this.flush();
428
+ this.emit('buffer-full');
429
+ }
430
+
431
+ // Auto-save based on time interval
432
+ if (!this.autoSaveTimer) {
433
+ this.autoSaveTimer = setTimeout(async () => {
434
+ if (this.insertionBuffer.length > 0) {
435
+ await this.flush();
436
+ this.emit('auto-save-timer');
437
+ }
438
+ this.autoSaveTimer = null;
439
+ }, this.options.autoSaveInterval);
440
+ }
441
+ } else {
442
+ // Manual mode: flush only when buffer is full
443
+ if (this.insertionBuffer.length >= this.insertionStats.batchSize) {
444
+ await this.flushInsertionBuffer();
445
+ }
384
446
  }
385
447
 
386
448
  this.shouldSave = true;
387
449
 
388
- // Save immediately if autoSave is enabled
389
- if (this.options.autoSave && this.shouldSave) {
390
- await this.save();
391
- }
392
-
393
450
  // Emit insert event
394
451
  this.emit('insert', record, this.recordCount - 1);
395
452
 
396
453
  return record; // Return immediately (ORIGINAL STRATEGY)
397
454
  }
398
455
 
399
- // ULTRA-OPTIMIZED STRATEGY: Bulk flush with minimal I/O
456
+ // PUBLIC METHOD: Flush buffer to disk
457
+ async flush() {
458
+ if (!this.isInitialized) {
459
+ throw new Error('Database not initialized');
460
+ }
461
+
462
+ if (this.insertionBuffer.length > 0) {
463
+ const flushCount = this.insertionBuffer.length;
464
+ await this.flushInsertionBuffer();
465
+ this.lastFlushTime = Date.now();
466
+ this.emit('buffer-flush', flushCount);
467
+ return flushCount;
468
+ }
469
+ return 0;
470
+ }
471
+
472
+ // PUBLIC METHOD: Force save - always saves regardless of buffer size
473
+ async forceSave() {
474
+ if (!this.isInitialized) {
475
+ throw new Error('Database not initialized');
476
+ }
477
+
478
+ await this.flush();
479
+ await this.save();
480
+ this.emit('save-complete');
481
+ }
482
+
483
+ // PUBLIC METHOD: Get buffer status information
484
+ getBufferStatus() {
485
+ return {
486
+ pendingCount: this.insertionBuffer.length,
487
+ bufferSize: this.options.batchSize,
488
+ lastFlush: this.lastFlushTime,
489
+ lastAutoSave: this.lastAutoSaveTime,
490
+ shouldFlush: this.insertionBuffer.length >= this.options.autoSaveThreshold,
491
+ autoSaveEnabled: this.options.autoSave,
492
+ autoSaveTimer: this.autoSaveTimer ? 'active' : 'inactive'
493
+ };
494
+ }
495
+
496
+ // ULTRA-OPTIMIZED STRATEGY: Bulk flush with minimal I/O (chunked to avoid OOM)
400
497
  async flushInsertionBuffer() {
401
498
  if (this.insertionBuffer.length === 0) {
402
499
  return;
@@ -413,45 +510,54 @@ class JSONLDatabase extends EventEmitter {
413
510
  currentOffset = 0;
414
511
  }
415
512
 
416
- // Pre-allocate arrays for better performance
417
- const offsets = new Array(this.insertionBuffer.length);
418
- const lines = new Array(this.insertionBuffer.length);
419
-
420
- // Batch process all records
513
+ // Write in chunks to avoid allocating a huge buffer/string at once
514
+ const maxChunkBytes = this.options.maxFlushChunkBytes || 8 * 1024 * 1024; // 8MB default
515
+ let chunkParts = [];
516
+ let chunkBytes = 0;
517
+
518
+ // We'll push offsets directly to avoid creating a separate large array
519
+ const pendingOffsets = [];
520
+
421
521
  for (let i = 0; i < this.insertionBuffer.length; i++) {
422
522
  const record = this.insertionBuffer[i];
423
-
424
- // Records are already indexed in insert/insertMany methods
425
- // No need to index again here
426
-
427
- // Serialize record (batch operation)
428
523
  const line = JSON.stringify(record) + '\n';
429
- lines[i] = line;
430
-
431
- // Calculate accurate offset (batch operation)
432
- offsets[i] = currentOffset;
433
- currentOffset += Buffer.byteLength(line, 'utf8');
524
+ const lineBytes = Buffer.byteLength(line, 'utf8');
525
+
526
+ // Track offset for this record
527
+ pendingOffsets.push(currentOffset);
528
+ currentOffset += lineBytes;
529
+
530
+ // If one line is larger than chunk size, write the current chunk and then this line alone
531
+ if (lineBytes > maxChunkBytes) {
532
+ if (chunkParts.length > 0) {
533
+ await fs.appendFile(this.filePath, chunkParts.join(''));
534
+ chunkParts.length = 0;
535
+ chunkBytes = 0;
536
+ }
537
+ await fs.appendFile(this.filePath, line);
538
+ continue;
539
+ }
540
+
541
+ // If adding this line would exceed the chunk size, flush current chunk first
542
+ if (chunkBytes + lineBytes > maxChunkBytes) {
543
+ await fs.appendFile(this.filePath, chunkParts.join(''));
544
+ chunkParts.length = 0;
545
+ chunkBytes = 0;
546
+ }
547
+
548
+ chunkParts.push(line);
549
+ chunkBytes += lineBytes;
434
550
  }
435
551
 
436
- // Single string concatenation (much faster than Buffer.concat)
437
- const batchString = lines.join('');
438
- const batchBuffer = Buffer.from(batchString, 'utf8');
439
-
440
- // Single file write operation
441
- await fs.appendFile(this.filePath, batchBuffer);
442
-
443
- // Batch update offsets
444
- this.offsets.push(...offsets);
445
-
446
- // Record count is already updated in insert/insertMany methods
447
- // No need to update it again here
448
-
449
- // Clear the insertion buffer
552
+ if (chunkParts.length > 0) {
553
+ await fs.appendFile(this.filePath, chunkParts.join(''));
554
+ }
555
+
556
+ // Update offsets and clear buffer
557
+ this.offsets.push(...pendingOffsets);
450
558
  this.insertionBuffer.length = 0;
451
-
452
- // Mark that we need to save (offset line will be added by save() method)
453
- this.shouldSave = true;
454
-
559
+ this.shouldSave = true; // Mark that we need to save (offset line will be added by save())
560
+
455
561
  } catch (error) {
456
562
  console.error('Error flushing insertion buffer:', error);
457
563
  throw new Error(`Failed to flush insertion buffer: ${error.message}`);
@@ -478,19 +584,21 @@ class JSONLDatabase extends EventEmitter {
478
584
  matchingIndices = this.queryIndex(indexedCriteria);
479
585
  }
480
586
 
481
- // If no indexed fields or no matches found, start with all records
482
- if (matchingIndices.length === 0) {
587
+ // If no indexed fields, start with all records
588
+ if (indexedFields.length === 0) {
483
589
  matchingIndices = Array.from({ length: this.recordCount }, (_, i) => i);
590
+ } else if (matchingIndices.length === 0) {
591
+ // If we have indexed fields but no matches, return empty array
592
+ return [];
484
593
  }
485
594
 
486
595
  if (matchingIndices.length === 0) {
487
596
  return [];
488
597
  }
489
598
 
490
- // Step 2: Collect results from both disk and buffer
599
+ // Step 2: Collect results from disk (existing records)
491
600
  const results = [];
492
601
 
493
- // First, get results from disk (existing records)
494
602
  for (const index of matchingIndices) {
495
603
  if (index < this.offsets.length) {
496
604
  const offset = this.offsets[index];
@@ -507,54 +615,16 @@ class JSONLDatabase extends EventEmitter {
507
615
  }
508
616
  }
509
617
 
510
- // Then, get results from buffer (new records) - only include records that match the indexed criteria
511
- const bufferIndices = new Set();
512
- if (indexedFields.length > 0) {
513
- // Use the same queryIndex logic for buffer records
514
- for (const [field, fieldCriteria] of Object.entries(indexedFields.reduce((acc, field) => {
515
- acc[field] = criteria[field];
516
- return acc;
517
- }, {}))) {
518
- const indexMap = this.indexes[field];
519
- if (indexMap) {
520
- if (typeof fieldCriteria === 'object' && !Array.isArray(fieldCriteria)) {
521
- // Handle operators like 'in'
522
- for (const [operator, operatorValue] of Object.entries(fieldCriteria)) {
523
- if (operator === 'in' && Array.isArray(operatorValue)) {
524
- for (const searchValue of operatorValue) {
525
- const indexSet = indexMap.get(searchValue);
526
- if (indexSet) {
527
- for (const index of indexSet) {
528
- if (index >= this.recordCount - this.insertionBuffer.length) {
529
- bufferIndices.add(index);
530
- }
531
- }
532
- }
533
- }
534
- }
535
- }
536
- }
537
- }
538
- }
539
- } else {
540
- // No indexed fields, include all buffer records
618
+ // Step 3: Add results from buffer (new records) if buffer is not empty
619
+ if (this.insertionBuffer.length > 0) {
620
+ // Check each buffer record against criteria
541
621
  for (let i = 0; i < this.insertionBuffer.length; i++) {
542
- bufferIndices.add(this.recordCount - this.insertionBuffer.length + i);
543
- }
544
- }
545
-
546
- // Add matching buffer records
547
- for (const bufferIndex of bufferIndices) {
548
- const bufferOffset = bufferIndex - (this.recordCount - this.insertionBuffer.length);
549
- if (bufferOffset >= 0 && bufferOffset < this.insertionBuffer.length) {
550
- const record = this.insertionBuffer[bufferOffset];
551
-
552
- // Check non-indexed fields
553
- if (nonIndexedFields.length === 0 || this.matchesCriteria(record, nonIndexedFields.reduce((acc, field) => {
554
- acc[field] = criteria[field];
555
- return acc;
556
- }, {}))) {
557
- results.push(record);
622
+ const record = this.insertionBuffer[i];
623
+ if (record && !record._deleted) {
624
+ // Check if record matches all criteria
625
+ if (this.matchesCriteria(record, criteria)) {
626
+ results.push(record);
627
+ }
558
628
  }
559
629
  }
560
630
  }
@@ -870,19 +940,32 @@ class JSONLDatabase extends EventEmitter {
870
940
  }
871
941
 
872
942
  async close() {
943
+ // Clear auto-save timer
944
+ if (this.autoSaveTimer) {
945
+ clearTimeout(this.autoSaveTimer);
946
+ this.autoSaveTimer = null;
947
+ }
948
+
873
949
  // Flush any pending inserts first
874
950
  if (this.insertionBuffer.length > 0) {
875
- await this.flushInsertionBuffer();
951
+ await this.flush();
876
952
  }
877
953
 
878
- if (this.shouldSave) {
954
+ // Force save on close if enabled
955
+ if (this.options.forceSaveOnClose && this.shouldSave) {
956
+ await this.save();
957
+ this.emit('close-save-complete');
958
+ } else if (this.shouldSave) {
879
959
  await this.save();
880
960
  }
961
+
881
962
  if (this.fileHandle) {
882
963
  await this.fileHandle.close();
883
964
  this.fileHandle = null;
884
965
  }
966
+
885
967
  this.isInitialized = false;
968
+ this.emit('close');
886
969
  }
887
970
 
888
971
  get length() {
@@ -899,7 +982,16 @@ class JSONLDatabase extends EventEmitter {
899
982
  memoryUsage: 0, // No buffer in memory - on-demand reading
900
983
  fileHandle: this.fileHandle ? 'open' : 'closed',
901
984
  insertionBufferSize: this.insertionBuffer.length,
902
- batchSize: this.insertionStats.batchSize
985
+ batchSize: this.insertionStats.batchSize,
986
+ // Auto-save information
987
+ autoSave: {
988
+ enabled: this.options.autoSave,
989
+ threshold: this.options.autoSaveThreshold,
990
+ interval: this.options.autoSaveInterval,
991
+ timerActive: this.autoSaveTimer ? true : false,
992
+ lastFlush: this.lastFlushTime,
993
+ lastAutoSave: this.lastAutoSaveTime
994
+ }
903
995
  };
904
996
  }
905
997
 
@@ -910,6 +1002,37 @@ class JSONLDatabase extends EventEmitter {
910
1002
  };
911
1003
  }
912
1004
 
1005
+ // PUBLIC METHOD: Configure performance settings
1006
+ configurePerformance(settings) {
1007
+ if (settings.batchSize !== undefined) {
1008
+ this.options.batchSize = Math.max(this.options.minBatchSize,
1009
+ Math.min(this.options.maxBatchSize, settings.batchSize));
1010
+ this.insertionStats.batchSize = this.options.batchSize;
1011
+ }
1012
+
1013
+ if (settings.autoSaveThreshold !== undefined) {
1014
+ this.options.autoSaveThreshold = settings.autoSaveThreshold;
1015
+ }
1016
+
1017
+ if (settings.autoSaveInterval !== undefined) {
1018
+ this.options.autoSaveInterval = settings.autoSaveInterval;
1019
+ }
1020
+
1021
+ this.emit('performance-configured', this.options);
1022
+ }
1023
+
1024
+ // PUBLIC METHOD: Get performance configuration
1025
+ getPerformanceConfig() {
1026
+ return {
1027
+ batchSize: this.options.batchSize,
1028
+ autoSaveThreshold: this.options.autoSaveThreshold,
1029
+ autoSaveInterval: this.options.autoSaveInterval,
1030
+ adaptiveBatchSize: this.options.adaptiveBatchSize,
1031
+ minBatchSize: this.options.minBatchSize,
1032
+ maxBatchSize: this.options.maxBatchSize
1033
+ };
1034
+ }
1035
+
913
1036
  /**
914
1037
  * Compatibility method: readColumnIndex - gets unique values from indexed columns only
915
1038
  * Maintains compatibility with JexiDB v1 code
@@ -1053,11 +1176,31 @@ class JSONLDatabase extends EventEmitter {
1053
1176
  }
1054
1177
 
1055
1178
  async destroy() {
1179
+ // destroy() agora é equivalente a close() - fecha instância, mantém arquivo
1056
1180
  await this.close();
1057
- await fs.unlink(this.filePath);
1058
1181
  this.emit('destroy');
1059
1182
  }
1060
1183
 
1184
+ async deleteDatabase() {
1185
+ await this.close();
1186
+ await fs.unlink(this.filePath);
1187
+
1188
+ // Also remove index file if it exists
1189
+ try {
1190
+ const indexPath = this.filePath.replace('.jdb', '.idx.jdb');
1191
+ await fs.unlink(indexPath);
1192
+ } catch (e) {
1193
+ // Index file might not exist
1194
+ }
1195
+
1196
+ this.emit('delete-database');
1197
+ }
1198
+
1199
+ // Alias for deleteDatabase
1200
+ async removeDatabase() {
1201
+ return this.deleteDatabase();
1202
+ }
1203
+
1061
1204
  async findOne(criteria = {}) {
1062
1205
  const results = await this.find(criteria);
1063
1206
  return results.length > 0 ? results[0] : null;