jexidb 2.1.3 → 2.1.5

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/src/Database.mjs CHANGED
@@ -403,7 +403,19 @@ class Database extends EventEmitter {
403
403
  saveTime: 0,
404
404
  loadTime: 0
405
405
  }
406
-
406
+
407
+ // Initialize integrity correction tracking
408
+ this.integrityCorrections = {
409
+ indexSync: 0, // index.totalLines vs offsets.length corrections
410
+ indexInconsistency: 0, // Index record count vs offsets mismatch
411
+ writeBufferForced: 0, // WriteBuffer not cleared after save
412
+ indexSaveFailures: 0, // Failed to save index data
413
+ dataIntegrity: 0, // General data integrity issues
414
+ utf8Recovery: 0, // UTF-8 decoding failures recovered
415
+ jsonRecovery: 0 // JSON parsing failures recovered
416
+ }
417
+
418
+
407
419
  // Initialize usage stats for QueryManager
408
420
  this.usageStats = {
409
421
  totalQueries: 0,
@@ -596,7 +608,9 @@ class Database extends EventEmitter {
596
608
  }
597
609
  }
598
610
  if (arrayStringFields.length > 0) {
599
- console.warn(`⚠️ Warning: The following array:string indexed fields were not added to term mapping: ${arrayStringFields.join(', ')}. This may impact performance.`)
611
+ if (this.opts.debugMode) {
612
+ console.warn(`⚠️ Warning: The following array:string indexed fields were not added to term mapping: ${arrayStringFields.join(', ')}. This may impact performance.`)
613
+ }
600
614
  }
601
615
  }
602
616
 
@@ -638,22 +652,27 @@ class Database extends EventEmitter {
638
652
  }
639
653
 
640
654
  /**
641
- * Get term mapping fields from indexes (auto-detected)
655
+ * Get term mapping fields from configuration or indexes (auto-detected)
642
656
  * @returns {string[]} Array of field names that use term mapping
643
657
  */
644
658
  getTermMappingFields() {
659
+ // If termMappingFields is explicitly configured, use it
660
+ if (this.opts.termMappingFields && Array.isArray(this.opts.termMappingFields)) {
661
+ return [...this.opts.termMappingFields]
662
+ }
663
+
664
+ // Auto-detect fields that benefit from term mapping from indexes
645
665
  if (!this.opts.indexes) return []
646
-
647
- // Auto-detect fields that benefit from term mapping
666
+
648
667
  const termMappingFields = []
649
-
668
+
650
669
  for (const [field, type] of Object.entries(this.opts.indexes)) {
651
670
  // Fields that should use term mapping (only array fields)
652
671
  if (type === 'array:string') {
653
672
  termMappingFields.push(field)
654
673
  }
655
674
  }
656
-
675
+
657
676
  return termMappingFields
658
677
  }
659
678
 
@@ -745,7 +764,7 @@ class Database extends EventEmitter {
745
764
 
746
765
  // Reset closed state when reinitializing
747
766
  this.closed = false
748
-
767
+
749
768
  // Initialize managers (protected against double initialization)
750
769
  this.initializeManagers()
751
770
 
@@ -768,12 +787,33 @@ class Database extends EventEmitter {
768
787
  await this.load()
769
788
  }
770
789
  }
771
-
790
+
791
+ // CRITICAL INTEGRITY CHECK: Ensure IndexManager is consistent with loaded offsets
792
+ // This must happen immediately after load() to prevent any subsequent operations from seeing inconsistent state
793
+ if (this.indexManager && this.offsets && this.offsets.length > 0) {
794
+ const currentTotalLines = this.indexManager.totalLines || 0
795
+ if (currentTotalLines !== this.offsets.length) {
796
+ this.indexManager.setTotalLines(this.offsets.length)
797
+ if (this.opts.debugMode) {
798
+ console.log(`🔧 Post-load integrity sync: IndexManager totalLines ${currentTotalLines} → ${this.offsets.length}`)
799
+ }
800
+ }
801
+ }
802
+
772
803
  // Manual save is now the default behavior
773
-
804
+
805
+ // CRITICAL FIX: Ensure IndexManager totalLines is consistent with offsets
806
+ // This prevents data integrity issues when database is initialized without existing data
807
+ if (this.indexManager && this.offsets) {
808
+ this.indexManager.setTotalLines(this.offsets.length)
809
+ if (this.opts.debugMode) {
810
+ console.log(`🔧 Initialized index totalLines to ${this.offsets.length}`)
811
+ }
812
+ }
813
+
774
814
  this.initialized = true
775
815
  this.emit('initialized')
776
-
816
+
777
817
  if (this.opts.debugMode) {
778
818
  console.log(`✅ Database initialized with ${this.writeBuffer.length} records`)
779
819
  }
