jexidb 1.0.6 → 1.0.8

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/.gitattributes CHANGED
@@ -1,2 +1,2 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/dist/Database.cjs CHANGED
@@ -8,6 +8,7 @@ var _events = require("events");
8
8
  var _FileHandler = _interopRequireDefault(require("./FileHandler.mjs"));
9
9
  var _IndexManager = _interopRequireDefault(require("./IndexManager.mjs"));
10
10
  var _Serializer = _interopRequireDefault(require("./Serializer.mjs"));
11
+ var _asyncMutex = require("async-mutex");
11
12
  var _fs = _interopRequireDefault(require("fs"));
12
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
13
14
  function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
@@ -67,6 +68,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
67
68
  _this2.indexManager = new _IndexManager["default"](_this2.opts);
68
69
  _this2.indexOffset = 0;
69
70
  _this2.writeBuffer = [];
71
+ _this2.mutex = new _asyncMutex.Mutex();
70
72
  return _this2;
71
73
  }
72
74
  _inherits(Database, _EventEmitter);
@@ -407,7 +409,8 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
407
409
  }
408
410
  _context5.next = 8;
409
411
  return this.serializer.serialize(data, {
410
- compress: this.opts.compress
412
+ compress: this.opts.compress,
413
+ v8: this.opts.v8
411
414
  });
412
415
  case 8:
413
416
  line = _context5.sent;
@@ -415,16 +418,16 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
415
418
  position = this.offsets.length;
416
419
  this.offsets.push(this.indexOffset);
417
420
  this.indexOffset += line.length;
418
- this.indexManager.add(data, position);
419
421
  this.emit('insert', data, position);
420
422
  this.writeBuffer.push(line);
421
423
  if (!(!this.flushing && this.currentWriteBufferSize() > this.opts.maxMemoryUsage)) {
422
- _context5.next = 18;
424
+ _context5.next = 17;
423
425
  break;
424
426
  }
425
- _context5.next = 18;
427
+ _context5.next = 17;
426
428
  return this.flush();
427
- case 18:
429
+ case 17:
430
+ this.indexManager.add(data, position);
428
431
  this.shouldSave = true;
429
432
  case 19:
430
433
  case "end":
@@ -472,18 +475,22 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
472
475
  key: "_flush",
473
476
  value: function () {
474
477
  var _flush2 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee6() {
475
- var fd, data, pos;
478
+ var release, fd, data, pos, err;
476
479
  return _regeneratorRuntime().wrap(function _callee6$(_context6) {
477
480
  while (1) switch (_context6.prev = _context6.next) {
478
481
  case 0:
479
482
  _context6.next = 2;
480
- return _fs["default"].promises.open(this.fileHandler.file, 'a');
483
+ return this.mutex.acquire();
481
484
  case 2:
485
+ release = _context6.sent;
486
+ _context6.next = 5;
487
+ return _fs["default"].promises.open(this.fileHandler.file, 'a');
488
+ case 5:
482
489
  fd = _context6.sent;
483
- _context6.prev = 3;
484
- case 4:
490
+ _context6.prev = 6;
491
+ case 7:
485
492
  if (!this.writeBuffer.length) {
486
- _context6.next = 23;
493
+ _context6.next = 26;
487
494
  break;
488
495
  }
489
496
  data = void 0;
@@ -491,21 +498,21 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
491
498
  return typeof b === 'number';
492
499
  });
493
500
  if (!(pos === 0)) {
494
- _context6.next = 18;
501
+ _context6.next = 21;
495
502
  break;
496
503
  }
497
- _context6.next = 10;
504
+ _context6.next = 13;
498
505
  return fd.close();
499
- case 10:
500
- _context6.next = 12;
506
+ case 13:
507
+ _context6.next = 15;
501
508
  return this.fileHandler.truncate(this.writeBuffer.shift());
502
- case 12:
503
- _context6.next = 14;
509
+ case 15:
510
+ _context6.next = 17;
504
511
  return _fs["default"].promises.open(this.fileHandler.file, 'a');
505
- case 14:
512
+ case 17:
506
513
  fd = _context6.sent;
507
- return _context6.abrupt("continue", 4);
508
- case 18:
514
+ return _context6.abrupt("continue", 7);
515
+ case 21:
509
516
  if (pos === -1) {
510
517
  data = Buffer.concat(this.writeBuffer);
511
518
  this.writeBuffer.length = 0;
@@ -513,31 +520,35 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
513
520
  data = Buffer.concat(this.writeBuffer.slice(0, pos));
514
521
  this.writeBuffer.splice(0, pos);
515
522
  }
516
- case 19:
517
- _context6.next = 21;
523
+ case 22:
524
+ _context6.next = 24;
518
525
  return fd.write(data);
519
- case 21:
520
- _context6.next = 4;
526
+ case 24:
527
+ _context6.next = 7;
521
528
  break;
522
- case 23:
529
+ case 26:
523
530
  this.shouldSave = true;
524
- _context6.next = 29;
531
+ _context6.next = 32;
525
532
  break;
526
- case 26:
527
- _context6.prev = 26;
528
- _context6.t0 = _context6["catch"](3);
529
- console.error('Error flushing:', _context6.t0);
530
533
  case 29:
531
534
  _context6.prev = 29;
532
- _context6.next = 32;
533
- return fd.close();
535
+ _context6.t0 = _context6["catch"](6);
536
+ console.error('Error flushing:', _context6.t0);
534
537
  case 32:
535
- return _context6.finish(29);
536
- case 33:
538
+ _context6.prev = 32;
539
+ _context6.next = 35;
540
+ return fd.close()["catch"](function (e) {
541
+ return err = e;
542
+ });
543
+ case 35:
544
+ release();
545
+ err && console.error('Error closing file:', err);
546
+ return _context6.finish(32);
547
+ case 38:
537
548
  case "end":
538
549
  return _context6.stop();
539
550
  }
540
- }, _callee6, this, [[3, 26, 29, 33]]);
551
+ }, _callee6, this, [[6, 29, 32, 38]]);
541
552
  }));
