jexidb 2.1.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jexidb",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "type": "module",
5
5
  "description": "JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.",
6
6
  "main": "./dist/Database.cjs",
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "author": "EdenwareApps",
24
24
  "license": "MIT",
25
+ "engines": {
26
+ "node": ">=16.0.0"
27
+ },
25
28
  "devDependencies": {
26
29
  "@babel/cli": "^7.25.6",
27
30
  "@babel/core": "^7.25.2",
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,7 +787,19 @@ 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
 
774
805
  // CRITICAL FIX: Ensure IndexManager totalLines is consistent with offsets
@@ -936,11 +967,11 @@ class Database extends EventEmitter {
936
967
  this.offsets = parsedIdxData.offsets
937
968
  // CRITICAL FIX: Update IndexManager totalLines to match offsets length
938
969
  // This ensures queries and length property work correctly even if offsets are reset later
939
- if (this.indexManager && this.offsets.length > 0) {
970
+ if (this.indexManager) {
940
971
  this.indexManager.setTotalLines(this.offsets.length)
941
- }
942
- if (this.opts.debugMode) {
943
- 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
+ }
944
975
  }
945
976
  }
946
977
 
@@ -1738,7 +1769,13 @@ class Database extends EventEmitter {
1738
1769
  // Check that all indexed records have valid line numbers
1739
1770
  const indexedRecordCount = this.indexManager.getIndexedRecordCount?.() || allData.length
1740
1771
  if (indexedRecordCount !== this.offsets.length) {
1741
- console.warn(`⚠️ Index inconsistency detected: indexed ${indexedRecordCount} records but offsets has ${this.offsets.length} entries`)
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
+
1742
1779
  // Force consistency by setting totalLines to match offsets
1743
1780
  this.indexManager.setTotalLines(this.offsets.length)
1744
1781
  } else {
@@ -2292,7 +2329,7 @@ class Database extends EventEmitter {
2292
2329
  */
2293
2330
  async find(criteria = {}, options = {}) {
2294
2331
  this._validateInitialization('find')
2295
-
2332
+
2296
2333
  // CRITICAL FIX: Validate state before find operation
2297
2334
  this.validateState()
2298
2335
 
@@ -2306,38 +2343,33 @@ class Database extends EventEmitter {
2306
2343
 
2307
2344
  try {
2308
2345
  // INTEGRITY CHECK: Validate data consistency before querying
2309
- // Check if index and offsets are synchronized
2346
+ // This is a safety net for unexpected inconsistencies - should rarely trigger
2310
2347
  if (this.indexManager && this.offsets && this.offsets.length > 0) {
2311
2348
  const indexTotalLines = this.indexManager.totalLines || 0
2312
2349
  const offsetsLength = this.offsets.length
2313
2350
 
2314
2351
  if (indexTotalLines !== offsetsLength) {
2315
- console.warn(`⚠️ Data integrity issue detected: index.totalLines=${indexTotalLines}, offsets.length=${offsetsLength}`)
2316
- // Auto-correct by updating index totalLines to match offsets
2317
- this.indexManager.setTotalLines(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
2318
2356
  if (this.opts.debugMode) {
2319
- console.log(`🔧 Auto-corrected index totalLines to ${offsetsLength}`)
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`)
2320
2363
  }
2321
2364
 
2322
- // CRITICAL FIX: Also save the corrected index to prevent persistence of inconsistency
2323
- // This ensures the .idx.jdb file contains the correct totalLines value
2365
+ this.indexManager.setTotalLines(offsetsLength)
2366
+
2367
+ // Try to persist the fix, but don't fail the operation if it doesn't work
2324
2368
  try {
2325
2369
  await this._saveIndexDataToFile()
2326
- if (this.opts.debugMode) {
2327
- console.log(`💾 Saved corrected index data to prevent future inconsistencies`)
2328
- }
2329
2370
  } catch (error) {
2330
- if (this.opts.debugMode) {
2331
- console.warn(`⚠️ Failed to save corrected index: ${error.message}`)
2332
- }
2333
- }
2334
-
2335
- // Verify the fix worked
2336
- const newIndexTotalLines = this.indexManager.totalLines || 0
2337
- if (newIndexTotalLines === offsetsLength) {
2338
- console.log(`✅ Data integrity successfully corrected: index.totalLines=${newIndexTotalLines}, offsets.length=${offsetsLength}`)
2339
- } else {
2340
- console.error(`❌ Data integrity correction failed: index.totalLines=${newIndexTotalLines}, offsets.length=${offsetsLength}`)
2371
+ // Just track the failure - don't throw since this is a safety net
2372
+ this.integrityCorrections.indexSaveFailures++
2341
2373
  }
2342
2374
  }
2343
2375
  }
@@ -4779,6 +4811,78 @@ class Database extends EventEmitter {
4779
4811
  return this._getWriteBufferBaseLineNumber() + writeBufferIndex
4780
4812
  }
4781
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
+
4782
4886
  async *_streamingRecoveryGenerator(criteria, options, alreadyYielded = 0, map = null, remainingSkipValue = 0) {
4783
4887
  if (this._offsetRecoveryInProgress) {
4784
4888
  return
@@ -5004,6 +5108,17 @@ class Database extends EventEmitter {
5004
5108
  // If no data at all, return empty
5005
5109
  if (this.indexOffset === 0 && this.writeBuffer.length === 0) return
5006
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
+
5007
5122
  let count = 0
5008
5123
  let remainingSkip = options.skip || 0
5009
5124
 
@@ -5142,10 +5257,50 @@ class Database extends EventEmitter {
5142
5257
  } catch (error) {
5143
5258
  // CRITICAL FIX: Log deserialization errors instead of silently ignoring them
5144
5259
  // This helps identify data corruption issues
5145
- if (1||this.opts.debugMode) {
5260
+ if (this.opts.debugMode) {
5146
5261
  console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`)
5147
5262
  console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`)
5148
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
+
5149
5304
  if (!this._offsetRecoveryInProgress) {
5150
5305
  for await (const recoveredEntry of this._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)) {
5151
5306
  yield recoveredEntry
@@ -5250,10 +5405,50 @@ class Database extends EventEmitter {
5250
5405
  } catch (error) {
5251
5406
  // CRITICAL FIX: Log deserialization errors instead of silently ignoring them
5252
5407
  // This helps identify data corruption issues
5253
- if (1||this.opts.debugMode) {
5408
+ if (this.opts.debugMode) {
5254
5409
  console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`)
5255
5410
  console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`)
5256
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
+
5257
5452
  if (!this._offsetRecoveryInProgress) {
5258
5453
  for await (const recoveredEntry of this._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)) {
5259
5454
  yield recoveredEntry
@@ -5497,7 +5692,13 @@ class Database extends EventEmitter {
5497
5692
  await this.save()
5498
5693
  // Ensure writeBuffer is cleared after save
5499
5694
  if (this.writeBuffer.length > 0) {
5500
- 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
+
5501
5702
  this.writeBuffer = []
5502
5703
  this.writeBufferOffsets = []
5503
5704
  this.writeBufferSizes = []
@@ -5622,7 +5823,8 @@ class Database extends EventEmitter {
5622
5823
  console.log(`💾 Index data saved to ${idxPath}`)
5623
5824
  }
5624
5825
  } catch (error) {
5625
- 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}`)
5626
5828
  throw error // Re-throw to let caller handle
5627
5829
  }
5628
5830
  }
@@ -5698,5 +5900,4 @@ class Database extends EventEmitter {
5698
5900
  }
5699
5901
 
5700
5902
  export { Database }
5701
- export default Database
5702
5903