jexidb 1.0.2 → 1.0.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/README.md +2 -0
- package/dist/Database.cjs +173 -124
- package/package.json +2 -2
- package/src/Database.mjs +35 -25
- package/src/FileHandler.mjs +12 -12
- package/src/{serializers/Advanced.mjs → Serializer.mjs} +38 -41
- package/test/README.md +13 -0
- 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/test/test.mjs +50 -34
- package/src/serializers/Simple.mjs +0 -21
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ const db = new Database('path/to/database.jdb', { // file will be created if it
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
|
+
You can [learn a bit more about these options at this link](https://github.com/EdenwareApps/jexidb/tree/main/test#readme).
|
|
39
|
+
|
|
38
40
|
|
|
39
41
|
### Initializing the Database
|
|
40
42
|
|
package/dist/Database.cjs
CHANGED
|
@@ -7,8 +7,7 @@ exports.Database = void 0;
|
|
|
7
7
|
var _events = require("events");
|
|
8
8
|
var _FileHandler = _interopRequireDefault(require("./FileHandler.mjs"));
|
|
9
9
|
var _IndexManager = _interopRequireDefault(require("./IndexManager.mjs"));
|
|
10
|
-
var
|
|
11
|
-
var _Advanced = _interopRequireDefault(require("./serializers/Advanced.mjs"));
|
|
10
|
+
var _Serializer = _interopRequireDefault(require("./Serializer.mjs"));
|
|
12
11
|
var _fs = _interopRequireDefault(require("fs"));
|
|
13
12
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
14
13
|
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
|
|
@@ -45,7 +44,7 @@ function AsyncGenerator(e) { var r, t; function resume(r, t) { try { var n = e[r
|
|
|
45
44
|
AsyncGenerator.prototype["function" == typeof Symbol && Symbol.asyncIterator || "@@asyncIterator"] = function () { return this; }, AsyncGenerator.prototype.next = function (e) { return this._invoke("next", e); }, AsyncGenerator.prototype["throw"] = function (e) { return this._invoke("throw", e); }, AsyncGenerator.prototype["return"] = function (e) { return this._invoke("return", e); };
|
|
46
45
|
function _OverloadYield(e, d) { this.v = e, this.k = d; }
|
|
47
46
|
var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
48
|
-
function Database(
|
|
47
|
+
function Database(file) {
|
|
49
48
|
var _this2;
|
|
50
49
|
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
51
50
|
_classCallCheck(this, Database);
|
|
@@ -60,13 +59,10 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
60
59
|
compressIndex: false,
|
|
61
60
|
maxMemoryUsage: 64 * 1024 // 64KB
|
|
62
61
|
}, opts);
|
|
62
|
+
_this2.offsets = [];
|
|
63
63
|
_this2.shouldSave = false;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} else {
|
|
67
|
-
_this2.serializer = new _Simple["default"](_this2.opts);
|
|
68
|
-
}
|
|
69
|
-
_this2.fileHandler = new _FileHandler["default"](filePath);
|
|
64
|
+
_this2.serializer = new _Serializer["default"](_this2.opts);
|
|
65
|
+
_this2.fileHandler = new _FileHandler["default"](file);
|
|
70
66
|
_this2.indexManager = new _IndexManager["default"](_this2.opts);
|
|
71
67
|
_this2.indexOffset = 0;
|
|
72
68
|
_this2.writeBuffer = [];
|
|
@@ -165,7 +161,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
165
161
|
case 38:
|
|
166
162
|
_context.prev = 38;
|
|
167
163
|
_context.t0 = _context["catch"](9);
|
|
168
|
-
if (
|
|
164
|
+
if (Array.isArray(this.offsets)) {
|
|
169
165
|
this.offsets = [];
|
|
170
166
|
}
|
|
171
167
|
this.indexOffset = 0;
|
|
@@ -204,18 +200,30 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
204
200
|
}
|
|
205
201
|
throw new Error('Database is destroyed');
|
|
206
202
|
case 2:
|
|
207
|
-
if (
|
|
203
|
+
if (this.initialized) {
|
|
208
204
|
_context2.next = 4;
|
|
209
205
|
break;
|
|
210
206
|
}
|
|
207
|
+
throw new Error('Database not initialized');
|
|
208
|
+
case 4:
|
|
209
|
+
if (!this.saving) {
|
|
210
|
+
_context2.next = 6;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
211
213
|
return _context2.abrupt("return", new Promise(function (resolve) {
|
|
212
214
|
return _this4.once('save', resolve);
|
|
213
215
|
}));
|
|
214
|
-
case
|
|
216
|
+
case 6:
|
|
215
217
|
this.saving = true;
|
|
216
|
-
_context2.next =
|
|
218
|
+
_context2.next = 9;
|
|
217
219
|
return this.flush();
|
|
218
|
-
case
|
|
220
|
+
case 9:
|
|
221
|
+
if (this.shouldSave) {
|
|
222
|
+
_context2.next = 11;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
return _context2.abrupt("return");
|
|
226
|
+
case 11:
|
|
219
227
|
this.emit('before-save');
|
|
220
228
|
index = Object.assign({
|
|
221
229
|
data: {}
|
|
@@ -226,12 +234,14 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
226
234
|
}
|
|
227
235
|
}
|
|
228
236
|
offsets = this.offsets.slice(0);
|
|
229
|
-
_context2.next =
|
|
237
|
+
_context2.next = 17;
|
|
230
238
|
return this.serializer.serialize(index, {
|
|
231
|
-
compress: this.opts.compressIndex
|
|
239
|
+
compress: this.opts.compressIndex,
|
|
240
|
+
linebreak: true
|
|
232
241
|
});
|
|
233
|
-
case
|
|
242
|
+
case 17:
|
|
234
243
|
indexString = _context2.sent;
|
|
244
|
+
// force linebreak here to allow 'init' to read last line as offsets correctly
|
|
235
245
|
for (_field in this.indexManager.index.data) {
|
|
236
246
|
for (_term in this.indexManager.index.data[_field]) {
|
|
237
247
|
this.indexManager.index.data[_field][_term] = new Set(index.data[_field][_term]); // set back to set because of serialization
|
|
@@ -239,22 +249,15 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
239
249
|
}
|
|
240
250
|
offsets.push(this.indexOffset);
|
|
241
251
|
offsets.push(this.indexOffset + indexString.length);
|
|
242
|
-
|
|
252
|
+
// save offsets as JSON always to prevent linebreaks on last line, which breaks 'init()'
|
|
253
|
+
_context2.next = 23;
|
|
243
254
|
return this.serializer.serialize(offsets, {
|
|
244
|
-
|
|
255
|
+
json: true,
|
|
256
|
+
compress: false,
|
|
245
257
|
linebreak: false
|
|
246
258
|
});
|
|
247
|
-
case 19:
|
|
248
|
-
offsetsString = _context2.sent;
|
|
249
|
-
if (!this.shouldTruncate) {
|
|
250
|
-
_context2.next = 24;
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
_context2.next = 23;
|
|
254
|
-
return this.fileHandler.truncate(this.indexOffset);
|
|
255
259
|
case 23:
|
|
256
|
-
|
|
257
|
-
case 24:
|
|
260
|
+
offsetsString = _context2.sent;
|
|
258
261
|
this.writeBuffer.push(indexString);
|
|
259
262
|
this.writeBuffer.push(offsetsString);
|
|
260
263
|
_context2.next = 28;
|
|
@@ -390,17 +393,24 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
390
393
|
}
|
|
391
394
|
throw new Error('Database is destroyed');
|
|
392
395
|
case 2:
|
|
393
|
-
|
|
396
|
+
if (this.initialized) {
|
|
397
|
+
_context5.next = 5;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
_context5.next = 5;
|
|
401
|
+
return this.init();
|
|
402
|
+
case 5:
|
|
403
|
+
if (this.shouldTruncate) {
|
|
404
|
+
this.writeBuffer.push(this.indexOffset);
|
|
405
|
+
this.shouldTruncate = false;
|
|
406
|
+
}
|
|
407
|
+
_context5.next = 8;
|
|
394
408
|
return this.serializer.serialize(data, {
|
|
395
409
|
compress: this.opts.compress
|
|
396
410
|
});
|
|
397
|
-
case
|
|
411
|
+
case 8:
|
|
398
412
|
line = _context5.sent;
|
|
399
413
|
// using Buffer for offsets accuracy
|
|
400
|
-
if (this.shouldTruncate) {
|
|
401
|
-
this.writeBuffer.push(this.indexOffset);
|
|
402
|
-
this.shouldTruncate = false;
|
|
403
|
-
}
|
|
404
414
|
position = this.offsets.length;
|
|
405
415
|
this.offsets.push(this.indexOffset);
|
|
406
416
|
this.indexOffset += line.length;
|
|
@@ -408,14 +418,14 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
408
418
|
this.emit('insert', data, position);
|
|
409
419
|
this.writeBuffer.push(line);
|
|
410
420
|
if (!(!this.flushing && this.currentWriteBufferSize() > this.opts.maxMemoryUsage)) {
|
|
411
|
-
_context5.next =
|
|
421
|
+
_context5.next = 18;
|
|
412
422
|
break;
|
|
413
423
|
}
|
|
414
|
-
_context5.next =
|
|
424
|
+
_context5.next = 18;
|
|
415
425
|
return this.flush();
|
|
416
|
-
case
|
|
426
|
+
case 18:
|
|
417
427
|
this.shouldSave = true;
|
|
418
|
-
case
|
|
428
|
+
case 19:
|
|
419
429
|
case "end":
|
|
420
430
|
return _context5.stop();
|
|
421
431
|
}
|
|
@@ -442,16 +452,18 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
442
452
|
key: "flush",
|
|
443
453
|
value: function flush() {
|
|
444
454
|
var _this7 = this;
|
|
445
|
-
if (this.flushing)
|
|
446
|
-
|
|
455
|
+
if (this.flushing) {
|
|
456
|
+
return this.flushing;
|
|
457
|
+
}
|
|
458
|
+
return this.flushing = new Promise(function (resolve, reject) {
|
|
447
459
|
if (_this7.destroyed) return reject(new Error('Database is destroyed'));
|
|
448
460
|
if (!_this7.writeBuffer.length) return resolve();
|
|
449
461
|
var err;
|
|
450
|
-
_this7.
|
|
462
|
+
_this7._flush()["catch"](function (e) {
|
|
451
463
|
return err = e;
|
|
452
464
|
})["finally"](function () {
|
|
453
|
-
_this7.flushing = false;
|
|
454
465
|
err ? reject(err) : resolve();
|
|
466
|
+
_this7.flushing = false;
|
|
455
467
|
});
|
|
456
468
|
});
|
|
457
469
|
}
|
|
@@ -464,7 +476,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
464
476
|
while (1) switch (_context6.prev = _context6.next) {
|
|
465
477
|
case 0:
|
|
466
478
|
_context6.next = 2;
|
|
467
|
-
return _fs["default"].promises.open(this.fileHandler.
|
|
479
|
+
return _fs["default"].promises.open(this.fileHandler.file, 'a');
|
|
468
480
|
case 2:
|
|
469
481
|
fd = _context6.sent;
|
|
470
482
|
_context6.prev = 3;
|
|
@@ -488,7 +500,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
488
500
|
return this.fileHandler.truncate(this.writeBuffer.shift());
|
|
489
501
|
case 12:
|
|
490
502
|
_context6.next = 14;
|
|
491
|
-
return _fs["default"].promises.open(this.fileHandler.
|
|
503
|
+
return _fs["default"].promises.open(this.fileHandler.file, 'a');
|
|
492
504
|
case 14:
|
|
493
505
|
fd = _context6.sent;
|
|
494
506
|
return _context6.abrupt("continue", 4);
|
|
@@ -547,20 +559,27 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
547
559
|
}
|
|
548
560
|
throw new Error('Database is destroyed');
|
|
549
561
|
case 2:
|
|
562
|
+
if (_this.initialized) {
|
|
563
|
+
_context8.next = 5;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
_context8.next = 5;
|
|
567
|
+
return _awaitAsyncGenerator(_this.init());
|
|
568
|
+
case 5:
|
|
550
569
|
_context8.t0 = _this.shouldSave;
|
|
551
570
|
if (!_context8.t0) {
|
|
552
|
-
_context8.next =
|
|
571
|
+
_context8.next = 9;
|
|
553
572
|
break;
|
|
554
573
|
}
|
|
555
|
-
_context8.next =
|
|
574
|
+
_context8.next = 9;
|
|
556
575
|
return _awaitAsyncGenerator(_this.save()["catch"](console.error));
|
|
557
|
-
case
|
|
576
|
+
case 9:
|
|
558
577
|
if (!(_this.indexOffset === 0)) {
|
|
559
|
-
_context8.next =
|
|
578
|
+
_context8.next = 11;
|
|
560
579
|
break;
|
|
561
580
|
}
|
|
562
581
|
return _context8.abrupt("return");
|
|
563
|
-
case
|
|
582
|
+
case 11:
|
|
564
583
|
if (!Array.isArray(map)) {
|
|
565
584
|
if (map && _typeof(map) === 'object') {
|
|
566
585
|
map = _this.indexManager.query(map, options.matchAny);
|
|
@@ -581,15 +600,15 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
581
600
|
}
|
|
582
601
|
m = 0;
|
|
583
602
|
_i = 0, _partitionedRanges = partitionedRanges;
|
|
584
|
-
case
|
|
603
|
+
case 17:
|
|
585
604
|
if (!(_i < _partitionedRanges.length)) {
|
|
586
|
-
_context8.next =
|
|
605
|
+
_context8.next = 34;
|
|
587
606
|
break;
|
|
588
607
|
}
|
|
589
608
|
_ranges = _partitionedRanges[_i];
|
|
590
|
-
_context8.next =
|
|
609
|
+
_context8.next = 21;
|
|
591
610
|
return _awaitAsyncGenerator(_this.fileHandler.readRanges(_ranges));
|
|
592
|
-
case
|
|
611
|
+
case 21:
|
|
593
612
|
lines = _context8.sent;
|
|
594
613
|
_loop = /*#__PURE__*/_regeneratorRuntime().mark(function _loop() {
|
|
595
614
|
var err, entry;
|
|
@@ -621,27 +640,27 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
621
640
|
}, _loop);
|
|
622
641
|
});
|
|
623
642
|
_context8.t1 = _regeneratorRuntime().keys(lines);
|
|
624
|
-
case
|
|
643
|
+
case 24:
|
|
625
644
|
if ((_context8.t2 = _context8.t1()).done) {
|
|
626
|
-
_context8.next =
|
|
645
|
+
_context8.next = 31;
|
|
627
646
|
break;
|
|
628
647
|
}
|
|
629
648
|
_line = _context8.t2.value;
|
|
630
|
-
return _context8.delegateYield(_loop(), "t3",
|
|
631
|
-
case
|
|
649
|
+
return _context8.delegateYield(_loop(), "t3", 27);
|
|
650
|
+
case 27:
|
|
632
651
|
if (!_context8.t3) {
|
|
633
|
-
_context8.next =
|
|
652
|
+
_context8.next = 29;
|
|
634
653
|
break;
|
|
635
654
|
}
|
|
636
|
-
return _context8.abrupt("continue",
|
|
637
|
-
case
|
|
638
|
-
_context8.next =
|
|
655
|
+
return _context8.abrupt("continue", 24);
|
|
656
|
+
case 29:
|
|
657
|
+
_context8.next = 24;
|
|
639
658
|
break;
|
|
640
|
-
case
|
|
659
|
+
case 31:
|
|
641
660
|
_i++;
|
|
642
|
-
_context8.next =
|
|
661
|
+
_context8.next = 17;
|
|
643
662
|
break;
|
|
644
|
-
case
|
|
663
|
+
case 34:
|
|
645
664
|
case "end":
|
|
646
665
|
return _context8.stop();
|
|
647
666
|
}
|
|
@@ -671,21 +690,28 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
671
690
|
}
|
|
672
691
|
throw new Error('Database is destroyed');
|
|
673
692
|
case 3:
|
|
693
|
+
if (this.initialized) {
|
|
694
|
+
_context9.next = 6;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
_context9.next = 6;
|
|
698
|
+
return this.init();
|
|
699
|
+
case 6:
|
|
674
700
|
_context9.t0 = this.shouldSave;
|
|
675
701
|
if (!_context9.t0) {
|
|
676
|
-
_context9.next =
|
|
702
|
+
_context9.next = 10;
|
|
677
703
|
break;
|
|
678
704
|
}
|
|
679
|
-
_context9.next =
|
|
705
|
+
_context9.next = 10;
|
|
680
706
|
return this.save()["catch"](console.error);
|
|
681
|
-
case
|
|
707
|
+
case 10:
|
|
682
708
|
if (!Array.isArray(criteria)) {
|
|
683
|
-
_context9.next =
|
|
709
|
+
_context9.next = 19;
|
|
684
710
|
break;
|
|
685
711
|
}
|
|
686
|
-
_context9.next =
|
|
712
|
+
_context9.next = 13;
|
|
687
713
|
return this.readLines(criteria);
|
|
688
|
-
case
|
|
714
|
+
case 13:
|
|
689
715
|
results = _context9.sent;
|
|
690
716
|
if (options.orderBy) {
|
|
691
717
|
_options$orderBy$spli = options.orderBy.split(' '), _options$orderBy$spli2 = _slicedToArray(_options$orderBy$spli, 2), field = _options$orderBy$spli2[0], _options$orderBy$spli3 = _options$orderBy$spli2[1], direction = _options$orderBy$spli3 === void 0 ? 'asc' : _options$orderBy$spli3;
|
|
@@ -699,22 +725,22 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
699
725
|
results = results.slice(0, options.limit);
|
|
700
726
|
}
|
|
701
727
|
return _context9.abrupt("return", results);
|
|
702
|
-
case
|
|
703
|
-
_context9.next =
|
|
728
|
+
case 19:
|
|
729
|
+
_context9.next = 21;
|
|
704
730
|
return this.indexManager.query(criteria, options.matchAny);
|
|
705
|
-
case
|
|
731
|
+
case 21:
|
|
706
732
|
matchingLines = _context9.sent;
|
|
707
733
|
if (!(!matchingLines || !matchingLines.size)) {
|
|
708
|
-
_context9.next =
|
|
734
|
+
_context9.next = 24;
|
|
709
735
|
break;
|
|
710
736
|
}
|
|
711
737
|
return _context9.abrupt("return", []);
|
|
712
|
-
case
|
|
713
|
-
_context9.next =
|
|
738
|
+
case 24:
|
|
739
|
+
_context9.next = 26;
|
|
714
740
|
return this.query(_toConsumableArray(matchingLines), options);
|
|
715
|
-
case
|
|
741
|
+
case 26:
|
|
716
742
|
return _context9.abrupt("return", _context9.sent);
|
|
717
|
-
case
|
|
743
|
+
case 27:
|
|
718
744
|
case "end":
|
|
719
745
|
return _context9.stop();
|
|
720
746
|
}
|
|
@@ -747,47 +773,58 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
747
773
|
while (1) switch (_context11.prev = _context11.next) {
|
|
748
774
|
case 0:
|
|
749
775
|
options = _args11.length > 2 && _args11[2] !== undefined ? _args11[2] : {};
|
|
776
|
+
if (this.shouldTruncate) {
|
|
777
|
+
this.writeBuffer.push(this.indexOffset);
|
|
778
|
+
this.shouldTruncate = false;
|
|
779
|
+
}
|
|
750
780
|
if (!this.destroyed) {
|
|
751
|
-
_context11.next =
|
|
781
|
+
_context11.next = 4;
|
|
752
782
|
break;
|
|
753
783
|
}
|
|
754
784
|
throw new Error('Database is destroyed');
|
|
755
|
-
case
|
|
756
|
-
|
|
757
|
-
if (!_context11.t0) {
|
|
785
|
+
case 4:
|
|
786
|
+
if (this.initialized) {
|
|
758
787
|
_context11.next = 7;
|
|
759
788
|
break;
|
|
760
789
|
}
|
|
761
790
|
_context11.next = 7;
|
|
762
|
-
return this.
|
|
791
|
+
return this.init();
|
|
763
792
|
case 7:
|
|
764
|
-
_context11.
|
|
793
|
+
_context11.t0 = this.shouldSave;
|
|
794
|
+
if (!_context11.t0) {
|
|
795
|
+
_context11.next = 11;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
_context11.next = 11;
|
|
799
|
+
return this.save()["catch"](console.error);
|
|
800
|
+
case 11:
|
|
801
|
+
_context11.next = 13;
|
|
765
802
|
return this.indexManager.query(criteria, options.matchAny);
|
|
766
|
-
case
|
|
803
|
+
case 13:
|
|
767
804
|
matchingLines = _context11.sent;
|
|
768
805
|
if (!(!matchingLines || !matchingLines.size)) {
|
|
769
|
-
_context11.next =
|
|
806
|
+
_context11.next = 16;
|
|
770
807
|
break;
|
|
771
808
|
}
|
|
772
809
|
return _context11.abrupt("return", []);
|
|
773
|
-
case
|
|
810
|
+
case 16:
|
|
774
811
|
ranges = this.getRanges(_toConsumableArray(matchingLines));
|
|
775
812
|
validMatchingLines = new Set(ranges.map(function (r) {
|
|
776
813
|
return r.index;
|
|
777
814
|
}));
|
|
778
815
|
if (validMatchingLines.size) {
|
|
779
|
-
_context11.next =
|
|
816
|
+
_context11.next = 20;
|
|
780
817
|
break;
|
|
781
818
|
}
|
|
782
819
|
return _context11.abrupt("return", []);
|
|
783
|
-
case
|
|
784
|
-
_context11.next =
|
|
820
|
+
case 20:
|
|
821
|
+
_context11.next = 22;
|
|
785
822
|
return this.readLines(_toConsumableArray(validMatchingLines), ranges);
|
|
786
|
-
case
|
|
823
|
+
case 22:
|
|
787
824
|
entries = _context11.sent;
|
|
788
825
|
lines = [];
|
|
789
826
|
_iterator = _createForOfIteratorHelper(entries);
|
|
790
|
-
_context11.prev =
|
|
827
|
+
_context11.prev = 25;
|
|
791
828
|
_loop2 = /*#__PURE__*/_regeneratorRuntime().mark(function _loop2() {
|
|
792
829
|
var entry, err, updated, ret;
|
|
793
830
|
return _regeneratorRuntime().wrap(function _loop2$(_context10) {
|
|
@@ -809,27 +846,27 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
809
846
|
}, _loop2);
|
|
810
847
|
});
|
|
811
848
|
_iterator.s();
|
|
812
|
-
case
|
|
849
|
+
case 28:
|
|
813
850
|
if ((_step = _iterator.n()).done) {
|
|
814
|
-
_context11.next =
|
|
851
|
+
_context11.next = 32;
|
|
815
852
|
break;
|
|
816
853
|
}
|
|
817
|
-
return _context11.delegateYield(_loop2(), "t1",
|
|
818
|
-
case
|
|
819
|
-
_context11.next =
|
|
854
|
+
return _context11.delegateYield(_loop2(), "t1", 30);
|
|
855
|
+
case 30:
|
|
856
|
+
_context11.next = 28;
|
|
820
857
|
break;
|
|
821
|
-
case
|
|
822
|
-
_context11.next =
|
|
858
|
+
case 32:
|
|
859
|
+
_context11.next = 37;
|
|
823
860
|
break;
|
|
824
|
-
case
|
|
825
|
-
_context11.prev =
|
|
826
|
-
_context11.t2 = _context11["catch"](
|
|
861
|
+
case 34:
|
|
862
|
+
_context11.prev = 34;
|
|
863
|
+
_context11.t2 = _context11["catch"](25);
|
|
827
864
|
_iterator.e(_context11.t2);
|
|
828
|
-
case
|
|
829
|
-
_context11.prev =
|
|
865
|
+
case 37:
|
|
866
|
+
_context11.prev = 37;
|
|
830
867
|
_iterator.f();
|
|
831
|
-
return _context11.finish(
|
|
832
|
-
case
|
|
868
|
+
return _context11.finish(37);
|
|
869
|
+
case 40:
|
|
833
870
|
offsets = [];
|
|
834
871
|
byteOffset = 0, k = 0;
|
|
835
872
|
this.offsets.forEach(function (n, i) {
|
|
@@ -843,20 +880,20 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
843
880
|
});
|
|
844
881
|
this.offsets = offsets;
|
|
845
882
|
this.indexOffset += byteOffset;
|
|
846
|
-
_context11.next =
|
|
883
|
+
_context11.next = 47;
|
|
847
884
|
return this.fileHandler.replaceLines(ranges, lines);
|
|
848
|
-
case
|
|
885
|
+
case 47:
|
|
849
886
|
_toConsumableArray(validMatchingLines).forEach(function (lineNumber, i) {
|
|
850
887
|
_this8.indexManager.dryRemove(lineNumber);
|
|
851
888
|
_this8.indexManager.add(entries[i], lineNumber);
|
|
852
889
|
});
|
|
853
890
|
this.shouldSave = true;
|
|
854
891
|
return _context11.abrupt("return", entries);
|
|
855
|
-
case
|
|
892
|
+
case 50:
|
|
856
893
|
case "end":
|
|
857
894
|
return _context11.stop();
|
|
858
895
|
}
|
|
859
|
-
}, _callee9, this, [[
|
|
896
|
+
}, _callee9, this, [[25, 34, 37, 40]]);
|
|
860
897
|
}));
|
|
861
898
|
function update(_x5, _x6) {
|
|
862
899
|
return _update.apply(this, arguments);
|
|
@@ -879,37 +916,48 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
879
916
|
while (1) switch (_context12.prev = _context12.next) {
|
|
880
917
|
case 0:
|
|
881
918
|
options = _args12.length > 1 && _args12[1] !== undefined ? _args12[1] : {};
|
|
919
|
+
if (this.shouldTruncate) {
|
|
920
|
+
this.writeBuffer.push(this.indexOffset);
|
|
921
|
+
this.shouldTruncate = false;
|
|
922
|
+
}
|
|
882
923
|
if (!this.destroyed) {
|
|
883
|
-
_context12.next =
|
|
924
|
+
_context12.next = 4;
|
|
884
925
|
break;
|
|
885
926
|
}
|
|
886
927
|
throw new Error('Database is destroyed');
|
|
887
|
-
case
|
|
888
|
-
|
|
889
|
-
if (!_context12.t0) {
|
|
928
|
+
case 4:
|
|
929
|
+
if (this.initialized) {
|
|
890
930
|
_context12.next = 7;
|
|
891
931
|
break;
|
|
892
932
|
}
|
|
893
933
|
_context12.next = 7;
|
|
894
|
-
return this.
|
|
934
|
+
return this.init();
|
|
895
935
|
case 7:
|
|
896
|
-
_context12.
|
|
936
|
+
_context12.t0 = this.shouldSave;
|
|
937
|
+
if (!_context12.t0) {
|
|
938
|
+
_context12.next = 11;
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
_context12.next = 11;
|
|
942
|
+
return this.save()["catch"](console.error);
|
|
943
|
+
case 11:
|
|
944
|
+
_context12.next = 13;
|
|
897
945
|
return this.indexManager.query(criteria, options.matchAny);
|
|
898
|
-
case
|
|
946
|
+
case 13:
|
|
899
947
|
matchingLines = _context12.sent;
|
|
900
948
|
if (!(!matchingLines || !matchingLines.size)) {
|
|
901
|
-
_context12.next =
|
|
949
|
+
_context12.next = 16;
|
|
902
950
|
break;
|
|
903
951
|
}
|
|
904
952
|
return _context12.abrupt("return", 0);
|
|
905
|
-
case
|
|
953
|
+
case 16:
|
|
906
954
|
ranges = this.getRanges(_toConsumableArray(matchingLines));
|
|
907
955
|
validMatchingLines = new Set(ranges.map(function (r) {
|
|
908
956
|
return r.index;
|
|
909
957
|
}));
|
|
910
|
-
_context12.next =
|
|
958
|
+
_context12.next = 20;
|
|
911
959
|
return this.fileHandler.replaceLines(ranges, []);
|
|
912
|
-
case
|
|
960
|
+
case 20:
|
|
913
961
|
offsets = [];
|
|
914
962
|
byteOffset = 0, k = 0;
|
|
915
963
|
this.offsets.forEach(function (n, i) {
|
|
@@ -926,7 +974,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
926
974
|
this.indexManager.remove(_toConsumableArray(validMatchingLines));
|
|
927
975
|
this.shouldSave = true;
|
|
928
976
|
return _context12.abrupt("return", ranges.length);
|
|
929
|
-
case
|
|
977
|
+
case 28:
|
|
930
978
|
case "end":
|
|
931
979
|
return _context12.stop();
|
|
932
980
|
}
|
|
@@ -972,7 +1020,8 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
972
1020
|
}, {
|
|
973
1021
|
key: "length",
|
|
974
1022
|
get: function get() {
|
|
975
|
-
|
|
1023
|
+
var _this$offsets;
|
|
1024
|
+
return (this === null || this === void 0 || (_this$offsets = this.offsets) === null || _this$offsets === void 0 ? void 0 : _this$offsets.length) || 0;
|
|
976
1025
|
}
|
|
977
1026
|
}, {
|
|
978
1027
|
key: "index",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jexidb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node test/test.mjs && exit 1",
|
|
14
|
+
"test": "node --expose-gc test/test.mjs && exit 1",
|
|
15
15
|
"build": "npx babel src/Database.mjs --plugins @babel/plugin-transform-async-generator-functions --out-file-extension .cjs --out-dir dist"
|
|
16
16
|
},
|
|
17
17
|
"author": "EdenwareApps",
|
package/src/Database.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from 'events'
|
|
2
2
|
import FileHandler from './FileHandler.mjs'
|
|
3
3
|
import IndexManager from './IndexManager.mjs'
|
|
4
|
-
import Serializer from './
|
|
5
|
-
import AdvancedSerializer from './serializers/Advanced.mjs'
|
|
4
|
+
import Serializer from './Serializer.mjs'
|
|
6
5
|
import fs from 'fs'
|
|
7
6
|
|
|
8
7
|
export class Database extends EventEmitter {
|
|
9
|
-
constructor(
|
|
8
|
+
constructor(file, opts={}) {
|
|
10
9
|
super()
|
|
11
10
|
this.opts = Object.assign({
|
|
12
11
|
v8: false,
|
|
@@ -16,13 +15,10 @@ export class Database extends EventEmitter {
|
|
|
16
15
|
compressIndex: false,
|
|
17
16
|
maxMemoryUsage: 64 * 1024 // 64KB
|
|
18
17
|
}, opts)
|
|
18
|
+
this.offsets = []
|
|
19
19
|
this.shouldSave = false
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} else {
|
|
23
|
-
this.serializer = new Serializer(this.opts)
|
|
24
|
-
}
|
|
25
|
-
this.fileHandler = new FileHandler(filePath)
|
|
20
|
+
this.serializer = new Serializer(this.opts)
|
|
21
|
+
this.fileHandler = new FileHandler(file)
|
|
26
22
|
this.indexManager = new IndexManager(this.opts)
|
|
27
23
|
this.indexOffset = 0
|
|
28
24
|
this.writeBuffer = []
|
|
@@ -60,7 +56,7 @@ export class Database extends EventEmitter {
|
|
|
60
56
|
const index = await this.serializer.deserialize(indexLine, {compress: this.opts.compressIndex})
|
|
61
57
|
index && this.indexManager.load(index)
|
|
62
58
|
} catch (e) {
|
|
63
|
-
if(
|
|
59
|
+
if(Array.isArray(this.offsets)) {
|
|
64
60
|
this.offsets = []
|
|
65
61
|
}
|
|
66
62
|
this.indexOffset = 0
|
|
@@ -76,9 +72,11 @@ export class Database extends EventEmitter {
|
|
|
76
72
|
|
|
77
73
|
async save() {
|
|
78
74
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
75
|
+
if(!this.initialized) throw new Error('Database not initialized')
|
|
79
76
|
if(this.saving) return new Promise(resolve => this.once('save', resolve))
|
|
80
77
|
this.saving = true
|
|
81
78
|
await this.flush()
|
|
79
|
+
if (!this.shouldSave) return
|
|
82
80
|
this.emit('before-save')
|
|
83
81
|
const index = Object.assign({data: {}}, this.indexManager.index)
|
|
84
82
|
for(const field in this.indexManager.index.data) {
|
|
@@ -87,7 +85,7 @@ export class Database extends EventEmitter {
|
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
const offsets = this.offsets.slice(0)
|
|
90
|
-
const indexString = await this.serializer.serialize(index, {compress: this.opts.compressIndex})
|
|
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
|
|
91
89
|
for(const field in this.indexManager.index.data) {
|
|
92
90
|
for(const term in this.indexManager.index.data[field]) {
|
|
93
91
|
this.indexManager.index.data[field][term] = new Set(index.data[field][term]) // set back to set because of serialization
|
|
@@ -95,11 +93,8 @@ export class Database extends EventEmitter {
|
|
|
95
93
|
}
|
|
96
94
|
offsets.push(this.indexOffset)
|
|
97
95
|
offsets.push(this.indexOffset + indexString.length)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await this.fileHandler.truncate(this.indexOffset)
|
|
101
|
-
this.shouldTruncate = false
|
|
102
|
-
}
|
|
96
|
+
// 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})
|
|
103
98
|
this.writeBuffer.push(indexString)
|
|
104
99
|
this.writeBuffer.push(offsetsString)
|
|
105
100
|
await this.flush() // write the index and offsets
|
|
@@ -122,7 +117,7 @@ export class Database extends EventEmitter {
|
|
|
122
117
|
}
|
|
123
118
|
return
|
|
124
119
|
}
|
|
125
|
-
let end = this.offsets[n + 1] || this.indexOffset || Number.MAX_SAFE_INTEGER
|
|
120
|
+
let end = (this.offsets[n + 1] || this.indexOffset || Number.MAX_SAFE_INTEGER)
|
|
126
121
|
return [this.offsets[n], end]
|
|
127
122
|
}
|
|
128
123
|
|
|
@@ -147,11 +142,12 @@ export class Database extends EventEmitter {
|
|
|
147
142
|
|
|
148
143
|
async insert(data) {
|
|
149
144
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
150
|
-
|
|
145
|
+
if(!this.initialized) await this.init()
|
|
151
146
|
if (this.shouldTruncate) {
|
|
152
147
|
this.writeBuffer.push(this.indexOffset)
|
|
153
148
|
this.shouldTruncate = false
|
|
154
149
|
}
|
|
150
|
+
const line = await this.serializer.serialize(data, {compress: this.opts.compress}) // using Buffer for offsets accuracy
|
|
155
151
|
const position = this.offsets.length
|
|
156
152
|
this.offsets.push(this.indexOffset)
|
|
157
153
|
this.indexOffset += line.length
|
|
@@ -170,20 +166,22 @@ export class Database extends EventEmitter {
|
|
|
170
166
|
}
|
|
171
167
|
|
|
172
168
|
flush() {
|
|
173
|
-
if(this.flushing)
|
|
174
|
-
|
|
169
|
+
if(this.flushing) {
|
|
170
|
+
return this.flushing
|
|
171
|
+
}
|
|
172
|
+
return this.flushing = new Promise((resolve, reject) => {
|
|
175
173
|
if(this.destroyed) return reject(new Error('Database is destroyed'))
|
|
176
174
|
if(!this.writeBuffer.length) return resolve()
|
|
177
175
|
let err
|
|
178
|
-
this.
|
|
179
|
-
this.flushing = false
|
|
176
|
+
this._flush().catch(e => err = e).finally(() => {
|
|
180
177
|
err ? reject(err) : resolve()
|
|
178
|
+
this.flushing = false
|
|
181
179
|
})
|
|
182
180
|
})
|
|
183
181
|
}
|
|
184
182
|
|
|
185
183
|
async _flush() {
|
|
186
|
-
let fd = await fs.promises.open(this.fileHandler.
|
|
184
|
+
let fd = await fs.promises.open(this.fileHandler.file, 'a')
|
|
187
185
|
try {
|
|
188
186
|
while(this.writeBuffer.length) {
|
|
189
187
|
let data
|
|
@@ -191,7 +189,7 @@ export class Database extends EventEmitter {
|
|
|
191
189
|
if(pos === 0) {
|
|
192
190
|
await fd.close()
|
|
193
191
|
await this.fileHandler.truncate(this.writeBuffer.shift())
|
|
194
|
-
fd = await fs.promises.open(this.fileHandler.
|
|
192
|
+
fd = await fs.promises.open(this.fileHandler.file, 'a')
|
|
195
193
|
continue
|
|
196
194
|
} else if(pos === -1) {
|
|
197
195
|
data = Buffer.concat(this.writeBuffer)
|
|
@@ -212,6 +210,7 @@ export class Database extends EventEmitter {
|
|
|
212
210
|
|
|
213
211
|
async *walk(map, options={}) {
|
|
214
212
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
213
|
+
if(!this.initialized) await this.init()
|
|
215
214
|
this.shouldSave && await this.save().catch(console.error)
|
|
216
215
|
if(this.indexOffset === 0) return
|
|
217
216
|
if(!Array.isArray(map)) {
|
|
@@ -250,6 +249,7 @@ export class Database extends EventEmitter {
|
|
|
250
249
|
|
|
251
250
|
async query(criteria, options={}) {
|
|
252
251
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
252
|
+
if(!this.initialized) await this.init()
|
|
253
253
|
this.shouldSave && await this.save().catch(console.error)
|
|
254
254
|
if(Array.isArray(criteria)) {
|
|
255
255
|
let results = await this.readLines(criteria)
|
|
@@ -275,7 +275,12 @@ export class Database extends EventEmitter {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
async update(criteria, data, options={}) {
|
|
278
|
+
if (this.shouldTruncate) {
|
|
279
|
+
this.writeBuffer.push(this.indexOffset)
|
|
280
|
+
this.shouldTruncate = false
|
|
281
|
+
}
|
|
278
282
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
283
|
+
if(!this.initialized) await this.init()
|
|
279
284
|
this.shouldSave && await this.save().catch(console.error)
|
|
280
285
|
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
281
286
|
if (!matchingLines || !matchingLines.size) {
|
|
@@ -317,7 +322,12 @@ export class Database extends EventEmitter {
|
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
async delete(criteria, options={}) {
|
|
325
|
+
if (this.shouldTruncate) {
|
|
326
|
+
this.writeBuffer.push(this.indexOffset)
|
|
327
|
+
this.shouldTruncate = false
|
|
328
|
+
}
|
|
320
329
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
330
|
+
if(!this.initialized) await this.init()
|
|
321
331
|
this.shouldSave && await this.save().catch(console.error)
|
|
322
332
|
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
323
333
|
if (!matchingLines || !matchingLines.size) {
|
|
@@ -355,7 +365,7 @@ export class Database extends EventEmitter {
|
|
|
355
365
|
}
|
|
356
366
|
|
|
357
367
|
get length() {
|
|
358
|
-
return this
|
|
368
|
+
return this?.offsets?.length || 0
|
|
359
369
|
}
|
|
360
370
|
|
|
361
371
|
get index() {
|
package/src/FileHandler.mjs
CHANGED
|
@@ -2,21 +2,21 @@ import fs from 'fs'
|
|
|
2
2
|
import pLimit from 'p-limit'
|
|
3
3
|
|
|
4
4
|
export default class FileHandler {
|
|
5
|
-
constructor(
|
|
6
|
-
this.
|
|
5
|
+
constructor(file) {
|
|
6
|
+
this.file = file
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async truncate(offset) {
|
|
10
10
|
try {
|
|
11
|
-
await fs.promises.access(this.
|
|
12
|
-
await fs.promises.truncate(this.
|
|
11
|
+
await fs.promises.access(this.file, fs.constants.F_OK)
|
|
12
|
+
await fs.promises.truncate(this.file, offset)
|
|
13
13
|
} catch (err) {
|
|
14
|
-
await fs.promises.writeFile(this.
|
|
14
|
+
await fs.promises.writeFile(this.file, '')
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async readRange(start, end) {
|
|
19
|
-
let fd = await fs.promises.open(this.
|
|
19
|
+
let fd = await fs.promises.open(this.file, 'r')
|
|
20
20
|
const length = end - start
|
|
21
21
|
let buffer = Buffer.alloc(length)
|
|
22
22
|
const { bytesRead } = await fd.read(buffer, 0, length, start).catch(console.error)
|
|
@@ -27,7 +27,7 @@ export default class FileHandler {
|
|
|
27
27
|
|
|
28
28
|
async readRanges(ranges, mapper) {
|
|
29
29
|
const lines = {}, limit = pLimit(4)
|
|
30
|
-
const fd = await fs.promises.open(this.
|
|
30
|
+
const fd = await fs.promises.open(this.file, 'r')
|
|
31
31
|
try {
|
|
32
32
|
const tasks = ranges.map(r => {
|
|
33
33
|
return async () => {
|
|
@@ -50,9 +50,9 @@ export default class FileHandler {
|
|
|
50
50
|
|
|
51
51
|
async replaceLines(ranges, lines) {
|
|
52
52
|
let closed
|
|
53
|
-
const tmpFile = this.
|
|
53
|
+
const tmpFile = this.file + '.tmp'
|
|
54
54
|
const writer = await fs.promises.open(tmpFile, 'w+')
|
|
55
|
-
const reader = await fs.promises.open(this.
|
|
55
|
+
const reader = await fs.promises.open(this.file, 'r')
|
|
56
56
|
try {
|
|
57
57
|
let i = 0, start = 0
|
|
58
58
|
for (const r of ranges) {
|
|
@@ -74,7 +74,7 @@ export default class FileHandler {
|
|
|
74
74
|
await reader.close()
|
|
75
75
|
await writer.close()
|
|
76
76
|
closed = true
|
|
77
|
-
await fs.promises.copyFile(tmpFile, this.
|
|
77
|
+
await fs.promises.copyFile(tmpFile, this.file)
|
|
78
78
|
} catch (e) {
|
|
79
79
|
console.error('Error replacing lines:', e)
|
|
80
80
|
} finally {
|
|
@@ -90,11 +90,11 @@ export default class FileHandler {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
writeDataSync(data) {
|
|
93
|
-
fs.writeFileSync(this.
|
|
93
|
+
fs.writeFileSync(this.file, data, { flag: 'a' })
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
async readLastLine() {
|
|
97
|
-
const reader = await fs.promises.open(this.
|
|
97
|
+
const reader = await fs.promises.open(this.file, 'r')
|
|
98
98
|
try {
|
|
99
99
|
const { size } = await reader.stat()
|
|
100
100
|
if (size < 1) throw 'empty file'
|
|
@@ -6,9 +6,6 @@ export default class Serializer extends EventEmitter {
|
|
|
6
6
|
constructor(opts = {}) {
|
|
7
7
|
super()
|
|
8
8
|
this.opts = Object.assign({}, opts)
|
|
9
|
-
this.linebreak = Buffer.from([0x0A])
|
|
10
|
-
this.delimiter = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF])
|
|
11
|
-
this.defaultBuffer = Buffer.alloc(4096)
|
|
12
9
|
this.brotliOptions = {
|
|
13
10
|
params: {
|
|
14
11
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
@@ -19,56 +16,54 @@ export default class Serializer extends EventEmitter {
|
|
|
19
16
|
async serialize(data, opts={}) {
|
|
20
17
|
let line
|
|
21
18
|
let header = 0x00 // 1 byte de header
|
|
22
|
-
const useV8 = this.opts.v8 || opts.v8 === true
|
|
23
|
-
const compress = this.opts.compress || opts.compress === true
|
|
19
|
+
const useV8 = (this.opts.v8 && opts.json !== true) || opts.v8 === true
|
|
20
|
+
const compress = (this.opts.compress && opts.compress !== false) || opts.compress === true
|
|
21
|
+
const addLinebreak = opts.linebreak !== false || (!useV8 && !compress && opts.linebreak !== false)
|
|
24
22
|
if (useV8) {
|
|
25
23
|
header |= 0x02 // set V8
|
|
26
24
|
line = v8.serialize(data)
|
|
27
25
|
} else {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} else {
|
|
31
|
-
return Buffer.from(JSON.stringify(data) + (opts.linebreak !== false ? '\n' : ''), 'utf-8')
|
|
32
|
-
}
|
|
26
|
+
let json = JSON.stringify(data)
|
|
27
|
+
line = Buffer.from(json, 'utf-8')
|
|
33
28
|
}
|
|
34
29
|
if (compress) {
|
|
35
30
|
let err
|
|
36
|
-
const
|
|
37
|
-
|
|
31
|
+
const compressionType = useV8 ? 'deflate' : 'brotli'
|
|
32
|
+
const buffer = await this.compress(line, compressionType).catch(e => err = e)
|
|
33
|
+
if(!err && buffer.length && buffer.length < line.length) {
|
|
38
34
|
header |= 0x01
|
|
39
35
|
line = buffer
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
|
-
const totalLength = 1 + line.length + (
|
|
38
|
+
const totalLength = 1 + line.length + (addLinebreak ? 1 : 0)
|
|
43
39
|
const result = Buffer.alloc(totalLength)
|
|
44
40
|
result[0] = header
|
|
45
|
-
line.copy(result, 1)
|
|
46
|
-
if
|
|
47
|
-
result[
|
|
41
|
+
line.copy(result, 1, 0, line.length)
|
|
42
|
+
if(addLinebreak) {
|
|
43
|
+
result[result.length - 1] = 0x0A
|
|
48
44
|
}
|
|
49
45
|
return result
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
async deserialize(data
|
|
53
|
-
let line
|
|
48
|
+
async deserialize(data) {
|
|
49
|
+
let line, isCompressed, isV8
|
|
54
50
|
const header = data.readUInt8(0)
|
|
55
51
|
const valid = header === 0x00 || header === 0x01 || header === 0x02 || header === 0x03
|
|
56
|
-
let isCompressed, isV8, decompresssed
|
|
57
52
|
if(valid) {
|
|
58
53
|
isCompressed = (header & 0x01) === 0x01
|
|
59
54
|
isV8 = (header & 0x02) === 0x02
|
|
60
55
|
line = data.subarray(1) // remove byte header
|
|
61
56
|
} else {
|
|
62
57
|
isCompressed = isV8 = false
|
|
63
|
-
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(data.toString('utf-8').trim())
|
|
60
|
+
} catch (e) {
|
|
61
|
+
throw new Error('Failed to deserialize legacy JSON data')
|
|
62
|
+
}
|
|
64
63
|
}
|
|
65
64
|
if (isCompressed) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if(!err) {
|
|
69
|
-
decompresssed = true
|
|
70
|
-
line = buffer
|
|
71
|
-
}
|
|
65
|
+
const compressionType = isV8 ? 'deflate' : 'brotli'
|
|
66
|
+
line = await this.decompress(line, compressionType).catch(e => err = e)
|
|
72
67
|
}
|
|
73
68
|
if (isV8) {
|
|
74
69
|
try {
|
|
@@ -80,41 +75,43 @@ export default class Serializer extends EventEmitter {
|
|
|
80
75
|
try {
|
|
81
76
|
return JSON.parse(line.toString('utf-8').trim())
|
|
82
77
|
} catch (e) {
|
|
83
|
-
console.error('Failed to deserialize', header, line.toString('utf-8').trim())
|
|
84
78
|
throw new Error('Failed to deserialize JSON data')
|
|
85
79
|
}
|
|
86
80
|
}
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
compress(data) {
|
|
83
|
+
compress(data, type) {
|
|
90
84
|
return new Promise((resolve, reject) => {
|
|
91
|
-
|
|
85
|
+
const callback = (err, buffer) => {
|
|
92
86
|
if (err) {
|
|
93
87
|
reject(err)
|
|
94
88
|
} else {
|
|
95
89
|
resolve(buffer)
|
|
96
90
|
}
|
|
97
|
-
}
|
|
91
|
+
}
|
|
92
|
+
if(type === 'brotli') {
|
|
93
|
+
zlib.brotliCompress(data, this.brotliOptions, callback)
|
|
94
|
+
} else {
|
|
95
|
+
zlib.deflate(data, callback)
|
|
96
|
+
}
|
|
98
97
|
})
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
decompress(data) {
|
|
100
|
+
decompress(data, type) {
|
|
102
101
|
return new Promise((resolve, reject) => {
|
|
103
|
-
|
|
102
|
+
const callback = (err, buffer) => {
|
|
104
103
|
if (err) {
|
|
105
104
|
reject(err)
|
|
106
105
|
} else {
|
|
107
106
|
resolve(buffer)
|
|
108
107
|
}
|
|
109
|
-
}
|
|
108
|
+
}
|
|
109
|
+
if(type === 'brotli') {
|
|
110
|
+
zlib.brotliDecompress(data, callback)
|
|
111
|
+
} else {
|
|
112
|
+
zlib.inflate(data, callback)
|
|
113
|
+
}
|
|
110
114
|
})
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
async safeDeserialize(json) {
|
|
114
|
-
try {
|
|
115
|
-
return await this.deserialize(json)
|
|
116
|
-
} catch (e) {
|
|
117
|
-
return null
|
|
118
|
-
}
|
|
119
|
-
}
|
|
116
|
+
|
|
120
117
|
}
|
package/test/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## Test Results
|
|
2
|
+
The following are the results of the automated tests conducted on my PC for different serialization formats and compression methods.
|
|
3
|
+
|
|
4
|
+
| Format | Size (bytes) | Time elapsed (ms) |
|
|
5
|
+
|-------------------------------|--------------|--------------------|
|
|
6
|
+
| JSON | 1117 | 21 |
|
|
7
|
+
| V8 Serialization | 1124 | 12 🏆 |
|
|
8
|
+
| JSON with Brotli Compression | 1038 🏆 | 20 |
|
|
9
|
+
| V8 with Brotli Compression | 1052 | 15 |
|
|
10
|
+
|
|
11
|
+
When using V8 for serialization, be aware that serialized data may not deserialize correctly across different versions of V8 (which powers Chromium, Node.js, and Electron), leading to potential data loss or corruption. This issue is specific to V8 serialization; however, when using JSON, the data remains universal and compatible across environments.
|
|
12
|
+
|
|
13
|
+
To avoid this issue, always use your V8 database with the same version of Node.js.
|
|
Binary file
|
package/test/test-json.jdb
CHANGED
|
Binary file
|
|
Binary file
|
package/test/test-v8.jdb
CHANGED
|
Binary file
|
package/test/test.mjs
CHANGED
|
@@ -50,13 +50,8 @@ const runTests = async (id, name, format, opts) => {
|
|
|
50
50
|
// Path to the test file
|
|
51
51
|
const testFilePath = path.join(__dirname, 'test-' + name + '.jdb');
|
|
52
52
|
|
|
53
|
-
// Function to clear the test file before each run
|
|
54
|
-
const clearTestFile = () => {
|
|
55
|
-
fs.writeFileSync(testFilePath, '', { encoding: null });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
53
|
console.log('Battle #' + id + ' (' + format + ') is starting...\n');
|
|
59
|
-
|
|
54
|
+
fs.writeFileSync(testFilePath, '', { encoding: null }); // Clear the file before starting the tests
|
|
60
55
|
const start = Date.now()
|
|
61
56
|
const db = new Database(testFilePath, opts); // Instantiate the database
|
|
62
57
|
|
|
@@ -97,7 +92,6 @@ const runTests = async (id, name, format, opts) => {
|
|
|
97
92
|
|
|
98
93
|
// 5. Test data deletion
|
|
99
94
|
await db.delete({ name: character.name + ' Updated' });
|
|
100
|
-
|
|
101
95
|
results = await db.query({ id: { '<=': 2 } });
|
|
102
96
|
const pass5 = results.length === 1;
|
|
103
97
|
const pass6 = results[0].name === 'Sub-Zero';
|
|
@@ -108,43 +102,65 @@ const runTests = async (id, name, format, opts) => {
|
|
|
108
102
|
// End the battle and log the result
|
|
109
103
|
if(pass1 && pass2 && pass4 && pass5 && pass6) {
|
|
110
104
|
let err, elapsed = Date.now() - start;
|
|
111
|
-
elapsed = elapsed < 1000 ? elapsed + 'ms' : (elapsed / 1000).toFixed(3) + 's';
|
|
112
105
|
const { size } = await fs.promises.stat(testFilePath);
|
|
113
|
-
benchmarks[format]
|
|
106
|
+
if(!benchmarks[format]) {
|
|
107
|
+
benchmarks[format] = { elapsed, size }
|
|
108
|
+
} else {
|
|
109
|
+
benchmarks[format].elapsed = (elapsed + benchmarks[format].elapsed)
|
|
110
|
+
benchmarks[format].size = (size + benchmarks[format].size)
|
|
111
|
+
}
|
|
114
112
|
console.log(`\nBattle #${id} ended: All tests with format "${format}" ran successfully! Fatality avoided this time.\n\n`);
|
|
113
|
+
global.gc()
|
|
115
114
|
} else {
|
|
116
115
|
benchmarks[format] = { elapsed: 'Error', size: 'Error' };
|
|
116
|
+
global.gc();
|
|
117
117
|
throw `\nBattle #${id} ended: Some tests failed with format "${format}"! Time to train harder.\n\n`;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
async function runAllTests() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
v8: false,
|
|
126
|
-
compress: false,
|
|
127
|
-
compressIndex:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
122
|
+
const depth = 100
|
|
123
|
+
let err, i = 1
|
|
124
|
+
let tests = [
|
|
125
|
+
['json', 'JSON', { indexes: { id: 'number', name: 'string' }, v8: false, compress: false, compressIndex: false }],
|
|
126
|
+
['v8', 'V8 serialization', { indexes: { id: 'number', name: 'string' }, v8: true, compress: false, compressIndex: false }],
|
|
127
|
+
['json-compressed', 'JSON with Brotli compression', { indexes: { id: 'number', name: 'string' }, v8: false, compress: false, compressIndex: true }],
|
|
128
|
+
['v8-compressed', 'V8 with Deflate compression', { indexes: { id: 'number', name: 'string' }, v8: true, compress: false, compressIndex: true }]
|
|
129
|
+
]
|
|
130
|
+
tests = Array(depth).fill(tests).flat()
|
|
131
|
+
tests = tests.map(value => ({ value, sort: Math.random() })).sort((a, b) => a.sort - b.sort).map(({ value }) => value)
|
|
132
|
+
for(const test of tests) {
|
|
133
|
+
await runTests(i++, test[0], test[1], test[2]).catch(e => {
|
|
134
|
+
benchmarks[test[1]] = { elapsed: 'Error', size: 'Error' };
|
|
135
|
+
console.error(e)
|
|
136
|
+
err = e
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
const winners = {}
|
|
140
|
+
for (const [format, result] of Object.entries(benchmarks)) {
|
|
141
|
+
if (result.elapsed !== 'Error' && result.size !== 'Error') {
|
|
142
|
+
if (typeof(winners.elapsed) === 'undefined' || result.elapsed < winners.elapsed) {
|
|
143
|
+
winners.elapsed = result.elapsed
|
|
144
|
+
winners.format = format
|
|
145
|
+
}
|
|
146
|
+
if (typeof(winners.size) === 'undefined' || result.size < winners.size) {
|
|
147
|
+
winners.size = result.size
|
|
148
|
+
winners.format = format
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for(const format in benchmarks) {
|
|
153
|
+
for(const prop of ['elapsed', 'size']) {
|
|
154
|
+
if(benchmarks[format][prop] === winners[prop]) {
|
|
155
|
+
benchmarks[format][prop] += ' \uD83C\uDFC6'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log('Benchmarks results after '+ tests.length +' battles:')
|
|
147
160
|
console.table(benchmarks)
|
|
161
|
+
// setInterval(() => {}, 1000)
|
|
162
|
+
// global.Database = Database
|
|
163
|
+
// global.__dirname = __dirname
|
|
148
164
|
process.exit(err ? 1 : 0)
|
|
149
165
|
}
|
|
150
166
|
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export default class Serializer {
|
|
2
|
-
|
|
3
|
-
constructor(opts={}) {
|
|
4
|
-
this.opts = Object.assign({}, opts)
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
async serialize(data, opts={}) {
|
|
8
|
-
return Buffer.from(JSON.stringify(data) + (opts.linebreak !== false ? '\n' : ''), 'utf-8')
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async deserialize(data, opts={}) {
|
|
12
|
-
const line = data.toString('utf-8')
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(line)
|
|
15
|
-
} catch (e) {
|
|
16
|
-
console.error('Failed to deserialize', line)
|
|
17
|
-
throw new Error('Failed to deserialize JSON data')
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
}
|