jexidb 1.0.2 → 1.0.4
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 +177 -126
- package/package.json +3 -3
- package/src/Database.mjs +39 -27
- 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,23 +559,32 @@ 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
|
-
if (map
|
|
566
|
-
map =
|
|
584
|
+
if (map instanceof Set) {
|
|
585
|
+
map = _toConsumableArray(map);
|
|
586
|
+
} else if (map && _typeof(map) === 'object') {
|
|
587
|
+
map = _toConsumableArray(_this.indexManager.query(map, options.matchAny));
|
|
567
588
|
} else {
|
|
568
589
|
map = _toConsumableArray(Array(_this.offsets.length).keys());
|
|
569
590
|
}
|
|
@@ -581,15 +602,15 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
581
602
|
}
|
|
582
603
|
m = 0;
|
|
583
604
|
_i = 0, _partitionedRanges = partitionedRanges;
|
|
584
|
-
case
|
|
605
|
+
case 17:
|
|
585
606
|
if (!(_i < _partitionedRanges.length)) {
|
|
586
|
-
_context8.next =
|
|
607
|
+
_context8.next = 34;
|
|
587
608
|
break;
|
|
588
609
|
}
|
|
589
610
|
_ranges = _partitionedRanges[_i];
|
|
590
|
-
_context8.next =
|
|
611
|
+
_context8.next = 21;
|
|
591
612
|
return _awaitAsyncGenerator(_this.fileHandler.readRanges(_ranges));
|
|
592
|
-
case
|
|
613
|
+
case 21:
|
|
593
614
|
lines = _context8.sent;
|
|
594
615
|
_loop = /*#__PURE__*/_regeneratorRuntime().mark(function _loop() {
|
|
595
616
|
var err, entry;
|
|
@@ -621,27 +642,27 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
621
642
|
}, _loop);
|
|
622
643
|
});
|
|
623
644
|
_context8.t1 = _regeneratorRuntime().keys(lines);
|
|
624
|
-
case
|
|
645
|
+
case 24:
|
|
625
646
|
if ((_context8.t2 = _context8.t1()).done) {
|
|
626
|
-
_context8.next =
|
|
647
|
+
_context8.next = 31;
|
|
627
648
|
break;
|
|
628
649
|
}
|
|
629
650
|
_line = _context8.t2.value;
|
|
630
|
-
return _context8.delegateYield(_loop(), "t3",
|
|
631
|
-
case
|
|
651
|
+
return _context8.delegateYield(_loop(), "t3", 27);
|
|
652
|
+
case 27:
|
|
632
653
|
if (!_context8.t3) {
|
|
633
|
-
_context8.next =
|
|
654
|
+
_context8.next = 29;
|
|
634
655
|
break;
|
|
635
656
|
}
|
|
636
|
-
return _context8.abrupt("continue",
|
|
637
|
-
case
|
|
638
|
-
_context8.next =
|
|
657
|
+
return _context8.abrupt("continue", 24);
|
|
658
|
+
case 29:
|
|
659
|
+
_context8.next = 24;
|
|
639
660
|
break;
|
|
640
|
-
case
|
|
661
|
+
case 31:
|
|
641
662
|
_i++;
|
|
642
|
-
_context8.next =
|
|
663
|
+
_context8.next = 17;
|
|
643
664
|
break;
|
|
644
|
-
case
|
|
665
|
+
case 34:
|
|
645
666
|
case "end":
|
|
646
667
|
return _context8.stop();
|
|
647
668
|
}
|
|
@@ -671,21 +692,28 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
671
692
|
}
|
|
672
693
|
throw new Error('Database is destroyed');
|
|
673
694
|
case 3:
|
|
695
|
+
if (this.initialized) {
|
|
696
|
+
_context9.next = 6;
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
_context9.next = 6;
|
|
700
|
+
return this.init();
|
|
701
|
+
case 6:
|
|
674
702
|
_context9.t0 = this.shouldSave;
|
|
675
703
|
if (!_context9.t0) {
|
|
676
|
-
_context9.next =
|
|
704
|
+
_context9.next = 10;
|
|
677
705
|
break;
|
|
678
706
|
}
|
|
679
|
-
_context9.next =
|
|
707
|
+
_context9.next = 10;
|
|
680
708
|
return this.save()["catch"](console.error);
|
|
681
|
-
case
|
|
709
|
+
case 10:
|
|
682
710
|
if (!Array.isArray(criteria)) {
|
|
683
|
-
_context9.next =
|
|
711
|
+
_context9.next = 19;
|
|
684
712
|
break;
|
|
685
713
|
}
|
|
686
|
-
_context9.next =
|
|
714
|
+
_context9.next = 13;
|
|
687
715
|
return this.readLines(criteria);
|
|
688
|
-
case
|
|
716
|
+
case 13:
|
|
689
717
|
results = _context9.sent;
|
|
690
718
|
if (options.orderBy) {
|
|
691
719
|
_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 +727,22 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
699
727
|
results = results.slice(0, options.limit);
|
|
700
728
|
}
|
|
701
729
|
return _context9.abrupt("return", results);
|
|
702
|
-
case
|
|
703
|
-
_context9.next =
|
|
730
|
+
case 19:
|
|
731
|
+
_context9.next = 21;
|
|
704
732
|
return this.indexManager.query(criteria, options.matchAny);
|
|
705
|
-
case
|
|
733
|
+
case 21:
|
|
706
734
|
matchingLines = _context9.sent;
|
|
707
735
|
if (!(!matchingLines || !matchingLines.size)) {
|
|
708
|
-
_context9.next =
|
|
736
|
+
_context9.next = 24;
|
|
709
737
|
break;
|
|
710
738
|
}
|
|
711
739
|
return _context9.abrupt("return", []);
|
|
712
|
-
case
|
|
713
|
-
_context9.next =
|
|
740
|
+
case 24:
|
|
741
|
+
_context9.next = 26;
|
|
714
742
|
return this.query(_toConsumableArray(matchingLines), options);
|
|
715
|
-
case
|
|
743
|
+
case 26:
|
|
716
744
|
return _context9.abrupt("return", _context9.sent);
|
|
717
|
-
case
|
|
745
|
+
case 27:
|
|
718
746
|
case "end":
|
|
719
747
|
return _context9.stop();
|
|
720
748
|
}
|
|
@@ -747,47 +775,58 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
747
775
|
while (1) switch (_context11.prev = _context11.next) {
|
|
748
776
|
case 0:
|
|
749
777
|
options = _args11.length > 2 && _args11[2] !== undefined ? _args11[2] : {};
|
|
778
|
+
if (this.shouldTruncate) {
|
|
779
|
+
this.writeBuffer.push(this.indexOffset);
|
|
780
|
+
this.shouldTruncate = false;
|
|
781
|
+
}
|
|
750
782
|
if (!this.destroyed) {
|
|
751
|
-
_context11.next =
|
|
783
|
+
_context11.next = 4;
|
|
752
784
|
break;
|
|
753
785
|
}
|
|
754
786
|
throw new Error('Database is destroyed');
|
|
755
|
-
case
|
|
756
|
-
|
|
757
|
-
if (!_context11.t0) {
|
|
787
|
+
case 4:
|
|
788
|
+
if (this.initialized) {
|
|
758
789
|
_context11.next = 7;
|
|
759
790
|
break;
|
|
760
791
|
}
|
|
761
792
|
_context11.next = 7;
|
|
762
|
-
return this.
|
|
793
|
+
return this.init();
|
|
763
794
|
case 7:
|
|
764
|
-
_context11.
|
|
795
|
+
_context11.t0 = this.shouldSave;
|
|
796
|
+
if (!_context11.t0) {
|
|
797
|
+
_context11.next = 11;
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
_context11.next = 11;
|
|
801
|
+
return this.save()["catch"](console.error);
|
|
802
|
+
case 11:
|
|
803
|
+
_context11.next = 13;
|
|
765
804
|
return this.indexManager.query(criteria, options.matchAny);
|
|
766
|
-
case
|
|
805
|
+
case 13:
|
|
767
806
|
matchingLines = _context11.sent;
|
|
768
807
|
if (!(!matchingLines || !matchingLines.size)) {
|
|
769
|
-
_context11.next =
|
|
808
|
+
_context11.next = 16;
|
|
770
809
|
break;
|
|
771
810
|
}
|
|
772
811
|
return _context11.abrupt("return", []);
|
|
773
|
-
case
|
|
812
|
+
case 16:
|
|
774
813
|
ranges = this.getRanges(_toConsumableArray(matchingLines));
|
|
775
814
|
validMatchingLines = new Set(ranges.map(function (r) {
|
|
776
815
|
return r.index;
|
|
777
816
|
}));
|
|
778
817
|
if (validMatchingLines.size) {
|
|
779
|
-
_context11.next =
|
|
818
|
+
_context11.next = 20;
|
|
780
819
|
break;
|
|
781
820
|
}
|
|
782
821
|
return _context11.abrupt("return", []);
|
|
783
|
-
case
|
|
784
|
-
_context11.next =
|
|
822
|
+
case 20:
|
|
823
|
+
_context11.next = 22;
|
|
785
824
|
return this.readLines(_toConsumableArray(validMatchingLines), ranges);
|
|
786
|
-
case
|
|
825
|
+
case 22:
|
|
787
826
|
entries = _context11.sent;
|
|
788
827
|
lines = [];
|
|
789
828
|
_iterator = _createForOfIteratorHelper(entries);
|
|
790
|
-
_context11.prev =
|
|
829
|
+
_context11.prev = 25;
|
|
791
830
|
_loop2 = /*#__PURE__*/_regeneratorRuntime().mark(function _loop2() {
|
|
792
831
|
var entry, err, updated, ret;
|
|
793
832
|
return _regeneratorRuntime().wrap(function _loop2$(_context10) {
|
|
@@ -809,27 +848,27 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
809
848
|
}, _loop2);
|
|
810
849
|
});
|
|
811
850
|
_iterator.s();
|
|
812
|
-
case
|
|
851
|
+
case 28:
|
|
813
852
|
if ((_step = _iterator.n()).done) {
|
|
814
|
-
_context11.next =
|
|
853
|
+
_context11.next = 32;
|
|
815
854
|
break;
|
|
816
855
|
}
|
|
817
|
-
return _context11.delegateYield(_loop2(), "t1",
|
|
818
|
-
case
|
|
819
|
-
_context11.next =
|
|
856
|
+
return _context11.delegateYield(_loop2(), "t1", 30);
|
|
857
|
+
case 30:
|
|
858
|
+
_context11.next = 28;
|
|
820
859
|
break;
|
|
821
|
-
case
|
|
822
|
-
_context11.next =
|
|
860
|
+
case 32:
|
|
861
|
+
_context11.next = 37;
|
|
823
862
|
break;
|
|
824
|
-
case
|
|
825
|
-
_context11.prev =
|
|
826
|
-
_context11.t2 = _context11["catch"](
|
|
863
|
+
case 34:
|
|
864
|
+
_context11.prev = 34;
|
|
865
|
+
_context11.t2 = _context11["catch"](25);
|
|
827
866
|
_iterator.e(_context11.t2);
|
|
828
|
-
case
|
|
829
|
-
_context11.prev =
|
|
867
|
+
case 37:
|
|
868
|
+
_context11.prev = 37;
|
|
830
869
|
_iterator.f();
|
|
831
|
-
return _context11.finish(
|
|
832
|
-
case
|
|
870
|
+
return _context11.finish(37);
|
|
871
|
+
case 40:
|
|
833
872
|
offsets = [];
|
|
834
873
|
byteOffset = 0, k = 0;
|
|
835
874
|
this.offsets.forEach(function (n, i) {
|
|
@@ -843,20 +882,20 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
843
882
|
});
|
|
844
883
|
this.offsets = offsets;
|
|
845
884
|
this.indexOffset += byteOffset;
|
|
846
|
-
_context11.next =
|
|
885
|
+
_context11.next = 47;
|
|
847
886
|
return this.fileHandler.replaceLines(ranges, lines);
|
|
848
|
-
case
|
|
887
|
+
case 47:
|
|
849
888
|
_toConsumableArray(validMatchingLines).forEach(function (lineNumber, i) {
|
|
850
889
|
_this8.indexManager.dryRemove(lineNumber);
|
|
851
890
|
_this8.indexManager.add(entries[i], lineNumber);
|
|
852
891
|
});
|
|
853
892
|
this.shouldSave = true;
|
|
854
893
|
return _context11.abrupt("return", entries);
|
|
855
|
-
case
|
|
894
|
+
case 50:
|
|
856
895
|
case "end":
|
|
857
896
|
return _context11.stop();
|
|
858
897
|
}
|
|
859
|
-
}, _callee9, this, [[
|
|
898
|
+
}, _callee9, this, [[25, 34, 37, 40]]);
|
|
860
899
|
}));
|
|
861
900
|
function update(_x5, _x6) {
|
|
862
901
|
return _update.apply(this, arguments);
|
|
@@ -879,37 +918,48 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
879
918
|
while (1) switch (_context12.prev = _context12.next) {
|
|
880
919
|
case 0:
|
|
881
920
|
options = _args12.length > 1 && _args12[1] !== undefined ? _args12[1] : {};
|
|
921
|
+
if (this.shouldTruncate) {
|
|
922
|
+
this.writeBuffer.push(this.indexOffset);
|
|
923
|
+
this.shouldTruncate = false;
|
|
924
|
+
}
|
|
882
925
|
if (!this.destroyed) {
|
|
883
|
-
_context12.next =
|
|
926
|
+
_context12.next = 4;
|
|
884
927
|
break;
|
|
885
928
|
}
|
|
886
929
|
throw new Error('Database is destroyed');
|
|
887
|
-
case
|
|
888
|
-
|
|
889
|
-
if (!_context12.t0) {
|
|
930
|
+
case 4:
|
|
931
|
+
if (this.initialized) {
|
|
890
932
|
_context12.next = 7;
|
|
891
933
|
break;
|
|
892
934
|
}
|
|
893
935
|
_context12.next = 7;
|
|
894
|
-
return this.
|
|
936
|
+
return this.init();
|
|
895
937
|
case 7:
|
|
896
|
-
_context12.
|
|
938
|
+
_context12.t0 = this.shouldSave;
|
|
939
|
+
if (!_context12.t0) {
|
|
940
|
+
_context12.next = 11;
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
_context12.next = 11;
|
|
944
|
+
return this.save()["catch"](console.error);
|
|
945
|
+
case 11:
|
|
946
|
+
_context12.next = 13;
|
|
897
947
|
return this.indexManager.query(criteria, options.matchAny);
|
|
898
|
-
case
|
|
948
|
+
case 13:
|
|
899
949
|
matchingLines = _context12.sent;
|
|
900
950
|
if (!(!matchingLines || !matchingLines.size)) {
|
|
901
|
-
_context12.next =
|
|
951
|
+
_context12.next = 16;
|
|
902
952
|
break;
|
|
903
953
|
}
|
|
904
954
|
return _context12.abrupt("return", 0);
|
|
905
|
-
case
|
|
955
|
+
case 16:
|
|
906
956
|
ranges = this.getRanges(_toConsumableArray(matchingLines));
|
|
907
957
|
validMatchingLines = new Set(ranges.map(function (r) {
|
|
908
958
|
return r.index;
|
|
909
959
|
}));
|
|
910
|
-
_context12.next =
|
|
960
|
+
_context12.next = 20;
|
|
911
961
|
return this.fileHandler.replaceLines(ranges, []);
|
|
912
|
-
case
|
|
962
|
+
case 20:
|
|
913
963
|
offsets = [];
|
|
914
964
|
byteOffset = 0, k = 0;
|
|
915
965
|
this.offsets.forEach(function (n, i) {
|
|
@@ -926,7 +976,7 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
926
976
|
this.indexManager.remove(_toConsumableArray(validMatchingLines));
|
|
927
977
|
this.shouldSave = true;
|
|
928
978
|
return _context12.abrupt("return", ranges.length);
|
|
929
|
-
case
|
|
979
|
+
case 28:
|
|
930
980
|
case "end":
|
|
931
981
|
return _context12.stop();
|
|
932
982
|
}
|
|
@@ -972,7 +1022,8 @@ var Database = exports.Database = /*#__PURE__*/function (_EventEmitter) {
|
|
|
972
1022
|
}, {
|
|
973
1023
|
key: "length",
|
|
974
1024
|
get: function get() {
|
|
975
|
-
|
|
1025
|
+
var _this$offsets;
|
|
1026
|
+
return (this === null || this === void 0 || (_this$offsets = this.offsets) === null || _this$offsets === void 0 ? void 0 : _this$offsets.length) || 0;
|
|
976
1027
|
}
|
|
977
1028
|
}, {
|
|
978
1029
|
key: "index",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jexidb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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,8 +11,8 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node test/test.mjs && exit 1",
|
|
15
|
-
"
|
|
14
|
+
"test": "node --expose-gc test/test.mjs && exit 1",
|
|
15
|
+
"prepare": "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",
|
|
18
18
|
"license": "MIT",
|
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,11 +210,14 @@ 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)) {
|
|
218
|
-
if(map
|
|
219
|
-
map =
|
|
217
|
+
if (map instanceof Set) {
|
|
218
|
+
map = [...map]
|
|
219
|
+
} else if(map && typeof map === 'object') {
|
|
220
|
+
map = [...this.indexManager.query(map, options.matchAny)]
|
|
220
221
|
} else {
|
|
221
222
|
map = [...Array(this.offsets.length).keys()]
|
|
222
223
|
}
|
|
@@ -250,6 +251,7 @@ export class Database extends EventEmitter {
|
|
|
250
251
|
|
|
251
252
|
async query(criteria, options={}) {
|
|
252
253
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
254
|
+
if(!this.initialized) await this.init()
|
|
253
255
|
this.shouldSave && await this.save().catch(console.error)
|
|
254
256
|
if(Array.isArray(criteria)) {
|
|
255
257
|
let results = await this.readLines(criteria)
|
|
@@ -275,7 +277,12 @@ export class Database extends EventEmitter {
|
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
async update(criteria, data, options={}) {
|
|
280
|
+
if (this.shouldTruncate) {
|
|
281
|
+
this.writeBuffer.push(this.indexOffset)
|
|
282
|
+
this.shouldTruncate = false
|
|
283
|
+
}
|
|
278
284
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
285
|
+
if(!this.initialized) await this.init()
|
|
279
286
|
this.shouldSave && await this.save().catch(console.error)
|
|
280
287
|
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
281
288
|
if (!matchingLines || !matchingLines.size) {
|
|
@@ -317,7 +324,12 @@ export class Database extends EventEmitter {
|
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
async delete(criteria, options={}) {
|
|
327
|
+
if (this.shouldTruncate) {
|
|
328
|
+
this.writeBuffer.push(this.indexOffset)
|
|
329
|
+
this.shouldTruncate = false
|
|
330
|
+
}
|
|
320
331
|
if(this.destroyed) throw new Error('Database is destroyed')
|
|
332
|
+
if(!this.initialized) await this.init()
|
|
321
333
|
this.shouldSave && await this.save().catch(console.error)
|
|
322
334
|
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
323
335
|
if (!matchingLines || !matchingLines.size) {
|
|
@@ -355,7 +367,7 @@ export class Database extends EventEmitter {
|
|
|
355
367
|
}
|
|
356
368
|
|
|
357
369
|
get length() {
|
|
358
|
-
return this
|
|
370
|
+
return this?.offsets?.length || 0
|
|
359
371
|
}
|
|
360
372
|
|
|
361
373
|
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
|
-
}
|