@@ -927,11 +967,11 @@ class Database extends EventEmitter {
927
967
  this.offsets = parsedIdxData.offsets
928
968
  // CRITICAL FIX: Update IndexManager totalLines to match offsets length
929
969
  // This ensures queries and length property work correctly even if offsets are reset later
930
- if (this.indexManager && this.offsets.length > 0) {
970
+ if (this.indexManager) {
931
971
  this.indexManager.setTotalLines(this.offsets.length)
932
- }
933
- if (this.opts.debugMode) {
934
- console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}`)
972
+ if (this.opts.debugMode) {
973
+ console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}, synced IndexManager totalLines`)
974
+ }
935
975
  }
936
976
  }
937
977
 
@@ -1606,6 +1646,7 @@ class Database extends EventEmitter {
1606
1646
 
1607
1647
  if (this.opts.debugMode) {
1608
1648
  console.log(`💾 Save: allData.length=${allData.length}, cleanedData.length=${cleanedData.length}`)
1649
+ console.log(`💾 Save: Current offsets.length before recalculation: ${this.offsets.length}`)
1609
1650
  console.log(`💾 Save: All records in allData before serialization:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
1610
1651
  console.log(`💾 Save: Sample cleaned record:`, cleanedData[0] ? Object.keys(cleanedData[0]) : 'null')
1611
1652
  }
@@ -1624,6 +1665,8 @@ class Database extends EventEmitter {
1624
1665
  }
1625
1666
  }
1626
1667
 
1668
+ // CRITICAL FIX: Always recalculate offsets from serialized data to ensure consistency
1669
+ // Even if _streamExistingRecords updated offsets, we need to recalculate based on actual serialized data
1627
1670
  this.offsets = []
1628
1671
  let currentOffset = 0
1629
1672
  for (let i = 0; i < lines.length; i++) {
@@ -1634,6 +1677,10 @@ class Database extends EventEmitter {
1634
1677
  currentOffset += Buffer.byteLength(lineWithNewline, 'utf8')
1635
1678
  }
1636
1679
 
1680
+ if (this.opts.debugMode) {
1681
+ console.log(`💾 Save: Recalculated offsets.length=${this.offsets.length}, should match lines.length=${lines.length}`)
1682
+ }
1683
+
1637
1684
  // CRITICAL FIX: Ensure indexOffset matches actual file size
1638
1685
  this.indexOffset = currentOffset
1639
1686
 
@@ -1655,11 +1702,15 @@ class Database extends EventEmitter {
1655
1702
  this.shouldSave = false
1656
1703
  this.lastSaveTime = Date.now()
1657
1704
 
1658
- // Clear writeBuffer and deletedIds after successful save only if we had data to save
1659
- if (allData.length > 0) {
1660
- // Rebuild index when records were deleted or updated to maintain consistency
1705
+ // CRITICAL FIX: Always clear deletedIds and rebuild index if there were deletions,
1706
+ // even if allData.length === 0 (all records were deleted)
1661
1707
  const hadDeletedRecords = deletedIdsSnapshot.size > 0
1662
1708
  const hadUpdatedRecords = writeBufferSnapshot.length > 0
1709
+
1710
+ // Clear writeBuffer and deletedIds after successful save
1711
+ // Also rebuild index if records were deleted or updated, even if allData is empty
1712
+ if (allData.length > 0 || hadDeletedRecords || hadUpdatedRecords) {
1713
+ // Rebuild index when records were deleted or updated to maintain consistency
1663
1714
  if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
1664
1715
  if (hadDeletedRecords || hadUpdatedRecords) {
1665
1716
  // Clear the index and rebuild it from the saved records
@@ -1713,8 +1764,26 @@ class Database extends EventEmitter {
1713
1764
 
1714
1765
  await this.indexManager.add(record, i)
1715
1766
  }
1767
+
1768
+ // VALIDATION: Ensure index consistency after rebuild
1769
+ // Check that all indexed records have valid line numbers
1770
+ const indexedRecordCount = this.indexManager.getIndexedRecordCount?.() || allData.length
1771
+ if (indexedRecordCount !== this.offsets.length) {
1772
+ this.integrityCorrections.indexInconsistency++
1773
+ console.log(`🔧 Auto-corrected index consistency: ${indexedRecordCount} indexed → ${this.offsets.length} offsets`)
1774
+
1775
+ if (this.integrityCorrections.indexInconsistency > 5) {
1776
+ console.warn(`⚠️ Frequent index inconsistencies detected (${this.integrityCorrections.indexInconsistency} times)`)
1777
+ }
1778
+
1779
+ // Force consistency by setting totalLines to match offsets
1780
+ this.indexManager.setTotalLines(this.offsets.length)
1781
+ } else {
1782
+ this.indexManager.setTotalLines(this.offsets.length)
1783
+ }
1784
+
1716
1785
  if (this.opts.debugMode) {
1717
- console.log(`💾 Save: Index rebuilt with ${allData.length} records`)
1786
+ console.log(`💾 Save: Index rebuilt with ${allData.length} records, totalLines set to ${this.offsets.length}`)
1718
1787
  }
1719
1788
  }
1720
1789
  }
@@ -1738,6 +1807,22 @@ class Database extends EventEmitter {
1738
1807
  for (const deletedId of deletedIdsSnapshot) {
1739
1808
  this.deletedIds.delete(deletedId)
1740
1809
  }
1810
+ } else if (hadDeletedRecords) {
1811
+ // CRITICAL FIX: Even if allData is empty, clear deletedIds and rebuild index
1812
+ // when records were deleted to ensure consistency
1813
+ if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
1814
+ // Clear the index since all records were deleted
1815
+ this.indexManager.clear()
1816
+ this.indexManager.setTotalLines(0)
1817
+ if (this.opts.debugMode) {
1818
+ console.log(`🧹 Cleared index after removing all ${deletedIdsSnapshot.size} deleted records`)
1819
+ }
1820
+ }
1821
+
1822
+ // Clear deletedIds even when allData is empty
1823
+ for (const deletedId of deletedIdsSnapshot) {
1824
+ this.deletedIds.delete(deletedId)
1825
+ }
1741
1826
 
1742
1827
  // CRITICAL FIX: Ensure writeBuffer is completely cleared after successful save
1743
1828
  if (this.writeBuffer.length > 0) {
@@ -2244,7 +2329,7 @@ class Database extends EventEmitter {
2244
2329
  */
2245
2330
  async find(criteria = {}, options = {}) {
2246
2331
  this._validateInitialization('find')
2247
-
2332
+
2248
2333
  // CRITICAL FIX: Validate state before find operation
2249
2334
  this.validateState()
2250
2335
 
@@ -2257,6 +2342,38 @@ class Database extends EventEmitter {
2257
2342
  }
2258
2343
 
2259
2344
  try {
2345
+ // INTEGRITY CHECK: Validate data consistency before querying
2346
+ // This is a safety net for unexpected inconsistencies - should rarely trigger
2347
+ if (this.indexManager && this.offsets && this.offsets.length > 0) {
2348
+ const indexTotalLines = this.indexManager.totalLines || 0
2349
+ const offsetsLength = this.offsets.length
2350
+
2351
+ if (indexTotalLines !== offsetsLength) {
2352
+ // This should be extremely rare - indicates a real bug if it happens frequently
2353
+ this.integrityCorrections.dataIntegrity++
2354
+
2355
+ // Only show in debug mode - these corrections indicate real issues
2356
+ if (this.opts.debugMode) {
2357
+ console.log(`🔧 Integrity correction needed: index.totalLines ${indexTotalLines} → ${offsetsLength} (${this.integrityCorrections.dataIntegrity} total)`)
2358
+ }
2359
+
2360
+ // Warn if corrections are becoming frequent (indicates a real problem)
2361
+ if (this.integrityCorrections.dataIntegrity > 5) {
2362
+ console.warn(`⚠️ Frequent integrity corrections (${this.integrityCorrections.dataIntegrity} times) - this indicates a systemic issue`)
2363
+ }
2364
+
2365
+ this.indexManager.setTotalLines(offsetsLength)
2366
+
2367
+ // Try to persist the fix, but don't fail the operation if it doesn't work
2368
+ try {
2369
+ await this._saveIndexDataToFile()
2370
+ } catch (error) {
2371
+ // Just track the failure - don't throw since this is a safety net
2372
+ this.integrityCorrections.indexSaveFailures++
2373
+ }
2374
+ }
2375
+ }
2376
+
2260
2377
  // Validate indexed query mode if enabled
2261
2378
  if (this.opts.indexedQueryMode === 'strict') {
2262
2379
  this._validateIndexedQuery(criteria, options)
@@ -2279,36 +2396,25 @@ class Database extends EventEmitter {
2279
2396
 
2280
2397
 
2281
2398
  // Combine results, removing duplicates (writeBuffer takes precedence)
2282
- // OPTIMIZATION: Use parallel processing for better performance when writeBuffer has many records
2399
+ // OPTIMIZATION: Unified efficient approach with consistent precedence rules
2283
2400
  let allResults
2284
- if (writeBufferResults.length > 50) {
2285
- // Parallel approach for large writeBuffer
2286
- const [fileResultsSet, writeBufferSet] = await Promise.all([
2287
- Promise.resolve(new Set(fileResultsWithTerms.map(r => r.id))),
2288
- Promise.resolve(new Set(writeBufferResultsWithTerms.map(r => r.id)))
2289
- ])
2401
+
2402
+ // Create efficient lookup map for writeBuffer records
2403
+ const writeBufferMap = new Map()
2404
+ writeBufferResultsWithTerms.forEach(record => {
2405
+ if (record && record.id) {
2406
+ writeBufferMap.set(record.id, record)
2407
+ }
2408
+ })
2290
2409
 
2291
- // Merge efficiently: keep file results not in writeBuffer, then add all writeBuffer results
2292
- const filteredFileResults = await Promise.resolve(
2293
- fileResultsWithTerms.filter(r => !writeBufferSet.has(r.id))
2294
- )
2410
+ // Filter file results to exclude any records that exist in writeBuffer
2411
+ // This ensures writeBuffer always takes precedence
2412
+ const filteredFileResults = fileResultsWithTerms.filter(record =>
2413
+ record && record.id && !writeBufferMap.has(record.id)
2414
+ )
2415
+
2416
+ // Combine results: file results (filtered) + all writeBuffer results
2295
2417
  allResults = [...filteredFileResults, ...writeBufferResultsWithTerms]
2296
- } else {
2297
- // Sequential approach for small writeBuffer (original logic)
2298
- allResults = [...fileResultsWithTerms]
2299
-
2300
- // Replace file records with writeBuffer records and add new writeBuffer records
2301
- for (const record of writeBufferResultsWithTerms) {
2302
- const existingIndex = allResults.findIndex(r => r.id === record.id)
2303
- if (existingIndex !== -1) {
2304
- // Replace existing record with writeBuffer version
2305
- allResults[existingIndex] = record
2306
- } else {
2307
- // Add new record from writeBuffer
2308
- allResults.push(record)
2309
- }
2310
- }
2311
- }
2312
2418
 
2313
2419
  // Remove records that are marked as deleted
2314
2420
  const finalResults = allResults.filter(record => !this.deletedIds.has(record.id))
@@ -2566,19 +2672,6 @@ class Database extends EventEmitter {
2566
2672
 
2567
2673
  // CRITICAL FIX: Validate state before update operation
2568
2674
  this.validateState()
2569
-
2570
- // CRITICAL FIX: If there's data to save, call save() to persist it
2571
- // Only save if there are actual records in writeBuffer
2572
- if (this.shouldSave && this.writeBuffer.length > 0) {
2573
- if (this.opts.debugMode) {
2574
- console.log(`🔄 UPDATE: Calling save() before update - writeBuffer.length=${this.writeBuffer.length}`)
2575
- }
2576
- const saveStart = Date.now()
2577
- await this.save(false) // Use save(false) since we're already in queue
2578
- if (this.opts.debugMode) {
2579
- console.log(`🔄 UPDATE: Save completed in ${Date.now() - saveStart}ms`)
2580
- }
2581
- }
2582
2675
 
2583
2676
  if (this.opts.debugMode) {
2584
2677
  console.log(`🔄 UPDATE: Starting find() - writeBuffer=${this.writeBuffer.length}`)
@@ -2591,7 +2684,12 @@ class Database extends EventEmitter {
2591
2684
  }
2592
2685
 
2593
2686
  const updatedRecords = []
2594
-
2687
+
2688
+ if (this.opts.debugMode) {
2689
+ console.log(`🔄 UPDATE: About to process ${records.length} records`)
2690
+ console.log(`🔄 UPDATE: Records:`, records.map(r => ({ id: r.id, value: r.value })))
2691
+ }
2692
+
2595
2693
  for (const record of records) {
2596
2694
  const recordStart = Date.now()
2597
2695
  if (this.opts.debugMode) {
@@ -2628,13 +2726,20 @@ class Database extends EventEmitter {
2628
2726
  // For records in the file, we need to ensure they are properly marked for replacement
2629
2727
  const index = this.writeBuffer.findIndex(r => r.id === record.id)
2630
2728
  let lineNumber = null
2631
-
2729
+
2730
+ if (this.opts.debugMode) {
2731
+ console.log(`🔄 UPDATE: writeBuffer.findIndex for ${record.id} returned ${index}`)
2732
+ console.log(`🔄 UPDATE: writeBuffer length: ${this.writeBuffer.length}`)
2733
+ console.log(`🔄 UPDATE: writeBuffer IDs:`, this.writeBuffer.map(r => r.id))
2734
+ }
2735
+
2632
2736
  if (index !== -1) {
2633
2737
  // Record is already in writeBuffer, update it
2634
2738
  this.writeBuffer[index] = updated
2635
2739
  lineNumber = this._getAbsoluteLineNumber(index)
2636
2740
  if (this.opts.debugMode) {
2637
2741
  console.log(`🔄 UPDATE: Updated existing writeBuffer record at index ${index}`)
2742
+ console.log(`🔄 UPDATE: writeBuffer now has ${this.writeBuffer.length} records`)
2638
2743
  }
2639
2744
  } else {
2640
2745
  // Record is in file, add updated version to writeBuffer
@@ -2644,6 +2749,7 @@ class Database extends EventEmitter {
2644
2749
  lineNumber = this._getAbsoluteLineNumber(this.writeBuffer.length - 1)
2645
2750
  if (this.opts.debugMode) {
2646
2751
  console.log(`🔄 UPDATE: Added updated record to writeBuffer (will replace file record ${record.id})`)
2752
+ console.log(`🔄 UPDATE: writeBuffer now has ${this.writeBuffer.length} records`)
2647
2753
  }
2648
2754
  }
2649
2755
 
@@ -2679,13 +2785,32 @@ class Database extends EventEmitter {
2679
2785
  */
2680
2786
  async delete(criteria) {
2681
2787
  this._validateInitialization('delete')
2682
-
2788
+
2683
2789
  return this.operationQueue.enqueue(async () => {
2684
2790
  this.isInsideOperationQueue = true
2685
2791
  try {
2686
2792
  // CRITICAL FIX: Validate state before delete operation
2687
2793
  this.validateState()
2688
-
2794
+
2795
+ // 🔧 NEW: Validate indexed query mode for delete operations
2796
+ if (this.opts.indexedQueryMode === 'strict') {
2797
+ this._validateIndexedQuery(criteria, { operation: 'delete' })
2798
+ }
2799
+
2800
+ // ⚠️ NEW: Warn about non-indexed fields in permissive mode
2801
+ if (this.opts.indexedQueryMode !== 'strict') {
2802
+ const indexedFields = Object.keys(this.opts.indexes || {})
2803
+ const queryFields = this._extractQueryFields(criteria)
2804
+ const nonIndexedFields = queryFields.filter(field => !indexedFields.includes(field))
2805
+
2806
+ if (nonIndexedFields.length > 0) {
2807
+ if (this.opts.debugMode) {
2808
+ console.warn(`⚠️ Delete operation using non-indexed fields: ${nonIndexedFields.join(', ')}`)
2809
+ console.warn(` This may be slow or fail silently. Consider indexing these fields.`)
2810
+ }
2811
+ }
2812
+ }
2813
+
2689
2814
  const records = await this.find(criteria)
2690
2815
  const deletedIds = []
2691
2816
 
@@ -4289,14 +4414,30 @@ class Database extends EventEmitter {
4289
4414
  try {
4290
4415
  const arrayData = JSON.parse(trimmedLine)
4291
4416
  if (Array.isArray(arrayData) && arrayData.length > 0) {
4292
- // For arrays without explicit ID, use the first element as a fallback
4293
- // or try to find the ID field if it exists
4417
+ // CRITICAL FIX: Use schema to find ID position, not hardcoded position
4418
+ // The schema defines the order of fields in the array
4419
+ if (this.serializer && this.serializer.schemaManager && this.serializer.schemaManager.isInitialized) {
4420
+ const schema = this.serializer.schemaManager.getSchema()
4421
+ const idIndex = schema.indexOf('id')
4422
+ if (idIndex !== -1 && arrayData.length > idIndex) {
4423
+ // ID is at the position defined by schema
4424
+ recordId = arrayData[idIndex]
4425
+ } else if (arrayData.length > schema.length) {
4426
+ // ID might be appended after schema fields (for backward compatibility)
4427
+ recordId = arrayData[schema.length]
4428
+ } else {
4429
+ // Fallback: use first element
4430
+ recordId = arrayData[0]
4431
+ }
4432
+ } else {
4433
+ // No schema available, try common positions
4294
4434
  if (arrayData.length > 2) {
4295
- // ID is typically at position 2 in array format [age, city, id, name]
4435
+ // Try position 2 (common in older formats)
4296
4436
  recordId = arrayData[2]
4297
4437
  } else {
4298
- // For arrays without ID field, use first element as fallback
4438
+ // Fallback: use first element
4299
4439
  recordId = arrayData[0]
4440
+ }
4300
4441
  }
4301
4442
  if (recordId !== undefined && recordId !== null) {
4302
4443
  recordId = String(recordId)
@@ -4369,7 +4510,7 @@ class Database extends EventEmitter {
4369
4510
  } else if (!deletedIdsSnapshot.has(String(recordWithIds.id))) {
4370
4511
  // Keep existing record if not deleted
4371
4512
  if (this.opts.debugMode) {
4372
- console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`)
4513
+ console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - not in deletedIdsSnapshot`)
4373
4514
  }
4374
4515
  return {
4375
4516
  type: 'kept',
@@ -4380,7 +4521,9 @@ class Database extends EventEmitter {
4380
4521
  } else {
4381
4522
  // Skip deleted record
4382
4523
  if (this.opts.debugMode) {
4383
- console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted`)
4524
+ console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted (found in deletedIdsSnapshot)`)
4525
+ console.log(`💾 Save: deletedIdsSnapshot contains:`, Array.from(deletedIdsSnapshot))
4526
+ console.log(`💾 Save: Record ID check: String(${recordWithIds.id}) = "${String(recordWithIds.id)}", has() = ${deletedIdsSnapshot.has(String(recordWithIds.id))}`)
4384
4527
  }
4385
4528
  return {
4386
4529
  type: 'deleted',
@@ -4426,6 +4569,54 @@ class Database extends EventEmitter {
4426
4569
 
4427
4570
  switch (result.type) {
4428
4571
  case 'unchanged':
4572
+ // CRITICAL FIX: Verify that unchanged records are not deleted
4573
+ // Extract ID from the line to check against deletedIdsSnapshot
4574
+ let unchangedRecordId = null
4575
+ try {
4576
+ if (result.line.startsWith('[') && result.line.endsWith(']')) {
4577
+ const arrayData = JSON.parse(result.line)
4578
+ if (Array.isArray(arrayData) && arrayData.length > 0) {
4579
+ // CRITICAL FIX: Use schema to find ID position, not hardcoded position
4580
+ if (this.serializer && this.serializer.schemaManager && this.serializer.schemaManager.isInitialized) {
4581
+ const schema = this.serializer.schemaManager.getSchema()
4582
+ const idIndex = schema.indexOf('id')
4583
+ if (idIndex !== -1 && arrayData.length > idIndex) {
4584
+ unchangedRecordId = String(arrayData[idIndex])
4585
+ } else if (arrayData.length > schema.length) {
4586
+ unchangedRecordId = String(arrayData[schema.length])
4587
+ } else {
4588
+ unchangedRecordId = String(arrayData[0])
4589
+ }
4590
+ } else {
4591
+ // No schema, try common positions
4592
+ if (arrayData.length > 2) {
4593
+ unchangedRecordId = String(arrayData[2])
4594
+ } else {
4595
+ unchangedRecordId = String(arrayData[0])
4596
+ }
4597
+ }
4598
+ }
4599
+ } else {
4600
+ const obj = JSON.parse(result.line)
4601
+ unchangedRecordId = obj.id ? String(obj.id) : null
4602
+ }
4603
+ } catch (e) {
4604
+ // If we can't parse, skip this record to be safe
4605
+ if (this.opts.debugMode) {
4606
+ console.log(`💾 Save: Could not parse unchanged record to check deletion: ${e.message}`)
4607
+ }
4608
+ continue
4609
+ }
4610
+
4611
+ // Skip if this record is deleted
4612
+ if (unchangedRecordId && deletedIdsSnapshot.has(unchangedRecordId)) {
4613
+ if (this.opts.debugMode) {
4614
+ console.log(`💾 Save: Skipping unchanged record ${unchangedRecordId} - deleted`)
4615
+ }
4616
+ deletedOffsets.add(offset)
4617
+ break
4618
+ }
4619
+
4429
4620
  // Collect unchanged lines for batch processing
4430
4621
  unchangedLines.push(result.line)
4431
4622
  keptRecords.push({ offset, type: 'unchanged', line: result.line })
@@ -4620,6 +4811,78 @@ class Database extends EventEmitter {
4620
4811
  return this._getWriteBufferBaseLineNumber() + writeBufferIndex
4621
4812
  }
4622
4813
 
4814
+
4815
+ /**
4816
+ * Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
4817
+ * @param {string} line - The corrupted line to recover
4818
+ * @returns {string|null} - The recovered line or null if recovery is not possible
4819
+ */
4820
+ _tryRecoverLine(line) {
4821
+ if (!line || typeof line !== 'string') {
4822
+ return null
4823
+ }
4824
+
4825
+ try {
4826
+ // Try parsing as-is first
4827
+ JSON.parse(line)
4828
+ return line // Line is already valid
4829
+ } catch (e) {
4830
+ // Line is corrupted, attempt recovery
4831
+ }
4832
+
4833
+ let recovered = line.trim()
4834
+
4835
+ // Remove invalid control characters (except \n, \r, \t)
4836
+ recovered = recovered.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
4837
+
4838
+ // Try to close unclosed strings
4839
+ // Count quotes and ensure they're balanced
4840
+ const quoteCount = (recovered.match(/"/g) || []).length
4841
+ if (quoteCount % 2 !== 0) {
4842
+ // Odd number of quotes - try to close the string
4843
+ const lastQuoteIndex = recovered.lastIndexOf('"')
4844
+ if (lastQuoteIndex > 0) {
4845
+ // Check if we're inside a string (not escaped)
4846
+ const beforeLastQuote = recovered.substring(0, lastQuoteIndex)
4847
+ const escapedQuotes = (beforeLastQuote.match(/\\"/g) || []).length
4848
+ const unescapedQuotes = (beforeLastQuote.match(/"/g) || []).length - escapedQuotes
4849
+
4850
+ if (unescapedQuotes % 2 !== 0) {
4851
+ // We're inside an unclosed string - try to close it
4852
+ recovered = recovered + '"'
4853
+ }
4854
+ }
4855
+ }
4856
+
4857
+ // Try to close unclosed arrays/objects
4858
+ const openBraces = (recovered.match(/\{/g) || []).length
4859
+ const closeBraces = (recovered.match(/\}/g) || []).length
4860
+ const openBrackets = (recovered.match(/\[/g) || []).length
4861
+ const closeBrackets = (recovered.match(/\]/g) || []).length
4862
+
4863
+ // Remove trailing commas before closing braces/brackets
4864
+ recovered = recovered.replace(/,\s*([}\]])/g, '$1')
4865
+
4866
+ // Try to close arrays
4867
+ if (openBrackets > closeBrackets) {
4868
+ recovered = recovered + ']'.repeat(openBrackets - closeBrackets)
4869
+ }
4870
+
4871
+ // Try to close objects
4872
+ if (openBraces > closeBraces) {
4873
+ recovered = recovered + '}'.repeat(openBraces - closeBraces)
4874
+ }
4875
+
4876
+ // Final validation - try to parse
4877
+ try {
4878
+ JSON.parse(recovered)
4879
+ return recovered
4880
+ } catch (e) {
4881
+ // Recovery failed
4882
+ return null
4883
+ }
4884
+ }
4885
+
4623
4886
  async *_streamingRecoveryGenerator(criteria, options, alreadyYielded = 0, map = null, remainingSkipValue = 0) {
4624
4887
  if (this._offsetRecoveryInProgress) {
4625
4888
  return
@@ -4845,6 +5108,17 @@ class Database extends EventEmitter {
4845
5108
  // If no data at all, return empty
4846
5109
  if (this.indexOffset === 0 && this.writeBuffer.length === 0) return
4847
5110
 
5111
+ // CRITICAL FIX: Wait for any ongoing save operations to complete
5112
+ // This prevents reading partially written data
5113
+ if (this.isSaving) {
5114
+ if (this.opts.debugMode) {
5115
+ console.log('🔍 walk(): waiting for save operation to complete')
5116
+ }
5117
+ while (this.isSaving) {
5118
+ await new Promise(resolve => setTimeout(resolve, 10))
5119
+ }
5120
+ }
5121
+
4848
5122
  let count = 0
4849
5123
  let remainingSkip = options.skip || 0
4850
5124
 
@@ -4983,10 +5257,50 @@ class Database extends EventEmitter {
4983
5257
  } catch (error) {
4984
5258
  // CRITICAL FIX: Log deserialization errors instead of silently ignoring them
4985
5259
  // This helps identify data corruption issues
4986
- if (1||this.opts.debugMode) {
5260
+ if (this.opts.debugMode) {
4987
5261
  console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`)