542
553
  function _flush() {
543
554
  return _flush2.apply(this, arguments);
@@ -550,7 +561,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
550
561
  var _this = this;
551
562
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
552
563
  return _wrapAsyncGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee7() {
553
- var ranges, readSize, groupedRanges, fd, _iterator2, _step2, groupedRange, _iteratorAbruptCompletion, _didIteratorError, _iteratorError, _iterator, _step, row, entry;
564
+ var ranges, groupedRanges, fd, _iterator2, _step2, groupedRange, _iteratorAbruptCompletion, _didIteratorError, _iteratorError, _iterator, _step, row, entry;
554
565
  return _regeneratorRuntime().wrap(function _callee7$(_context7) {
555
566
  while (1) switch (_context7.prev = _context7.next) {
556
567
  case 0:
@@ -591,21 +602,21 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
591
602
  }
592
603
  }
593
604
  ranges = _this.getRanges(map);
594
- readSize = 512 * 1024; // 512KB
595
- _context7.next = 16;
605
+ _context7.next = 15;
596
606
  return _awaitAsyncGenerator(_this.fileHandler.groupedRanges(ranges));
597
- case 16:
607
+ case 15:
598
608
  groupedRanges = _context7.sent;
599
- _context7.next = 19;
609
+ _context7.next = 18;
600
610
  return _awaitAsyncGenerator(_fs["default"].promises.open(_this.fileHandler.file, 'r'));
601
- case 19:
611
+ case 18:
602
612
  fd = _context7.sent;
613
+ _context7.prev = 19;
603
614
  _iterator2 = _createForOfIteratorHelper(groupedRanges);
604
615
  _context7.prev = 21;
605
616
  _iterator2.s();
606
617
  case 23:
607
618
  if ((_step2 = _iterator2.n()).done) {
608
- _context7.next = 64;
619
+ _context7.next = 66;
609
620
  break;
610
621
  }
611
622
  groupedRange = _step2.value;
@@ -618,85 +629,95 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
618
629
  return _awaitAsyncGenerator(_iterator.next());
619
630
  case 31:
620
631
  if (!(_iteratorAbruptCompletion = !(_step = _context7.sent).done)) {
621
- _context7.next = 46;
632
+ _context7.next = 48;
622
633
  break;
623
634
  }
624
635
  row = _step.value;
625
636
  _context7.next = 35;
626
637
  return _awaitAsyncGenerator(_this.serializer.deserialize(row.line, {
627
- compress: _this.opts.compress
638
+ compress: _this.opts.compress,
639
+ v8: _this.opts.v8
628
640
  }));
629
641
  case 35:
630
642
  entry = _context7.sent;
643
+ if (!(entry === null)) {
644
+ _context7.next = 38;
645
+ break;
646
+ }
647
+ return _context7.abrupt("continue", 45);
648
+ case 38:
631
649
  if (!options.includeOffsets) {
632
- _context7.next = 41;
650
+ _context7.next = 43;
633
651
  break;
634
652
  }
635
- _context7.next = 39;
653
+ _context7.next = 41;
636
654
  return {
637
655
  entry: entry,
638
656
  start: row.start
639
657
  };
640
- case 39:
641
- _context7.next = 43;
642
- break;
643
658
  case 41:
644
- _context7.next = 43;
645
- return entry;
659
+ _context7.next = 45;
660
+ break;
646
661
  case 43:
662
+ _context7.next = 45;
663
+ return entry;
664
+ case 45:
647
665
  _iteratorAbruptCompletion = false;
648
666
  _context7.next = 29;
649
667
  break;
650
- case 46:
651
- _context7.next = 52;
652
- break;
653
668
  case 48:
654
- _context7.prev = 48;
669
+ _context7.next = 54;
670
+ break;
671
+ case 50:
672
+ _context7.prev = 50;
655
673
  _context7.t1 = _context7["catch"](27);
656
674
  _didIteratorError = true;
657
675
  _iteratorError = _context7.t1;
658
- case 52:
659
- _context7.prev = 52;
660
- _context7.prev = 53;
676
+ case 54:
677
+ _context7.prev = 54;
678
+ _context7.prev = 55;
661
679
  if (!(_iteratorAbruptCompletion && _iterator["return"] != null)) {
662
- _context7.next = 57;
680
+ _context7.next = 59;
663
681
  break;
664
682
  }
665
- _context7.next = 57;
683
+ _context7.next = 59;
666
684
  return _awaitAsyncGenerator(_iterator["return"]());
667
- case 57:
668
- _context7.prev = 57;
685
+ case 59:
686
+ _context7.prev = 59;
669
687
  if (!_didIteratorError) {
670
- _context7.next = 60;
688
+ _context7.next = 62;
671
689
  break;
672
690
  }
673
691
  throw _iteratorError;
674
- case 60:
675
- return _context7.finish(57);
676
- case 61:
677
- return _context7.finish(52);
678
692
  case 62:
679
- _context7.next = 23;
680
- break;
693
+ return _context7.finish(59);
694
+ case 63:
695
+ return _context7.finish(54);
681
696
  case 64:
682
- _context7.next = 69;
697
+ _context7.next = 23;
683
698
  break;
684
699
  case 66:
685
- _context7.prev = 66;
700
+ _context7.next = 71;
701
+ break;
702
+ case 68:
703
+ _context7.prev = 68;
686
704
  _context7.t2 = _context7["catch"](21);
687
705
  _iterator2.e(_context7.t2);
688
- case 69:
689
- _context7.prev = 69;
706
+ case 71:
707
+ _context7.prev = 71;
690
708
  _iterator2.f();
691
- return _context7.finish(69);
692
- case 72:
693
- _context7.next = 74;
694
- return _awaitAsyncGenerator(fd.close());
709
+ return _context7.finish(71);
695
710
  case 74:
711
+ _context7.prev = 74;
712
+ _context7.next = 77;
713
+ return _awaitAsyncGenerator(fd.close());
714
+ case 77:
715
+ return _context7.finish(74);
716
+ case 78:
696
717
  case "end":
697
718
  return _context7.stop();
698
719
  }
699
- }, _callee7, null, [[21, 66, 69, 72], [27, 48, 52, 62], [53,, 57, 61]]);
720
+ }, _callee7, null, [[19,, 74, 78], [21, 68, 71, 74], [27, 50, 54, 64], [55,, 59, 63]]);
700
721
  }))();
