jexidb 2.1.2 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jexidb",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
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",
package/src/Database.mjs CHANGED
@@ -1319,38 +1319,21 @@ class Database extends EventEmitter {
1319
1319
  this.pendingIndexUpdates = []
1320
1320
  }
1321
1321
 
1322
- // CRITICAL FIX: Flush write buffer completely after capturing snapshot
1323
- await this._flushWriteBufferCompletely()
1324
-
1325
- // CRITICAL FIX: Wait for all I/O operations to complete before clearing writeBuffer
1326
- await this._waitForIOCompletion()
1327
-
1328
- // CRITICAL FIX: Verify write buffer is empty after I/O completion
1329
- // But allow for ongoing insertions during high-volume scenarios
1330
- if (this.writeBuffer.length > 0) {
1331
- if (this.opts.debugMode) {
1332
- console.log(`💾 Save: WriteBuffer still has ${this.writeBuffer.length} items after flush - this may indicate ongoing insertions`)
1333
- }
1334
-
1335
- // If we have a reasonable number of items, continue processing
1336
- if (this.writeBuffer.length < 10000) { // Reasonable threshold
1337
- if (this.opts.debugMode) {
1338
- console.log(`💾 Save: Continuing to process remaining ${this.writeBuffer.length} items`)
1339
- }
1340
- // Continue with the save process - the remaining items will be included in the final save
1341
- } else {
1342
- // Too many items remaining - likely a real problem
1343
- throw new Error(`WriteBuffer has too many items after flush: ${this.writeBuffer.length} items remaining (threshold: 10000)`)
1344
- }
1322
+ // CRITICAL FIX: DO NOT flush writeBuffer before processing existing records
1323
+ // This prevents duplicating updated records in the file.
1324
+ // The _streamExistingRecords() will handle replacing old records with updated ones from writeBufferSnapshot.
1325
+ // After processing, all records (existing + updated + new) will be written to file in one operation.
1326
+ if (this.opts.debugMode) {
1327
+ console.log(`💾 Save: writeBufferSnapshot captured with ${writeBufferSnapshot.length} records (will be processed with existing records)`)
1345
1328
  }
1346
1329
 
1347
1330
  // OPTIMIZATION: Parallel operations - cleanup and data preparation
1348
1331
  let allData = []
1349
1332
  let orphanedCount = 0
1350
1333
 
1351
- // Check if there are new records to save (after flush, writeBuffer should be empty)
1352
- // CRITICAL FIX: Also check writeBufferSnapshot.length > 0 to handle updates/deletes
1353
- // that were in writeBuffer before flush but are now in snapshot
1334
+ // Check if there are records to save from writeBufferSnapshot
1335
+ // CRITICAL FIX: Process writeBufferSnapshot records (both new and updated) with existing records
1336
+ // Updated records will replace old ones via _streamExistingRecords, new records will be added
1354
1337
  if (this.opts.debugMode) {
1355
1338
  console.log(`💾 Save: writeBuffer.length=${this.writeBuffer.length}, writeBufferSnapshot.length=${writeBufferSnapshot.length}`)
1356
1339
  }
@@ -1397,11 +1380,49 @@ class Database extends EventEmitter {
1397
1380
  // CRITICAL FIX: Normalize IDs to strings for consistent comparison
1398
1381
  const existingRecordIds = new Set(existingRecords.filter(r => r && r.id).map(r => String(r.id)))
1399
1382
 
1383
+ // CRITICAL FIX: Create a map of records in existingRecords by ID for comparison
1384
+ const existingRecordsById = new Map()
1385
+ existingRecords.forEach(r => {
1386
+ if (r && r.id) {
1387
+ existingRecordsById.set(String(r.id), r)
1388
+ }
1389
+ })
1390
+
1400
1391
  // Add only NEW records from writeBufferSnapshot (not updates, as those are already in existingRecords)
1392
+ // CRITICAL FIX: Also ensure that if an updated record wasn't properly replaced, we replace it now
1401
1393
  for (const record of writeBufferSnapshot) {
1402
- if (record && record.id && !deletedIdsSnapshot.has(String(record.id)) && !existingRecordIds.has(String(record.id))) {
1394
+ if (!record || !record.id) continue
1395
+ if (deletedIdsSnapshot.has(String(record.id))) continue
1396
+
1397
+ const recordIdStr = String(record.id)
1398
+ const existingRecord = existingRecordsById.get(recordIdStr)
1399
+
1400
+ if (!existingRecord) {
1403
1401
  // This is a new record, not an update
1404
1402
  allData.push(record)
1403
+ if (this.opts.debugMode) {
1404
+ console.log(`💾 Save: Adding NEW record to allData:`, { id: recordIdStr, price: record.price, app_id: record.app_id, currency: record.currency })
1405
+ }
1406
+ } else {
1407
+ // This is an update - verify that existingRecords contains the updated version
1408
+ // If not, replace it (this handles edge cases where substitution might have failed)
1409
+ const existingIndex = allData.findIndex(r => r && r.id && String(r.id) === recordIdStr)
1410
+ if (existingIndex !== -1) {
1411
+ // Verify if the existing record is actually the updated one
1412
+ // Compare key fields to detect if replacement is needed
1413
+ const needsReplacement = JSON.stringify(allData[existingIndex]) !== JSON.stringify(record)
1414
+ if (needsReplacement) {
1415
+ if (this.opts.debugMode) {
1416
+ console.log(`💾 Save: REPLACING existing record with updated version in allData:`, {
1417
+ old: { id: String(allData[existingIndex].id), price: allData[existingIndex].price },
1418
+ new: { id: recordIdStr, price: record.price }
1419
+ })
1420
+ }
1421
+ allData[existingIndex] = record
1422
+ } else if (this.opts.debugMode) {
1423
+ console.log(`💾 Save: Record already correctly updated in allData:`, { id: recordIdStr })
1424
+ }
1425
+ }
1405
1426
  }
1406
1427
  }
1407
1428
  })
@@ -1452,13 +1473,52 @@ class Database extends EventEmitter {
1452
1473
  allData = [...existingRecords]
1453
1474
 
1454
1475
  // OPTIMIZATION: Use Set for faster lookups of existing record IDs
1455
- const existingRecordIds = new Set(existingRecords.filter(r => r && r.id).map(r => r.id))
1476
+ // CRITICAL FIX: Normalize IDs to strings for consistent comparison
1477
+ const existingRecordIds = new Set(existingRecords.filter(r => r && r.id).map(r => String(r.id)))
1478
+
1479
+ // CRITICAL FIX: Create a map of records in existingRecords by ID for comparison
1480
+ const existingRecordsById = new Map()
1481
+ existingRecords.forEach(r => {
1482
+ if (r && r.id) {
1483
+ existingRecordsById.set(String(r.id), r)
1484
+ }
1485
+ })
1456
1486
 
1457
1487
  // Add only NEW records from writeBufferSnapshot (not updates, as those are already in existingRecords)
1488
+ // CRITICAL FIX: Also ensure that if an updated record wasn't properly replaced, we replace it now
1458
1489
  for (const record of writeBufferSnapshot) {
1459
- if (record && record.id && !deletedIdsSnapshot.has(String(record.id)) && !existingRecordIds.has(record.id)) {
1490
+ if (!record || !record.id) continue
1491
+ if (deletedIdsSnapshot.has(String(record.id))) continue
1492
+
1493
+ const recordIdStr = String(record.id)
1494
+ const existingRecord = existingRecordsById.get(recordIdStr)
1495
+
1496
+ if (!existingRecord) {
1460
1497
  // This is a new record, not an update
1461
1498
  allData.push(record)
1499
+ if (this.opts.debugMode) {
1500
+ console.log(`💾 Save: Adding NEW record to allData:`, { id: recordIdStr, price: record.price, app_id: record.app_id, currency: record.currency })
1501
+ }
1502
+ } else {
1503
+ // This is an update - verify that existingRecords contains the updated version
1504
+ // If not, replace it (this handles edge cases where substitution might have failed)
1505
+ const existingIndex = allData.findIndex(r => r && r.id && String(r.id) === recordIdStr)
1506
+ if (existingIndex !== -1) {
1507
+ // Verify if the existing record is actually the updated one
1508
+ // Compare key fields to detect if replacement is needed
1509
+ const needsReplacement = JSON.stringify(allData[existingIndex]) !== JSON.stringify(record)
1510
+ if (needsReplacement) {
1511
+ if (this.opts.debugMode) {
1512
+ console.log(`💾 Save: REPLACING existing record with updated version in allData:`, {
1513
+ old: { id: String(allData[existingIndex].id), price: allData[existingIndex].price },
1514
+ new: { id: recordIdStr, price: record.price }
1515
+ })
1516
+ }
1517
+ allData[existingIndex] = record
1518
+ } else if (this.opts.debugMode) {
1519
+ console.log(`💾 Save: Record already correctly updated in allData:`, { id: recordIdStr })
1520
+ }
1521
+ }
1462
1522
  }
1463
1523
  }
1464
1524
 
@@ -1466,10 +1526,11 @@ class Database extends EventEmitter {
1466
1526
  const updatedCount = writeBufferSnapshot.filter(r => r && r.id && existingRecordIds.has(String(r.id))).length
1467
1527
  const newCount = writeBufferSnapshot.filter(r => r && r.id && !existingRecordIds.has(String(r.id))).length
1468
1528
  console.log(`💾 Save: Combined data - existingRecords: ${existingRecords.length}, updatedFromBuffer: ${updatedCount}, newFromBuffer: ${newCount}, total: ${allData.length}`)
1469
- console.log(`💾 Save: WriteBuffer record IDs:`, writeBufferSnapshot.map(r => r && r.id ? r.id : 'no-id'))
1529
+ console.log(`💾 Save: WriteBuffer record IDs:`, writeBufferSnapshot.map(r => r && r.id ? String(r.id) : 'no-id'))
1470
1530
  console.log(`💾 Save: Existing record IDs:`, Array.from(existingRecordIds))
1471
- console.log(`💾 Save: Sample existing record:`, existingRecords[0] ? { id: existingRecords[0].id, name: existingRecords[0].name, tags: existingRecords[0].tags } : 'null')
1472
- console.log(`💾 Save: Sample writeBuffer record:`, writeBufferSnapshot[0] ? { id: writeBufferSnapshot[0].id, name: writeBufferSnapshot[0].name, tags: writeBufferSnapshot[0].tags } : 'null')
1531
+ console.log(`💾 Save: All records in allData:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
1532
+ console.log(`💾 Save: Sample existing record:`, existingRecords[0] ? { id: String(existingRecords[0].id), price: existingRecords[0].price, app_id: existingRecords[0].app_id, currency: existingRecords[0].currency } : 'null')
1533
+ console.log(`💾 Save: Sample writeBuffer record:`, writeBufferSnapshot[0] ? { id: String(writeBufferSnapshot[0].id), price: writeBufferSnapshot[0].price, app_id: writeBufferSnapshot[0].app_id, currency: writeBufferSnapshot[0].currency } : 'null')
1473
1534
  }
1474
1535
  }).catch(error => {
1475
1536
  if (this.opts.debugMode) {
@@ -1545,6 +1606,7 @@ class Database extends EventEmitter {
1545
1606
 
1546
1607
  if (this.opts.debugMode) {
1547
1608
  console.log(`💾 Save: allData.length=${allData.length}, cleanedData.length=${cleanedData.length}`)
1609
+ 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'))
1548
1610
  console.log(`💾 Save: Sample cleaned record:`, cleanedData[0] ? Object.keys(cleanedData[0]) : 'null')
1549
1611
  }
1550
1612
 
@@ -1556,6 +1618,7 @@ class Database extends EventEmitter {
1556
1618
 
1557
1619
  if (this.opts.debugMode) {
1558
1620
  console.log(`💾 Save: Serialized ${lines.length} lines`)
1621
+ console.log(`💾 Save: All records in allData after serialization check:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
1559
1622
  if (lines.length > 0) {
1560
1623
  console.log(`💾 Save: First line (first 200 chars):`, lines[0].substring(0, 200))
1561
1624
  }
@@ -1578,56 +1641,9 @@ class Database extends EventEmitter {
1578
1641
  console.log(`💾 Save: Calculated indexOffset: ${this.indexOffset}, allData.length: ${allData.length}`)
1579
1642
  }
1580
1643
 
1581
- // OPTIMIZATION: Parallel operations - file writing and index data preparation
1582
- const parallelWriteOperations = []
1583
-
1584
- // Add main file write operation
1585
- parallelWriteOperations.push(
1586
- this.fileHandler.writeBatch([jsonlData])
1587
- )
1588
-
1589
- // Add index file operations - ALWAYS save offsets, even without indexed fields
1590
- if (this.indexManager) {
1591
- const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb')
1592
-
1593
- // OPTIMIZATION: Parallel data preparation
1594
- const indexDataPromise = Promise.resolve({
1595
- index: this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {},
1596
- offsets: this.offsets, // Save actual offsets for efficient file operations
1597
- indexOffset: this.indexOffset // Save file size for proper range calculations
1598
- })
1599
-
1600
- // Add term mapping data if needed
1601
- const termMappingFields = this.getTermMappingFields()
1602
- if (termMappingFields.length > 0 && this.termManager) {
1603
- const termDataPromise = this.termManager.saveTerms()
1604
-
1605
- // Combine index data and term data
1606
- const combinedDataPromise = Promise.all([indexDataPromise, termDataPromise]).then(([indexData, termData]) => {
1607
- indexData.termMapping = termData
1608
- return indexData
1609
- })
1610
-
1611
- // Add index file write operation
1612
- parallelWriteOperations.push(
1613
- combinedDataPromise.then(indexData => {
1614
- const idxFileHandler = new FileHandler(idxPath, this.fileMutex, this.opts)
1615
- return idxFileHandler.writeAll(JSON.stringify(indexData, null, 2))
1616
- })
1617
- )
1618
- } else {
1619
- // Add index file write operation without term mapping
1620
- parallelWriteOperations.push(
1621
- indexDataPromise.then(indexData => {
1622
- const idxFileHandler = new FileHandler(idxPath, this.fileMutex, this.opts)
1623
- return idxFileHandler.writeAll(JSON.stringify(indexData, null, 2))
1624
- })
1625
- )
1626
- }
1627
- }
1628
-
1629
- // Execute parallel write operations
1630
- await Promise.all(parallelWriteOperations)
1644
+ // CRITICAL FIX: Write main data file first
1645
+ // Index will be saved AFTER reconstruction to ensure it contains correct data
1646
+ await this.fileHandler.writeBatch([jsonlData])
1631
1647
 
1632
1648
  if (this.opts.debugMode) {
1633
1649
  console.log(`💾 Saved ${allData.length} records to ${this.normalizedFile}`)
@@ -1661,9 +1677,16 @@ class Database extends EventEmitter {
1661
1677
 
1662
1678
  // Rebuild index from the saved records
1663
1679
  // CRITICAL: Process term mapping for records loaded from file to ensure ${field}Ids are available
1680
+ if (this.opts.debugMode) {
1681
+ console.log(`💾 Save: Rebuilding index from ${allData.length} records in allData`)
1682
+ }
1664
1683
  for (let i = 0; i < allData.length; i++) {
1665
1684
  let record = allData[i]
1666
1685
 
1686
+ if (this.opts.debugMode && i < 3) {
1687
+ console.log(`💾 Save: Rebuilding index record[${i}]:`, { id: String(record.id), price: record.price, app_id: record.app_id, currency: record.currency })
1688
+ }
1689
+
1667
1690
  // CRITICAL FIX: Ensure records have ${field}Ids for term mapping fields
1668
1691
  // Records from writeBuffer already have ${field}Ids from processTermMapping
1669
1692
  // Records from file need to be processed to restore ${field}Ids
@@ -1690,6 +1713,9 @@ class Database extends EventEmitter {
1690
1713
 
1691
1714
  await this.indexManager.add(record, i)
1692
1715
  }
1716
+ if (this.opts.debugMode) {
1717
+ console.log(`💾 Save: Index rebuilt with ${allData.length} records`)
1718
+ }
1693
1719
  }
1694
1720
  }
1695
1721
 
@@ -2574,11 +2600,20 @@ class Database extends EventEmitter {
2574
2600
 
2575
2601
  const updated = { ...record, ...updateData }
2576
2602
 
2603
+ // DEBUG: Log the update operation details
2604
+ if (this.opts.debugMode) {
2605
+ console.log(`🔄 UPDATE: Original record ID: ${record.id}, type: ${typeof record.id}`)
2606
+ console.log(`🔄 UPDATE: Updated record ID: ${updated.id}, type: ${typeof updated.id}`)
2607
+ console.log(`🔄 UPDATE: Update data keys:`, Object.keys(updateData))
2608
+ console.log(`🔄 UPDATE: Updated record keys:`, Object.keys(updated))
2609
+ }
2610
+
2577
2611
  // Process term mapping for update
2578
2612
  const termMappingStart = Date.now()
2579
2613
  this.processTermMapping(updated, true, record)
2580
2614
  if (this.opts.debugMode) {
2581
2615
  console.log(`🔄 UPDATE: Term mapping completed in ${Date.now() - termMappingStart}ms`)
2616
+ console.log(`🔄 UPDATE: After term mapping - ID: ${updated.id}, type: ${typeof updated.id}`)
2582
2617
  }
2583
2618
 
2584
2619
  // CRITICAL FIX: Remove old terms from index before adding new ones
@@ -4084,14 +4119,23 @@ class Database extends EventEmitter {
4084
4119
  // Create a map of updated records for quick lookup
4085
4120
  // CRITICAL FIX: Normalize IDs to strings for consistent comparison
4086
4121
  const updatedRecordsMap = new Map()
4087
- writeBufferSnapshot.forEach(record => {
4122
+ writeBufferSnapshot.forEach((record, index) => {
4088
4123
  if (record && record.id !== undefined && record.id !== null) {
4089
4124
  // Normalize ID to string for consistent comparison
4090
4125
  const normalizedId = String(record.id)
4091
4126
  updatedRecordsMap.set(normalizedId, record)
4127
+ if (this.opts.debugMode) {
4128
+ console.log(`💾 Save: Added to updatedRecordsMap: ID=${normalizedId} (original: ${record.id}, type: ${typeof record.id}), index=${index}`)
4129
+ }
4130
+ } else if (this.opts.debugMode) {
4131
+ console.log(`⚠️ Save: Skipped record in writeBufferSnapshot[${index}] - missing or invalid ID:`, record ? { id: record.id, keys: Object.keys(record) } : 'null')
4092
4132
  }
4093
4133
  })
4094
4134
 
4135
+ if (this.opts.debugMode) {
4136
+ console.log(`💾 Save: updatedRecordsMap size: ${updatedRecordsMap.size}, keys:`, Array.from(updatedRecordsMap.keys()))
4137
+ }
4138
+
4095
4139
  // OPTIMIZATION: Cache file stats to avoid repeated stat() calls
4096
4140
  let fileSize = 0
4097
4141
  if (this._cachedFileStats && this._cachedFileStats.timestamp > Date.now() - 1000) {
@@ -4301,11 +4345,20 @@ class Database extends EventEmitter {
4301
4345
 
4302
4346
  // CRITICAL FIX: Normalize ID to string for consistent comparison
4303
4347
  const normalizedId = String(recordWithIds.id)
4348
+ if (this.opts.debugMode) {
4349
+ console.log(`💾 Save: Checking record ID=${normalizedId} (original: ${recordWithIds.id}, type: ${typeof recordWithIds.id}) in updatedRecordsMap`)
4350
+ console.log(`💾 Save: updatedRecordsMap.has(${normalizedId}): ${updatedRecordsMap.has(normalizedId)}`)
4351
+ if (!updatedRecordsMap.has(normalizedId)) {
4352
+ console.log(`💾 Save: Record ${normalizedId} NOT found in updatedRecordsMap. Available keys:`, Array.from(updatedRecordsMap.keys()))
4353
+ }
4354
+ }
4304
4355
  if (updatedRecordsMap.has(normalizedId)) {
4305
4356
  // Replace with updated version
4306
4357
  const updatedRecord = updatedRecordsMap.get(normalizedId)
4307
4358
  if (this.opts.debugMode) {
4308
- console.log(`💾 Save: Updated record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`)
4359
+ console.log(`💾 Save: REPLACING record ${recordWithIds.id} with updated version`)
4360
+ console.log(`💾 Save: Old record:`, { id: recordWithIds.id, price: recordWithIds.price, app_id: recordWithIds.app_id, currency: recordWithIds.currency })
4361
+ console.log(`💾 Save: New record:`, { id: updatedRecord.id, price: updatedRecord.price, app_id: updatedRecord.app_id, currency: updatedRecord.currency })
4309
4362
  }
4310
4363
  return {
4311
4364
  type: 'updated',