4988
5262
  console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`)
4989
5263
  }
5264
+
5265
+ // CRITICAL FIX: Attempt to recover corrupted line before giving up
5266
+ const recoveredLine = this._tryRecoverLine(row.line)
5267
+ if (recoveredLine) {
5268
+ try {
5269
+ const record = this.serializer.deserialize(recoveredLine)
5270
+ if (record !== null) {
5271
+ this.integrityCorrections.jsonRecovery++
5272
+ console.log(`🔧 Recovered corrupted JSON line (${this.integrityCorrections.jsonRecovery} recoveries)`)
5273
+
5274
+ if (this.integrityCorrections.jsonRecovery > 20) {
5275
+ console.warn(`⚠️ Frequent JSON recovery detected (${this.integrityCorrections.jsonRecovery} times) - may indicate data corruption`)
5276
+ }
5277
+
5278
+ const recordWithTerms = this.restoreTermIdsAfterDeserialization(record)
5279
+
5280
+ if (remainingSkip > 0) {
5281
+ remainingSkip--
5282
+ continue
5283
+ }
5284
+
5285
+ count++
5286
+ if (options.includeOffsets) {
5287
+ yield { entry: recordWithTerms, start: row.start, _: row._ || 0 }
5288
+ } else {
5289
+ if (this.opts.includeLinePosition) {
5290
+ recordWithTerms._ = row._ || 0
5291
+ }
5292
+ yield recordWithTerms
5293
+ }
5294
+ continue // Successfully recovered and yielded
5295
+ }
5296
+ } catch (recoveryError) {
5297
+ // Recovery attempt failed, continue with normal error handling
5298
+ if (this.opts.debugMode) {
5299
+ console.warn(`⚠️ walk(): Line recovery failed: ${recoveryError.message}`)
5300
+ }
5301
+ }
5302
+ }
5303
+
4990
5304
  if (!this._offsetRecoveryInProgress) {
4991
5305
  for await (const recoveredEntry of this._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)) {
4992
5306
  yield recoveredEntry
@@ -5091,10 +5405,50 @@ class Database extends EventEmitter {
5091
5405
  } catch (error) {
5092
5406
  // CRITICAL FIX: Log deserialization errors instead of silently ignoring them
5093
5407
  // This helps identify data corruption issues
5094
- if (1||this.opts.debugMode) {
5408
+ if (this.opts.debugMode) {
5095
5409
  console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`)
