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 +2 -2
- package/dist/Database.cjs +98 -77
- package/package.json +2 -1
- package/src/Database.mjs +89 -80
- package/src/FileHandler.mjs +68 -49
- package/src/Serializer.mjs +4 -1
- package/test/test-json-compressed.jdb +0 -0
- package/test/test-json.jdb +0 -0
- package/test/test-v8-compressed.jdb +0 -0
- package/test/test-v8.jdb +0 -0
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 =
|
|
424
|
+
_context5.next = 17;
|
|
423
425
|
break;
|
|
424
426
|
}
|
|
425
|
-
_context5.next =
|
|
427
|
+
_context5.next = 17;
|
|
426
428
|
return this.flush();
|
|
427
|
-
case
|
|
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
|
|
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 =
|
|
484
|
-
case
|
|
490
|
+
_context6.prev = 6;
|
|
491
|
+
case 7:
|
|
485
492
|
if (!this.writeBuffer.length) {
|
|
486
|
-
_context6.next =
|
|
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 =
|
|
501
|
+
_context6.next = 21;
|
|
495
502
|
break;
|
|
496
503
|
}
|
|
497
|
-
_context6.next =
|
|
504
|
+
_context6.next = 13;
|
|
498
505
|
return fd.close();
|
|
499
|
-
case
|
|
500
|
-
_context6.next =
|
|
506
|
+
case 13:
|
|
507
|
+
_context6.next = 15;
|
|
501
508
|
return this.fileHandler.truncate(this.writeBuffer.shift());
|
|
502
|
-
case
|
|
503
|
-
_context6.next =
|
|
509
|
+
case 15:
|
|
510
|
+
_context6.next = 17;
|
|
504
511
|
return _fs["default"].promises.open(this.fileHandler.file, 'a');
|
|
505
|
-
case
|
|
512
|
+
case 17:
|
|
506
513
|
fd = _context6.sent;
|
|
507
|
-
return _context6.abrupt("continue",
|
|
508
|
-
case
|
|
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
|
|
517
|
-
_context6.next =
|
|
523
|
+
case 22:
|
|
524
|
+
_context6.next = 24;
|
|
518
525
|
return fd.write(data);
|
|
519
|
-
case
|
|
520
|
-
_context6.next =
|
|
526
|
+
case 24:
|
|
527
|
+
_context6.next = 7;
|
|
521
528
|
break;
|
|
522
|
-
case
|
|
529
|
+
case 26:
|
|
523
530
|
this.shouldSave = true;
|
|
524
|
-
_context6.next =
|
|
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.
|
|
533
|
-
|
|
535
|
+
_context6.t0 = _context6["catch"](6);
|
|
536
|
+
console.error('Error flushing:', _context6.t0);
|
|
534
537
|
case 32:
|
|
535
|
-
|
|
536
|
-
|
|
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, [[
|
|
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,
|
|
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
|
-
|
|
595
|
-
_context7.next = 16;
|
|
605
|
+
_context7.next = 15;
|
|
596
606
|
return _awaitAsyncGenerator(_this.fileHandler.groupedRanges(ranges));
|
|
597
|
-
case
|
|
607
|
+
case 15:
|
|
598
608
|
groupedRanges = _context7.sent;
|
|
599
|
-
_context7.next =
|
|
609
|
+
_context7.next = 18;
|
|
600
610
|
return _awaitAsyncGenerator(_fs["default"].promises.open(_this.fileHandler.file, 'r'));
|
|
601
|
-
case
|
|
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 =
|
|
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 =
|
|
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 =
|
|
650
|
+
_context7.next = 43;
|
|
633
651
|
break;
|
|
634
652
|
}
|
|
635
|
-
_context7.next =
|
|
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 =
|
|
645
|
-
|
|
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.
|
|
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
|
|
659
|
-
_context7.prev =
|
|
660
|
-
_context7.prev =
|
|
676
|
+
case 54:
|
|
677
|
+
_context7.prev = 54;
|
|
678
|
+
_context7.prev = 55;
|
|
661
679
|
if (!(_iteratorAbruptCompletion && _iterator["return"] != null)) {
|
|
662
|
-
_context7.next =
|
|
680
|
+
_context7.next = 59;
|
|
663
681
|
break;
|
|
664
682
|
}
|
|
665
|
-
_context7.next =
|
|
683
|
+
_context7.next = 59;
|
|
666
684
|
return _awaitAsyncGenerator(_iterator["return"]());
|
|
667
|
-
case
|
|
668
|
-
_context7.prev =
|
|
685
|
+
case 59:
|
|
686
|
+
_context7.prev = 59;
|
|
669
687
|
if (!_didIteratorError) {
|
|
670
|
-
_context7.next =
|
|
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.
|
|
680
|
-
|
|
693
|
+
return _context7.finish(59);
|
|
694
|
+
case 63:
|
|
695
|
+
return _context7.finish(54);
|
|
681
696
|
case 64:
|
|
682
|
-
_context7.next =
|
|
697
|
+
_context7.next = 23;
|
|
683
698
|
break;
|
|
684
699
|
case 66:
|
|
685
|
-
_context7.
|
|
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
|
|
689
|
-
_context7.prev =
|
|
706
|
+
case 71:
|
|
707
|
+
_context7.prev = 71;
|
|
690
708
|
_iterator2.f();
|
|
691
|
-
return _context7.finish(
|
|
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,
|
|
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.
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
for
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
335
|
+
return 0
|
|
327
336
|
}
|
|
328
337
|
const ranges = this.getRanges([...matchingLines])
|
|
329
338
|
const validMatchingLines = new Set(ranges.map(r => r.index))
|
package/src/FileHandler.mjs
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
103
|
+
async *walk(ranges) {
|
|
91
104
|
const fd = await fs.promises.open(this.file, 'r')
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for
|
|
95
|
-
|
|
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
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await
|
|
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('
|
|
147
|
+
console.error('Erro ao substituir linhas:', e);
|
|
148
|
+
throw e;
|
|
130
149
|
} finally {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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) {
|
package/src/Serializer.mjs
CHANGED
|
@@ -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
|
|
64
|
+
throw new Error('Failed to deserialize JSON data')
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
if (isCompressed) {
|
|
Binary file
|
package/test/test-json.jdb
CHANGED
|
Binary file
|
|
Binary file
|
package/test/test-v8.jdb
CHANGED
|
Binary file
|