701
722
  }
702
723
  }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jexidb",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "JexiDB is a pure JS NPM library for managing data on disk using JSONL efficiently, without the need for a server.",
5
5
  "main": "./dist/Database.cjs",
6
6
  "module": "./src/Database.mjs",
@@ -23,6 +23,7 @@
23
23
  "@babel/preset-env": "^7.25.4"
24
24
  },
25
25
  "dependencies": {
26
+ "async-mutex": "^0.5.0",
26
27
  "p-limit": "^6.1.0"
27
28
  },
28
29
  "directories": {
package/src/Database.mjs CHANGED
@@ -2,14 +2,15 @@ import { EventEmitter } from 'events'
2
2
  import FileHandler from './FileHandler.mjs'
3
3
  import IndexManager from './IndexManager.mjs'
4
4
  import Serializer from './Serializer.mjs'
5
+ import { Mutex } from 'async-mutex'
5
6
  import fs from 'fs'
6
7
 
7
8
  export class Database extends EventEmitter {
8
- constructor(file, opts={}) {
9
+ constructor(file, opts = {}) {
9
10
  super()
10
11
  this.opts = Object.assign({
11
12
  v8: false,
12
- index: {data: {}},
13
+ index: { data: {} },
13
14
  indexes: {},
14
15
  compress: false,
15
16
  compressIndex: false,
@@ -22,29 +23,30 @@ export class Database extends EventEmitter {
22
23
  this.indexManager = new IndexManager(this.opts)
23
24
  this.indexOffset = 0
24
25
  this.writeBuffer = []
26
+ this.mutex = new Mutex()
25
27
  }
26
28
 
27
29
  use(plugin) {
28
- if(this.destroyed) throw new Error('Database is destroyed')
30
+ if (this.destroyed) throw new Error('Database is destroyed')
29
31
  plugin(this)
30
32
  }
31
33
 
32
34
  async init() {
33
- if(this.destroyed) throw new Error('Database is destroyed')
34
- if(this.initialized) return
35
- if(this.initlializing) return await new Promise(resolve => this.once('init', resolve))
35
+ if (this.destroyed) throw new Error('Database is destroyed')
36
+ if (this.initialized) return
37
+ if (this.initlializing) return await new Promise(resolve => this.once('init', resolve))
36
38
  this.initializing = true
37
39
  try {
38
- if(this.opts.clear) {
40
+ if (this.opts.clear) {
39
41
  await this.fileHandler.truncate(0).catch(console.error)
40
42
  throw new Error('Cleared, empty file')
41
43
  }
42
44
  const lastLine = await this.fileHandler.readLastLine()
43
- if(!lastLine || !lastLine.length) {
45
+ if (!lastLine || !lastLine.length) {
44
46
  throw new Error('File does not exists or is a empty file')
45
47
  }
46
- const offsets = await this.serializer.deserialize(lastLine, {compress: this.opts.compressIndex})
47
- if(!Array.isArray(offsets)) {
48
+ const offsets = await this.serializer.deserialize(lastLine, { compress: this.opts.compressIndex })
49
+ if (!Array.isArray(offsets)) {
48
50
  throw new Error('File to parse offsets, expected an array')
49
51
  }
50
52
  this.indexOffset = offsets[offsets.length - 2]
@@ -53,14 +55,14 @@ export class Database extends EventEmitter {
53
55
  this.offsets = this.offsets.slice(0, -2)
54
56
  this.shouldTruncate = true
55
57
  let indexLine = await this.fileHandler.readRange(...ptr)
56
- const index = await this.serializer.deserialize(indexLine, {compress: this.opts.compressIndex})
58
+ const index = await this.serializer.deserialize(indexLine, { compress: this.opts.compressIndex })
57
59
  index && this.indexManager.load(index)
58
60
  } catch (e) {
59
- if(Array.isArray(this.offsets)) {
61
+ if (Array.isArray(this.offsets)) {
60
62
  this.offsets = []
61
63
  }
62
64
  this.indexOffset = 0
63
- if(!String(e).includes('empty file')) {
65
+ if (!String(e).includes('empty file')) {
64
66
  console.error('Error loading database:', e)
65
67
  }
66
68
  } finally {
@@ -71,30 +73,30 @@ export class Database extends EventEmitter {
71
73
  }
72
74
 
73
75
  async save() {
74
- if(this.destroyed) throw new Error('Database is destroyed')
75
- if(!this.initialized) throw new Error('Database not initialized')
76
- if(this.saving) return new Promise(resolve => this.once('save', resolve))
76
+ if (this.destroyed) throw new Error('Database is destroyed')
77
+ if (!this.initialized) throw new Error('Database not initialized')
78
+ if (this.saving) return new Promise(resolve => this.once('save', resolve))
77
79
  this.saving = true
78
80
  await this.flush()
79
81
  if (!this.shouldSave) return
80
82
  this.emit('before-save')
81
- const index = Object.assign({data: {}}, this.indexManager.index)
82
- for(const field in this.indexManager.index.data) {
83
- for(const term in this.indexManager.index.data[field]) {
83
+ const index = Object.assign({ data: {} }, this.indexManager.index)
84
+ for (const field in this.indexManager.index.data) {
85
+ for (const term in this.indexManager.index.data[field]) {
84
86
  index.data[field][term] = [...this.indexManager.index.data[field][term]] // set to array
85
87
  }
86
88
  }
87
89
  const offsets = this.offsets.slice(0)
88
- const indexString = await this.serializer.serialize(index, {compress: this.opts.compressIndex, linebreak: true}) // force linebreak here to allow 'init' to read last line as offsets correctly
89
- for(const field in this.indexManager.index.data) {
90
- for(const term in this.indexManager.index.data[field]) {
90
+ const indexString = await this.serializer.serialize(index, { compress: this.opts.compressIndex, linebreak: true }) // force linebreak here to allow 'init' to read last line as offsets correctly
91
+ for (const field in this.indexManager.index.data) {
92
+ for (const term in this.indexManager.index.data[field]) {
91
93
  this.indexManager.index.data[field][term] = new Set(index.data[field][term]) // set back to set because of serialization
92
94
  }
93
95
  }
94
96
  offsets.push(this.indexOffset)
95
97
  offsets.push(this.indexOffset + indexString.length)
96
98
  // save offsets as JSON always to prevent linebreaks on last line, which breaks 'init()'
97
- const offsetsString = await this.serializer.serialize(offsets, {json: true, compress: false, linebreak: false})
99
+ const offsetsString = await this.serializer.serialize(offsets, { json: true, compress: false, linebreak: false })
98
100
  this.writeBuffer.push(indexString)
99
101
  this.writeBuffer.push(offsetsString)
100
102
  await this.flush() // write the index and offsets
@@ -112,7 +114,7 @@ export class Database extends EventEmitter {
112
114
 
113
115
  locate(n) {
114
116
  if (this.offsets[n] === undefined) {
115
- if(this.offsets[n - 1]) {
117
+ if (this.offsets[n - 1]) {
116
118
  return [this.indexOffset, Number.MAX_SAFE_INTEGER]
117
119
  }
118
120
  return
@@ -120,58 +122,58 @@ export class Database extends EventEmitter {
120
122
  let end = (this.offsets[n + 1] || this.indexOffset || Number.MAX_SAFE_INTEGER)
121
123
  return [this.offsets[n], end]
122
124
  }
123
-
125
+
124
126
  getRanges(map) {
125
127
  return (map || Array.from(this.offsets.keys())).map(n => {
126
- const ret = this.locate(n)
127
- if(ret !== undefined) return {start: ret[0], end: ret[1], index: n}
128
+ const ret = this.locate(n)
129
+ if (ret !== undefined) return { start: ret[0], end: ret[1], index: n }
128
130
  }).filter(n => n !== undefined)
129
131
  }
130
132
 
131
133
  async readLines(map, ranges) {
132
- if(!ranges) ranges = this.getRanges(map)
134
+ if (!ranges) ranges = this.getRanges(map)
133
135
  const results = await this.fileHandler.readRanges(ranges, this.serializer.deserialize.bind(this.serializer))
134
136
  let i = 0
135
- for(const start in results) {
136
- if(!results[start] || results[start]._ !== undefined) continue
137
- while(this.offsets[i] != start && i < map.length) i++ // weak comparison as 'start' is a string
137
+ for (const start in results) {
138
+ if (!results[start] || results[start]._ !== undefined) continue
139
+ while (this.offsets[i] != start && i < map.length) i++ // weak comparison as 'start' is a string
138
140
  results[start]._ = map[i++]
139
141
  }
140
142
  return Object.values(results).filter(r => r !== undefined)
141
143
  }
142
144
 
143
145
  async insert(data) {
144
- if(this.destroyed) throw new Error('Database is destroyed')
145
- if(!this.initialized) await this.init()
146
+ if (this.destroyed) throw new Error('Database is destroyed')
147
+ if (!this.initialized) await this.init()
146
148
  if (this.shouldTruncate) {
147
- this.writeBuffer.push(this.indexOffset)
148
- this.shouldTruncate = false
149
+ this.writeBuffer.push(this.indexOffset)
150
+ this.shouldTruncate = false
149
151
  }
150
- const line = await this.serializer.serialize(data, {compress: this.opts.compress}) // using Buffer for offsets accuracy
152
+ const line = await this.serializer.serialize(data, { compress: this.opts.compress, v8: this.opts.v8 }) // using Buffer for offsets accuracy
151
153
  const position = this.offsets.length
152
154
  this.offsets.push(this.indexOffset)
153
155
  this.indexOffset += line.length
154
- this.indexManager.add(data, position)
155
156
  this.emit('insert', data, position)
156
157
  this.writeBuffer.push(line)
157
- if(!this.flushing && this.currentWriteBufferSize() > this.opts.maxMemoryUsage) {
158
+ if (!this.flushing && this.currentWriteBufferSize() > this.opts.maxMemoryUsage) {
158
159
  await this.flush()
159
160
  }
161
+ this.indexManager.add(data, position)
160
162
  this.shouldSave = true
161
163
  }
162
164
 
163
- currentWriteBufferSize(){
165
+ currentWriteBufferSize() {
164
166
  const lengths = this.writeBuffer.filter(b => Buffer.isBuffer(b)).map(b => b.length)
165
167
  return lengths.reduce((a, b) => a + b, 0)
166
168
  }
167
169
 
168
170
  flush() {
169
- if(this.flushing) {
171
+ if (this.flushing) {
170
172
  return this.flushing
171
173
  }
172
174
  return this.flushing = new Promise((resolve, reject) => {
173
- if(this.destroyed) return reject(new Error('Database is destroyed'))
174
- if(!this.writeBuffer.length) return resolve()
175
+ if (this.destroyed) return reject(new Error('Database is destroyed'))
176
+ if (!this.writeBuffer.length) return resolve()
175
177
  let err
176
178
  this._flush().catch(e => err = e).finally(() => {
177
179
  err ? reject(err) : resolve()
@@ -181,17 +183,18 @@ export class Database extends EventEmitter {
181
183
  }
182
184
 
183
185
  async _flush() {
186
+ const release = await this.mutex.acquire()
184
187
  let fd = await fs.promises.open(this.fileHandler.file, 'a')
185
188
  try {
186
- while(this.writeBuffer.length) {
189
+ while (this.writeBuffer.length) {
187
190
  let data
188
191
  const pos = this.writeBuffer.findIndex(b => typeof b === 'number')
189
- if(pos === 0) {
192
+ if (pos === 0) {
190
193
  await fd.close()
191
194
  await this.fileHandler.truncate(this.writeBuffer.shift())
192
195
  fd = await fs.promises.open(this.fileHandler.file, 'a')
193
196
  continue
194
- } else if(pos === -1) {
197
+ } else if (pos === -1) {
195
198
  data = Buffer.concat(this.writeBuffer)
196
199
  this.writeBuffer.length = 0
197
200
  } else {
@@ -201,66 +204,72 @@ export class Database extends EventEmitter {
201
204
  await fd.write(data)
202
205
  }
203
206
  this.shouldSave = true
204
- } catch(err) {
207
+ } catch (err) {
205
208
  console.error('Error flushing:', err)
206
209
  } finally {
207
- await fd.close()
210
+ let err
211
+ await fd.close().catch(e => err = e)
212
+ release()
213
+ err && console.error('Error closing file:', err)
208
214
  }
209
215
  }
210
216
 
211
- async *walk(map, options={}) {
212
- if(this.destroyed) throw new Error('Database is destroyed')
213
- if(!this.initialized) await this.init()
217
+ async *walk(map, options = {}) {
218
+ if (this.destroyed) throw new Error('Database is destroyed')
219
+ if (!this.initialized) await this.init()
214
220
  this.shouldSave && await this.save().catch(console.error)
215
- if(this.indexOffset === 0) return
216
- if(!Array.isArray(map)) {
221
+ if (this.indexOffset === 0) return
222
+ if (!Array.isArray(map)) {
217
223
  if (map instanceof Set) {
218
224
  map = [...map]
219
- } else if(map && typeof map === 'object') {
225
+ } else if (map && typeof map === 'object') {
220
226
  map = [...this.indexManager.query(map, options)]
221
227
  } else {
222
228
  map = [...Array(this.offsets.length).keys()]
223
229
  }
224
230
  }
225
231
  const ranges = this.getRanges(map)
226
- const readSize = 512 * 1024 // 512KB
227
232
  const groupedRanges = await this.fileHandler.groupedRanges(ranges)
228
233
  const fd = await fs.promises.open(this.fileHandler.file, 'r')
229
- for(const groupedRange of groupedRanges) {
230
- for await (const row of this.fileHandler.readGroupedRange(groupedRange, fd)) {
231
- const entry = await this.serializer.deserialize(row.line, {compress: this.opts.compress})
232
- if(options.includeOffsets) {
233
- yield {entry, start: row.start}
234
- } else {
235
- yield entry
234
+ try {
235
+ for (const groupedRange of groupedRanges) {
236
+ for await (const row of this.fileHandler.readGroupedRange(groupedRange, fd)) {
237
+ const entry = await this.serializer.deserialize(row.line, { compress: this.opts.compress, v8: this.opts.v8 })
238
+ if (entry === null) continue
239
+ if (options.includeOffsets) {
240
+ yield { entry, start: row.start }
241
+ } else {
242
+ yield entry
243
+ }
236
244
  }
237
245
  }
246
+ } finally {
247
+ await fd.close()
238
248
  }
239
- await fd.close()
240
249
  }
241
250
 
242
- async query(criteria, options={}) {
243
- if(this.destroyed) throw new Error('Database is destroyed')
244
- if(!this.initialized) await this.init()
251
+ async query(criteria, options = {}) {
252
+ if (this.destroyed) throw new Error('Database is destroyed')
253
+ if (!this.initialized) await this.init()
245
254
  this.shouldSave && await this.save().catch(console.error)
246
- if(Array.isArray(criteria)) {
255
+ if (Array.isArray(criteria)) {
247
256
  let results = await this.readLines(criteria)
248
257
  if (options.orderBy) {
249
- const [field, direction = 'asc'] = options.orderBy.split(' ')
250
- results.sort((a, b) => {
251
- if (a[field] > b[field]) return direction === 'asc' ? 1 : -1
252
- if (a[field] < b[field]) return direction === 'asc' ? -1 : 1
253
- return 0;
254
- })
258
+ const [field, direction = 'asc'] = options.orderBy.split(' ')
259
+ results.sort((a, b) => {
260
+ if (a[field] > b[field]) return direction === 'asc' ? 1 : -1
261
+ if (a[field] < b[field]) return direction === 'asc' ? -1 : 1
262
+ return 0;
263
+ })
255
264
  }
256
265
  if (options.limit) {
257
- results = results.slice(0, options.limit);
266
+ results = results.slice(0, options.limit);
258
267
  }
259
268
  return results
260
269
  } else {
261
270
  const matchingLines = await this.indexManager.query(criteria, options)
262
271
  if (!matchingLines || !matchingLines.size) {
263
- return []
272
+ return []
264
273
  }
265
274
  return await this.query([...matchingLines], options)
266
275
  }
@@ -313,17 +322,17 @@ export class Database extends EventEmitter {
313
322
  return entries
314
323
  }
315
324
 
316
- async delete(criteria, options={}) {
325
+ async delete(criteria, options = {}) {
317
326
  if (this.shouldTruncate) {
318
- this.writeBuffer.push(this.indexOffset)
319
- this.shouldTruncate = false
327
+ this.writeBuffer.push(this.indexOffset)
328
+ this.shouldTruncate = false
320
329
  }
321
- if(this.destroyed) throw new Error('Database is destroyed')
322
- if(!this.initialized) await this.init()
330
+ if (this.destroyed) throw new Error('Database is destroyed')
331
+ if (!this.initialized) await this.init()
323
332
  this.shouldSave && await this.save().catch(console.error)
324
333
  const matchingLines = await this.indexManager.query(criteria, options)
325
334
  if (!matchingLines || !matchingLines.size) {
326
- return 0
335
+ return 0
327
336
  }
328
337
  const ranges = this.getRanges([...matchingLines])
329
338
  const validMatchingLines = new Set(ranges.map(r => r.index))
@@ -24,17 +24,19 @@ export default class FileHandler {
24
24
  if(buffer.length > bytesRead) return buffer.subarray(0, bytesRead)
25
25
  return buffer
26
26
  }
27
-
27
+
28
28
  async readRanges(ranges, mapper) {
29
29
  const lines = {}, limit = pLimit(4)
30
30
  const fd = await fs.promises.open(this.file, 'r')
31
31
  const groupedRanges = await this.groupedRanges(ranges)
32
32
  try {
33
- for(const groupedRange of groupedRanges) {
34
- for await (const row of this.readGroupedRange(groupedRange, fd)) {
35
- lines[row.start] = mapper ? (await mapper(row.line, groupedRange)) : row.line
36
- }
37
- }
33
+ await Promise.allSettled(groupedRanges.map(async (groupedRange) => {
34
+ await limit(async () => {
35
+ for await (const row of this.readGroupedRange(groupedRange, fd)) {
36
+ lines[row.start] = mapper ? (await mapper(row.line, groupedRange)) : row.line
37
+ }
38
+ })
39
+ }))
38
40
  } catch (e) {
39
41
  console.error('Error reading ranges:', e)
40
42
  } finally {
@@ -43,7 +45,7 @@ export default class FileHandler {
43
45
  return lines
44
46
  }
45
47
 
46
- async groupedRanges(ranges) {
48
+ async groupedRanges(ranges) { // expects ordered ranges from Database.getRanges()
47
49
  const readSize = 512 * 1024 // 512KB
48
50
  const groupedRanges = []
49
51
  let currentGroup = []
@@ -79,62 +81,78 @@ export default class FileHandler {
79
81
  let i = 0, buffer = Buffer.alloc(options.end - options.start)
80
82
  const results = {}, { bytesRead } = await fd.read(buffer, 0, options.end - options.start, options.start)
81
83
  if(buffer.length > bytesRead) buffer = buffer.subarray(0, bytesRead)
82
- for(const range of groupedRange) {
83
- const line = buffer.subarray(range.start, range.end)
84
- yield {line, start: range.start}
84
+
85
+ for (const range of groupedRange) {
86
+ const startOffset = range.start - options.start;
87
+ let endOffset = range.end - options.start;
88
+ if (endOffset > buffer.length) {
89
+ endOffset = buffer.length;
90
+ }
91
+ if (startOffset >= buffer.length) {
92
+ continue;
93
+ }
94
+ const line = buffer.subarray(startOffset, endOffset);
95
+ if (line.length === 0) continue;
96
+ yield { line, start: range.start };
85
97
  }
86
98
 
99
+
87
100
  return results
88
101
  }
89
102
 
90
- async *walk(ranges, options={}) {
103
+ async *walk(ranges) {
91
104
  const fd = await fs.promises.open(this.file, 'r')
92
- const groupedRanges = await this.groupedRanges(ranges)
93
- for(const groupedRange of groupedRanges) {
94
- for await (const row of this.readGroupedRange(groupedRange, fd)) {
95
- yield row
105
+ try {
106
+ const groupedRanges = await this.groupedRanges(ranges)
107
+ for(const groupedRange of groupedRanges) {
108
+ for await (const row of this.readGroupedRange(groupedRange, fd)) {
109
+ yield row
110
+ }
96
111
  }
112
+ } finally {
113
+ await fd.close()
97
114
  }
98
- await fd.close()
99
115
  }
100
116
 
101
117
  async replaceLines(ranges, lines) {
102
- let closed
103
- const tmpFile = this.file + '.tmp'
104
- const writer = await fs.promises.open(tmpFile, 'w+')
105
- const reader = await fs.promises.open(this.file, 'r')
118
+ const tmpFile = this.file + '.tmp';
119
+ const writer = await fs.promises.open(tmpFile, 'w+');
120
+ const reader = await fs.promises.open(this.file, 'r');
106
121
  try {
107
- let i = 0, start = 0
108
- for (const r of ranges) {
109
- const length = r.start - start
110
- const buffer = Buffer.alloc(length)
111
- await reader.read(buffer, 0, length, start)
112
- start = r.end
113
- buffer.length && await writer.write(buffer)
114
- if (lines[i]) {
115
- await writer.write(lines[i])
122
+ let position = 0;
123
+ let lineIndex = 0;
124
+
125
+ for (const range of ranges) {
126
+ if (position < range.start) {
127
+ const buffer = await this.readRange(position, range.start);
128
+ await writer.write(buffer);
116
129
  }
117
- i++
130
+ if (lineIndex < lines.length && lines[lineIndex]) {
131
+ await writer.write(lines[lineIndex]);
132
+ }
133
+ position = range.end;
134
+ lineIndex++;
135
+ }
136
+
137
+ const { size } = await reader.stat();
138
+ if (position < size) {
139
+ const buffer = await this.readRange(position, size);
140
+ await writer.write(buffer);
118
141
  }
119
- const size = (await reader.stat()).size
120
- const length = size - start
121
- const buffer = Buffer.alloc(length)
122
- await reader.read(buffer, 0, length, start)
123
- await writer.write(buffer)
124
- await reader.close()
125
- await writer.close()
126
- closed = true
127
- await fs.promises.copyFile(tmpFile, this.file)
142
+
143
+ await reader.close();
144
+ await writer.close();
145
+ await fs.promises.rename(tmpFile, this.file);
128
146
  } catch (e) {
129
- console.error('Error replacing lines:', e)
147
+ console.error('Erro ao substituir linhas:', e);
148
+ throw e;
130
149
  } finally {
131
- if(!closed) {
132
- await reader.close()
133
- await writer.close()
134
- }
135
- await fs.promises.unlink(tmpFile).catch(() => {})
150
+ await reader.close().catch(() => { });
151
+ await writer.close().catch(() => { });
152
+ await fs.promises.unlink(tmpFile).catch(() => { });
136
153
  }
137
154
  }
155
+
138
156
  async writeData(data, immediate, fd) {
139
157
  await fd.write(data)
140
158
  }
@@ -150,18 +168,19 @@ export default class FileHandler {
150
168
  if (size < 1) throw 'empty file'
151
169
  this.size = size
152
170
  const bufferSize = 16384
153
- let buffer, lastReadSize, readPosition = Math.max(size - bufferSize, 0)
171
+ let buffer, isFirstRead = true, lastReadSize, readPosition = Math.max(size - bufferSize, 0)
154
172
  while (readPosition >= 0) {
155
173
  const readSize = Math.min(bufferSize, size - readPosition)
156
174
  if (readSize !== lastReadSize) {
157
175
  lastReadSize = readSize
158
176
  buffer = Buffer.alloc(readSize)
159
177
  }
160
- const { bytesRead } = await reader.read(buffer, 0, readSize, readPosition)
178
+ const { bytesRead } = await reader.read(buffer, 0, isFirstRead ? (readSize - 1) : readSize, readPosition)
179
+ if (isFirstRead) isFirstRead = false
161
180
  if (bytesRead === 0) break
162
- const newlineIndex = buffer.lastIndexOf(10, size - 4) // 0x0A is the ASCII code for '\n'
181
+ const newlineIndex = buffer.lastIndexOf(10)
182
+ const start = readPosition + newlineIndex + 1
163
183
  if (newlineIndex !== -1) {
164
- const start = readPosition + newlineIndex + 1
165
184
  const lastLine = Buffer.alloc(size - start)
166
185
  await reader.read(lastLine, 0, size - start, start)
167
186
  if (!lastLine || !lastLine.length) {
@@ -46,6 +46,9 @@ export default class Serializer extends EventEmitter {
46
46
  }
47
47
 
48
48
  async deserialize(data) {
49
+ if(data.length === 0) {
50
+ return null
51
+ }
49
52
  let line, isCompressed, isV8
50
53
  const header = data.readUInt8(0)
51
54
  const valid = header === 0x00 || header === 0x01 || header === 0x02 || header === 0x03
@@ -58,7 +61,7 @@ export default class Serializer extends EventEmitter {
58
61
  try {
59
62
  return JSON.parse(data.toString('utf-8').trim())
60
63
  } catch (e) {
61
- throw new Error('Failed to deserialize legacy JSON data')
64
+ throw new Error('Failed to deserialize JSON data')
62
65
  }
63
66
  }
64
67
  if (isCompressed) {
Binary file
Binary file
Binary file
package/test/test-v8.jdb CHANGED
Binary file