5096
5410
  console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`)
5097
5411
  }
5412
+
5413
+ // CRITICAL FIX: Attempt to recover corrupted line before giving up
5414
+ const recoveredLine = this._tryRecoverLine(row.line)
5415
+ if (recoveredLine) {
5416
+ try {
5417
+ const entry = await this.serializer.deserialize(recoveredLine, { compress: this.opts.compress, v8: this.opts.v8 })
5418
+ if (entry !== null) {
5419
+ this.integrityCorrections.jsonRecovery++
5420
+ console.log(`🔧 Recovered corrupted JSON line (${this.integrityCorrections.jsonRecovery} recoveries)`)
5421
+
5422
+ if (this.integrityCorrections.jsonRecovery > 20) {
5423
+ console.warn(`⚠️ Frequent JSON recovery detected (${this.integrityCorrections.jsonRecovery} times) - may indicate data corruption`)
5424
+ }
5425
+
5426
+ const entryWithTerms = this.restoreTermIdsAfterDeserialization(entry)
5427
+
5428
+ if (remainingSkip > 0) {
5429
+ remainingSkip--
5430
+ continue
5431
+ }
5432
+
5433
+ count++
5434
+ if (options.includeOffsets) {
5435
+ yield { entry: entryWithTerms, start: row.start, _: row._ || this.offsets.findIndex(n => n === row.start) }
5436
+ } else {
5437
+ if (this.opts.includeLinePosition) {
5438
+ entryWithTerms._ = row._ || this.offsets.findIndex(n => n === row.start)
5439
+ }
5440
+ yield entryWithTerms
5441
+ }
5442
+ continue // Successfully recovered and yielded
5443
+ }
5444
+ } catch (recoveryError) {
5445
+ // Recovery attempt failed, continue with normal error handling
5446
+ if (this.opts.debugMode) {
5447
+ console.warn(`⚠️ walk(): Line recovery failed: ${recoveryError.message}`)
5448
+ }
5449
+ }
5450
+ }
5451
+
5098
5452
  if (!this._offsetRecoveryInProgress) {
5099
5453
  for await (const recoveredEntry of this._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)) {
5100
5454
  yield recoveredEntry
@@ -5338,7 +5692,13 @@ class Database extends EventEmitter {
5338
5692
  await this.save()
5339
5693
  // Ensure writeBuffer is cleared after save
5340
5694
  if (this.writeBuffer.length > 0) {
5341
- console.warn('⚠️ WriteBuffer not cleared after save() - forcing clear')
5695
+ this.integrityCorrections.writeBufferForced++
5696
+ console.log(`🔧 Forced WriteBuffer clear after save (${this.writeBuffer.length} items remaining)`)
5697
+
5698
+ if (this.integrityCorrections.writeBufferForced > 3) {
5699
+ console.warn(`⚠️ Frequent WriteBuffer clear issues detected (${this.integrityCorrections.writeBufferForced} times)`)
5700
+ }
5701
+
5342
5702
  this.writeBuffer = []
5343
5703
  this.writeBufferOffsets = []
5344
5704
  this.writeBufferSizes = []
@@ -5463,7 +5823,8 @@ class Database extends EventEmitter {
5463
5823
  console.log(`💾 Index data saved to ${idxPath}`)
5464
5824
  }
5465
5825
  } catch (error) {
5466
- console.warn('Failed to save index data:', error.message)
5826
+ this.integrityCorrections.indexSaveFailures++
5827
+ console.warn(`⚠️ Index save failure (${this.integrityCorrections.indexSaveFailures} times): ${error.message}`)
5467
5828
  throw error // Re-throw to let caller handle
5468
5829
  }
5469
5830
  }
@@ -5539,5 +5900,4 @@ class Database extends EventEmitter {
5539
5900
  }
5540
5901
 
5541
5902
  export { Database }
5542
- export default Database
5543
5903