@jnode/db 1.0.0

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/src/dble.js ADDED
@@ -0,0 +1,1039 @@
1
+ /*
2
+ @jnode/db/dble.js
3
+
4
+ Simple database package for Node.js.
5
+
6
+ JDB Lite Extended (DBLE) is a simple, fast and extensiable database format.
7
+
8
+ by JustApple (format design, programming, testing) &
9
+ Google Gemini (code review, small bug fix, completion of default types)
10
+ */
11
+
12
+ // load class
13
+ const { JDBFile } = require('./jdb.js');
14
+
15
+ // dble file manager
16
+ class DBLEFile {
17
+ constructor(jdb, options = {}) {
18
+ this.jdb = jdb;
19
+ this.options = options;
20
+ this._lineCache = new Map();
21
+ this.types = Object.assign({}, defaultDBLETypes, options.types);
22
+ }
23
+
24
+ static async load(path, options = {}) {
25
+ const jdb = await JDBFile.load(path);
26
+
27
+ try {
28
+ if (jdb.dbType !== 0x44424C45) throw new Error('JDB (DBLE): DBType is not "DBLE".'); // db type
29
+ if (jdb.dbVersion !== 1) throw new Error('JDB (DBLE): Unsupported DBLE DBVersion.'); // db version
30
+
31
+ const dble = new DBLEFile(jdb, options);
32
+ await dble._loadHead();
33
+ await dble._loadIndices();
34
+
35
+ return dble;
36
+ } catch (e) {
37
+ await jdb.close();
38
+ throw e;
39
+ }
40
+ }
41
+
42
+ static async create(path, options = {}) {
43
+ const jdb = await JDBFile.create(path, {
44
+ dbType: 0x44424C45,
45
+ dbVersion: 1
46
+ });
47
+
48
+ const dble = new DBLEFile(jdb, options);
49
+ try {
50
+ await dble._init(options);
51
+ return dble;
52
+ } catch (e) {
53
+ await jdb.close();
54
+ throw e;
55
+ }
56
+ }
57
+
58
+ static async forceCreate(path, options = {}) {
59
+ const jdb = await JDBFile.forceCreate(path, {
60
+ dbType: 0x44424C45,
61
+ dbVersion: 1
62
+ });
63
+
64
+ const dble = new DBLEFile(jdb, options);
65
+ try {
66
+ await dble._init(options);
67
+ return dble;
68
+ } catch (e) {
69
+ await jdb.close();
70
+ throw e;
71
+ }
72
+ }
73
+
74
+ async _init(options = {}) {
75
+ if (!options.fields) throw new Error('`options.fields` is required.');
76
+
77
+ this.fields = options.fields;
78
+ this.fieldsMap = {};
79
+ this.fieldOffsets = {};
80
+ this.indices = {};
81
+ this.relatives = {};
82
+ this.relativeOffsets = {};
83
+
84
+ // generate definitions
85
+ const dbHeadBufs = [];
86
+ let dbHeadLength = 22;
87
+ let dbLineLength = 6;
88
+ for (let i = 0; i < options.fields.length; i++) {
89
+ const def = options.fields[i];
90
+
91
+ if (this.fieldOffsets[def.name]) throw new Error('Could not have two fields in the same name.');
92
+ this.fieldOffsets[def.name] = dbLineLength;
93
+ this.fieldsMap[def.name] = def;
94
+ if (def.isKey) this.indices[def.name] = new Map(); // set index
95
+
96
+ const definitionBuf = def.toDefinitionBuffer(i === options.fields.length - 1);
97
+ dbHeadBufs.push(definitionBuf);
98
+ dbHeadLength += definitionBuf.length;
99
+ dbLineLength += def.length;
100
+ if (def.isRelative) { // set relatives
101
+ this.relatives[def.name] = this._getDefaultOf(def);
102
+ this.relativeOffsets[def.name] = dbHeadLength - def.length - 6;
103
+ }
104
+ }
105
+
106
+ this.extOffset = dbLineLength;
107
+
108
+ // calculate ext field length dynamically as a power of 2
109
+ let n = dbLineLength + 1 + (options.extBaseLength ?? 0);
110
+ n |= n >> 1;
111
+ n |= n >> 2;
112
+ n |= n >> 4;
113
+ n |= n >> 8;
114
+ n |= n >> 16;
115
+ n++;
116
+
117
+ let extBaseLength = n - dbLineLength;
118
+ this.extBaseLength = extBaseLength;
119
+ this.lineLength = n;
120
+
121
+ // padding
122
+ const dbHeadPadBuf = Buffer.alloc(((dbHeadLength + n - 1) & ~(n - 1)) - dbHeadLength + 6);
123
+ dbHeadPadBuf.writeUInt16LE(extBaseLength); // extend field base length
124
+ dbHeadPadBuf.writeUInt32LE(0xFFFFFFFF, 2); // last empty line
125
+ dbHeadBufs.push(dbHeadPadBuf);
126
+ this.lastEmptyLine = 0xFFFFFFFF;
127
+ this.lastEmptyLineOffset = dbHeadLength - 4;
128
+
129
+ // write to file
130
+ const dbHeadBuf = Buffer.concat(dbHeadBufs);
131
+ this.bodyOffset = 16 + dbHeadBuf.length;
132
+ await this.jdb.handle.write(dbHeadBuf, 0, undefined, 16);
133
+ }
134
+
135
+ async _loadHead() {
136
+ const handle = this.jdb.handle;
137
+
138
+ this.fields = [];
139
+ this.fieldsMap = {};
140
+ this.fieldOffsets = {};
141
+ this.indices = {};
142
+ this.bodyOffset = 16;
143
+ this.relatives = {};
144
+ this.relativeOffsets = {};
145
+
146
+ // parse definitions
147
+ let fieldIsLast = false;
148
+ let fieldIsKey = false;
149
+ let fieldIsRelative = false;
150
+ let fieldLength = 0;
151
+ let fieldType = 'null';
152
+ let fieldName = 'null';
153
+
154
+ let dbLineLength = 6;
155
+
156
+ const readBytes = async (len) => {
157
+ const buf = Buffer.allocUnsafe(len);
158
+ const { bytesRead } = await handle.read(buf, 0, len, this.bodyOffset);
159
+ if (bytesRead !== len) throw new Error('JDB (DBLE): Fail to parse the file.');
160
+ this.bodyOffset += len;
161
+ return buf;
162
+ };
163
+
164
+ while (!fieldIsLast) {
165
+ // load flags and field length
166
+ const flagLenBuf = await readBytes(4);
167
+ const fieldFlags = flagLenBuf.readUInt16BE();
168
+ fieldIsLast = fieldFlags & 1;
169
+ fieldIsKey = fieldFlags & (1 << 1);
170
+ fieldIsRelative = fieldFlags & (1 << 2);
171
+ fieldLength = flagLenBuf.readUInt16LE(2);
172
+
173
+ // load type
174
+ const typeLenBuf = await readBytes(1);
175
+ fieldType = (await readBytes(typeLenBuf[0])).toString('utf8');
176
+
177
+ // load name
178
+ const nameLenBuf = await readBytes(1);
179
+ fieldName = (await readBytes(nameLenBuf[0])).toString('utf8');
180
+
181
+ // save fields
182
+ const FieldType = this.types[fieldType] || DBLEAnyField;
183
+ const field = new FieldType;
184
+ field.isKey = !!fieldIsKey;
185
+ field.isRelative = !!fieldIsRelative;
186
+ field.length = fieldLength;
187
+ field.type = fieldType;
188
+ field.name = fieldName;
189
+ this.fields.push(field);
190
+
191
+ if (this.fieldOffsets[fieldName]) throw new Error('JDB (DBLE): Fail to parse the file, could not have two fields in the same name.');
192
+ this.fieldOffsets[fieldName] = dbLineLength;
193
+ this.fieldsMap[fieldName] = field;
194
+ if (fieldIsKey) this.indices[fieldName] = new Map(); // set index
195
+
196
+ // set relatives
197
+ if (fieldIsRelative) {
198
+ this.relatives[fieldName] = field.parse(await readBytes(fieldLength), 0);
199
+ this.relativeOffsets[fieldName] = this.bodyOffset - fieldLength;
200
+ }
201
+
202
+ dbLineLength += fieldLength;
203
+ }
204
+
205
+ const eblLelBuf = await readBytes(6);
206
+ this.extBaseLength = eblLelBuf.readUInt16LE();
207
+ this.extOffset = dbLineLength;
208
+ this.lineLength = dbLineLength += this.extBaseLength;
209
+ this.lastEmptyLine = eblLelBuf.readUInt32LE(2);
210
+ this.lastEmptyLineOffset = this.bodyOffset - 4;
211
+ let dbHeadLength = this.bodyOffset;
212
+
213
+ // calculate padding
214
+ this.bodyOffset += ((dbHeadLength + dbLineLength - 1) & ~(dbLineLength - 1)) - dbHeadLength;
215
+ }
216
+
217
+ _getDefaultOf(type, relative) {
218
+ if (typeof type.default === 'function') {
219
+ return type.default(relative);
220
+ }
221
+ }
222
+
223
+ // safely retrieve normalized index mapping target mapping
224
+ _getIndexKey(val) {
225
+ return Buffer.isBuffer(val) ? val.toString('hex') : val;
226
+ }
227
+
228
+ // close file handle safely through the inner task queue
229
+ close() {
230
+ return this._doTask(async () => {
231
+ return await this.jdb.close();
232
+ });
233
+ }
234
+
235
+ // load indices
236
+ async _loadIndices() {
237
+ for await (let { chunk, line } of this._readlines()) {
238
+ for (let i in this.indices) {
239
+ this.indices[i].set(this._getIndexKey(this._getFieldFromLine(chunk, i)), line);
240
+ }
241
+ }
242
+ }
243
+
244
+ // simple task queue
245
+ _doTask(func, skipQueue) {
246
+ if (skipQueue) return func();
247
+ return this._task = (async () => {
248
+ try { await this._task; } catch { }
249
+ return await func();
250
+ })();
251
+ }
252
+
253
+ _setLineCache(line, buf) {
254
+ // enforce LRU replacement mechanic mapping
255
+ this._lineCache.delete(line);
256
+ this._lineCache.set(line, buf);
257
+
258
+ // remove oldest added cache
259
+ if (this._lineCache.size > (this.options.maxLineCache || (this.options.maxLineCache = Math.ceil((this.options.maxLineCacheSize || 1048576) / this.lineLength)))) {
260
+ this._lineCache.delete(this._lineCache.keys().next().value);
261
+ }
262
+ }
263
+
264
+ // get a line's buffer
265
+ async _getLineBuffer(line) {
266
+ if (typeof line !== 'number') throw new TypeError('line must be an integer.');
267
+
268
+ // already in cache
269
+ if (this._lineCache.has(line)) return this._lineCache.get(line);
270
+
271
+ // read line
272
+ const lineBuf = Buffer.allocUnsafe(this.lineLength);
273
+ const { bytesRead } = await this.jdb.handle.read(lineBuf, 0, this.lineLength, this.bodyOffset + line * this.lineLength);
274
+ if (bytesRead !== this.lineLength) throw new Error('JDB (DBLE): Unexpected end of file.');
275
+
276
+ // add to cache
277
+ this._setLineCache(line, lineBuf);
278
+
279
+ return lineBuf;
280
+ }
281
+
282
+ // write a buffer to line
283
+ async _writeLineBuffer(line, buf, offset = 0) {
284
+ if (typeof line !== 'number') throw new TypeError('line must be an integer.');
285
+ if (!Buffer.isBuffer(buf)) throw new TypeError('buf must be a buffer.');
286
+
287
+ const writeBuf = offset === 0 && buf.length === this.lineLength ? buf : buf.subarray(offset, offset + this.lineLength);
288
+
289
+ // write line
290
+ await this.jdb.handle.write(writeBuf, 0, this.lineLength, this.bodyOffset + line * this.lineLength);
291
+
292
+ // add to cache
293
+ this._setLineCache(line, writeBuf);
294
+ }
295
+
296
+ _getRelative(field) {
297
+ return this.relatives[field];
298
+ }
299
+
300
+ async _setRelative(field, value) {
301
+ const type = this.fieldsMap[field];
302
+ const buf = Buffer.alloc(type.length);
303
+ type.write(value, buf, 0);
304
+
305
+ // write line
306
+ await this.jdb.handle.write(buf, 0, buf.length, this.relativeOffsets[field]);
307
+
308
+ this.relatives[field] = value;
309
+ }
310
+
311
+ _getDefaultByField(field, relative) {
312
+ return this._getDefaultOf(this.fieldsMap[field], relative);
313
+ }
314
+
315
+ // append a line
316
+ async _appendLineBuffer(buf, offset = 0) {
317
+ if (!Buffer.isBuffer(buf)) throw new TypeError('buf must be a buffer.');
318
+
319
+ const writeBuf = offset === 0 && buf.length === this.lineLength ? buf : buf.subarray(offset, offset + this.lineLength);
320
+ let targetLine = this.lastEmptyLine;
321
+
322
+ if (targetLine === 0xFFFFFFFF) { // no empty lines
323
+ const { size } = await this.jdb.handle.stat();
324
+
325
+ // write line
326
+ await this.jdb.handle.write(writeBuf, 0, this.lineLength, size);
327
+
328
+ // add to cache
329
+ targetLine = (size - this.bodyOffset) / this.lineLength;
330
+ this._setLineCache(targetLine, writeBuf);
331
+ } else { // empty line
332
+ const lineBuf = await this._getLineBuffer(targetLine);
333
+ this.lastEmptyLine = lineBuf.readUInt32LE(2);
334
+ await this._writeLineBuffer(targetLine, writeBuf);
335
+
336
+ // write back the last empty line
337
+ const lastEmptyLineBuf = Buffer.allocUnsafe(4);
338
+ lastEmptyLineBuf.writeUInt32LE(this.lastEmptyLine);
339
+ await this.jdb.handle.write(lastEmptyLineBuf, 0, 4, this.lastEmptyLineOffset);
340
+ }
341
+
342
+ // update for start line
343
+ if ((writeBuf[0] & 0xC0) === (0b00 << 6)) {
344
+ // update indecies
345
+ for (let i in this.indices) {
346
+ this.indices[i].set(this._getIndexKey(this._getFieldFromLine(writeBuf, i)), targetLine);
347
+ }
348
+
349
+ // update relatives
350
+ for (let i in this.relatives) {
351
+ await this._setRelative(i, this._getFieldFromLine(writeBuf, i));
352
+ }
353
+ }
354
+
355
+ return targetLine;
356
+ }
357
+
358
+ async _getLastEmptyLine(after) {
359
+ if ((typeof after !== 'number') && (this.lastEmptyLine !== 0xFFFFFFFF)) return this.lastEmptyLine;
360
+
361
+ const { size } = await this.jdb.handle.stat();
362
+ const newLine = (size - this.bodyOffset) / this.lineLength;
363
+
364
+ if (typeof after === 'number') {
365
+ if (after >= newLine) return after + 1;
366
+ const lineBuf = await this._getLineBuffer(after);
367
+ const next = lineBuf.readUInt32LE(2);
368
+ if (next !== 0xFFFFFFFF) return next;
369
+ }
370
+
371
+ return newLine;
372
+ }
373
+
374
+ _getFieldFromLine(buf, field) {
375
+ return this.fieldsMap[field].parse(buf, this.fieldOffsets[field]);
376
+ }
377
+
378
+ _setFieldsToLine(buf, fields) {
379
+ for (let field in fields) {
380
+ if (this.fieldsMap[field]) {
381
+ if (fields[field] === undefined) continue;
382
+ this.fieldsMap[field].write(fields[field], buf, this.fieldOffsets[field]);
383
+ }
384
+ }
385
+ return buf;
386
+ }
387
+
388
+ async _clearLinesFrom(line) {
389
+ if (line === 0xFFFFFFFF) return;
390
+
391
+ let lineBuf;
392
+ let nextLine = line;
393
+
394
+ while (nextLine !== 0xFFFFFFFF) {
395
+ lineBuf = await this._getLineBuffer(nextLine);
396
+ const actualNext = lineBuf.readUInt32LE(2);
397
+
398
+ const type = lineBuf[0] & 0xC0;
399
+ if (type === (0b00 << 6) || type === (0b11 << 6)) { // remove from index mapping
400
+ for (let i in this.indices) {
401
+ const key = this._getIndexKey(this._getFieldFromLine(lineBuf, i));
402
+ if (this.indices[i].get(key) === nextLine) this.indices[i].delete(key);
403
+ }
404
+ }
405
+
406
+ const clearedBuf = Buffer.alloc(this.lineLength);
407
+ clearedBuf[0] = (lineBuf[0] & 0x3F) | (0b10 << 6); // Preserve 14-bit flag, toggle 'Empty' state safely
408
+ clearedBuf.writeUInt32LE(this.lastEmptyLine, 2);
409
+
410
+ this.lastEmptyLine = nextLine;
411
+ await this._writeLineBuffer(nextLine, clearedBuf);
412
+ this._lineCache.delete(nextLine);
413
+
414
+ nextLine = actualNext;
415
+ }
416
+
417
+ // write back the last empty line
418
+ const lastEmptyLineBuf = Buffer.allocUnsafe(4);
419
+ lastEmptyLineBuf.writeUInt32LE(this.lastEmptyLine);
420
+ await this.jdb.handle.write(lastEmptyLineBuf, 0, 4, this.lastEmptyLineOffset);
421
+ }
422
+
423
+ async * _readlines(start = 0, end, startLineOnly = true) {
424
+ let line = start;
425
+ const bufSize = this.options.scanBufferLines || (this.options.scanBufferLines = Math.ceil((this.options.scanBufferSize || 1048576) / this.lineLength));
426
+ let chunkLine = 0;
427
+ let chunk = null;
428
+ let chunkSize = 0;
429
+
430
+ while (true) {
431
+ if (end !== undefined && line >= end) return;
432
+ if (chunkLine === chunkSize) {
433
+ chunk = Buffer.allocUnsafe(bufSize * this.lineLength);
434
+ const { bytesRead } = await this.jdb.handle.read(chunk, 0, chunk.length, this.bodyOffset + line * this.lineLength);
435
+ chunkSize = bytesRead / this.lineLength;
436
+ if (bytesRead % this.lineLength !== 0) throw new Error('JDB (DBLE): File broken. Reading non-aligned chunk blocks.');
437
+ if (bytesRead === 0) return;
438
+ chunkLine = 0;
439
+ continue;
440
+ }
441
+
442
+ const lineChunk = chunk.subarray(chunkLine * this.lineLength, (chunkLine + 1) * this.lineLength);
443
+
444
+ line++;
445
+ chunkLine++;
446
+
447
+ if (startLineOnly && ((lineChunk[0] & 0xC0) !== (0b00 << 6))) continue;
448
+ yield { chunk: lineChunk, line: line - 1 };
449
+ }
450
+ }
451
+
452
+ _parseLine(buf) {
453
+ const lineType = (buf[0] & 0xC0) >> 6;
454
+ const nextLine = buf.readUInt32LE(2);
455
+ const line = { type: lineType };
456
+
457
+ if (line.type === 0b00) { // normal start
458
+ line.nextLine = nextLine;
459
+
460
+ line.fields = {};
461
+ for (let i in this.fieldsMap) {
462
+ line.fields[i] = this._getFieldFromLine(buf, i);
463
+ }
464
+ } else if (line.type === 0b01) { // extended (this function does not parse extended lines)
465
+ line.nextLine = nextLine;
466
+ } else if (line.type === 0b10) { // empty
467
+ line.lastEmptyLine = nextLine;
468
+ }
469
+
470
+ return line;
471
+ }
472
+
473
+ // get field from line
474
+ getField(line, field, skipQueue) {
475
+ return this._doTask(async () => {
476
+ return this._getFieldFromLine(await this._getLineBuffer(line), field);
477
+ }, skipQueue);
478
+ }
479
+
480
+ getExt(line, skipQueue) {
481
+ return this._doTask(async () => {
482
+ let extBufs = [];
483
+ let lineBuf;
484
+ let nextLine = line;
485
+ let length;
486
+ let lengthRead = 0;
487
+
488
+ while (nextLine !== 0xFFFFFFFF) {
489
+ if (length === lengthRead) break;
490
+
491
+ lineBuf = await this._getLineBuffer(nextLine);
492
+ nextLine = lineBuf.readUInt32LE(2);
493
+
494
+ if (extBufs.length === 0) {
495
+ length = lineBuf.readUInt16LE(this.extOffset);
496
+ const extBufPart = lineBuf.subarray(this.extOffset + 2, Math.min(this.extOffset + 2 + length, lineBuf.length));
497
+ lengthRead += extBufPart.length;
498
+ extBufs.push(extBufPart);
499
+ } else {
500
+ const extBufPart = lineBuf.subarray(6, Math.min(6 + length - lengthRead, lineBuf.length));
501
+ lengthRead += extBufPart.length;
502
+ extBufs.push(extBufPart);
503
+ }
504
+ }
505
+
506
+ return Buffer.concat(extBufs);
507
+ }, skipQueue);
508
+ }
509
+
510
+ getLineByField(field, query, skipQueue) {
511
+ return this._doTask(async () => {
512
+ if (this.indices[field]) {
513
+ return this.indices[field].get(this._getIndexKey(query));
514
+ } else {
515
+ for await (let { chunk, line } of this._readlines()) {
516
+ let val = this._getFieldFromLine(chunk, field);
517
+ if (Buffer.isBuffer(val) && Buffer.isBuffer(query)) {
518
+ if (val.equals(query)) return line;
519
+ } else if (val === query) {
520
+ return line;
521
+ }
522
+ }
523
+ }
524
+ }, skipQueue);
525
+ }
526
+
527
+ readLine(line, skipQueue) {
528
+ return this._doTask(async () => {
529
+ return this._parseLine(await this._getLineBuffer(line));
530
+ }, skipQueue);
531
+ }
532
+
533
+ readLineByField(field, query, skipQueue) {
534
+ return this._doTask(async () => {
535
+ const line = await this.getLineByField(field, query, true);
536
+ if (line === undefined) return;
537
+ return this.readLine(line, true);
538
+ }, skipQueue);
539
+ }
540
+
541
+ findLine(fn, skipQueue) {
542
+ return this._doTask(async () => {
543
+ for await (let { chunk, line } of this._readlines()) {
544
+ const parsed = this._parseLine(chunk);
545
+ if (await fn(parsed)) return line;
546
+ }
547
+ }, skipQueue);
548
+ }
549
+
550
+ forEachLine(fn, start, to, skipQueue) {
551
+ return this._doTask(async () => {
552
+ for await (let { chunk, line } of this._readlines(start, to)) {
553
+ const parsed = this._parseLine(chunk);
554
+ await fn(parsed, line);
555
+ }
556
+ }, skipQueue);
557
+ }
558
+
559
+ setLine(line, fields = {}, skipQueue) {
560
+ return this._doTask(async () => {
561
+ if (typeof line === 'number') { // edit a line
562
+ const buf = await this._getLineBuffer(line);
563
+ buf[0] = (buf[0] & 0x3F) | (0b00 << 6); // set type appropriately
564
+
565
+ // check index mappings accurately
566
+ const oldValues = {};
567
+ for (let i in this.indices) {
568
+ if (fields[i] !== undefined) oldValues[i] = this._getFieldFromLine(buf, i);
569
+ }
570
+
571
+ this._setFieldsToLine(buf, fields);
572
+
573
+ for (let i in this.indices) {
574
+ if (fields[i] !== undefined) {
575
+ const oldVal = oldValues[i];
576
+ const newVal = this._getFieldFromLine(buf, i);
577
+
578
+ let isDifferent = oldVal !== newVal;
579
+ if (Buffer.isBuffer(oldVal) && Buffer.isBuffer(newVal)) {
580
+ isDifferent = !oldVal.equals(newVal);
581
+ }
582
+
583
+ if (isDifferent) {
584
+ const oldKey = this._getIndexKey(oldVal);
585
+ if (this.indices[i].get(oldKey) === line) this.indices[i].delete(oldKey);
586
+ this.indices[i].set(this._getIndexKey(newVal), line);
587
+ }
588
+ }
589
+ }
590
+
591
+ await this._writeLineBuffer(line, buf);
592
+ return line;
593
+ } else { // create a new line
594
+ return await this.appendLine(fields, true);
595
+ }
596
+ }, skipQueue);
597
+ }
598
+
599
+ setLineByField(field, query, fields = {}, skipQueue) {
600
+ return this._doTask(async () => {
601
+ const line = await this.getLineByField(field, query, true);
602
+ if (fields[field] === undefined) fields[field] = query;
603
+ await this.setLine(line, fields, true);
604
+ }, skipQueue);
605
+ }
606
+
607
+ appendLine(fields = {}, skipQueue) {
608
+ return this._doTask(async () => {
609
+ const buf = Buffer.alloc(this.lineLength);
610
+ buf[0] = (buf[0] & 0x3F) | (0b00 << 6); // set type
611
+
612
+ // set relatives
613
+ for (let i in this.relatives) {
614
+ fields[i] = fields[i] ?? this._getDefaultByField(i, this.relatives[i]);
615
+ }
616
+
617
+ this._setFieldsToLine(buf, fields);
618
+ buf.writeUInt32LE(0xFFFFFFFF, 2);
619
+
620
+ return await this._appendLineBuffer(buf);
621
+ }, skipQueue);
622
+ }
623
+
624
+ deleteLine(line, releaseSpace, skipQueue) {
625
+ return this._doTask(async () => {
626
+ if (releaseSpace) {
627
+ await this._clearLinesFrom(line);
628
+ } else {
629
+ const buf = await this._getLineBuffer(line);
630
+ if ((buf[0] & 0xC0) === (0b00 << 6)) {
631
+ for (let i in this.indices) {
632
+ this.indices[i].delete(this._getIndexKey(this._getFieldFromLine(buf, i)));
633
+ }
634
+ }
635
+ buf[0] = (buf[0] & 0x3F) | (0b11 << 6); // preserve flags safely, set deleted type
636
+ await this._writeLineBuffer(line, buf);
637
+ }
638
+ }, skipQueue);
639
+ }
640
+
641
+ deleteLineByField(field, query, releaseSpace, skipQueue) {
642
+ return this._doTask(async () => {
643
+ const line = await this.getLineByField(field, query, true);
644
+ if (line !== undefined) await this.deleteLine(line, releaseSpace, true);
645
+ }, skipQueue);
646
+ }
647
+
648
+ cleanUp(skipQueue) {
649
+ return this._doTask(async () => {
650
+ for await (let { chunk, line } of this._readlines(0, undefined, false)) {
651
+ if ((chunk[0] & 0xC0) === (0b11 << 6)) await this._clearLinesFrom(line);
652
+ }
653
+ }, skipQueue);
654
+ }
655
+
656
+ setExt(line, ext = Buffer.alloc(0), skipQueue) {
657
+ return this._doTask(async () => {
658
+ if (!Buffer.isBuffer(ext)) ext = Buffer.from(ext);
659
+
660
+ const linesNeeded = ext.length <= this.extBaseLength - 2 ? 1 : 1 + Math.ceil((ext.length - this.extBaseLength + 2) / (this.lineLength - 6));
661
+ let offset = this.extBaseLength - 2;
662
+ let lineBuf = await this._getLineBuffer(line);
663
+ await this._clearLinesFrom(lineBuf.readUInt32LE(2));
664
+
665
+ // write length and first line
666
+ let nextLine = await this._getLastEmptyLine();
667
+ lineBuf.writeUInt32LE((linesNeeded > 1) ? nextLine : 0xFFFFFFFF, 2);
668
+ lineBuf.writeUInt16LE(ext.length, this.extOffset);
669
+
670
+ const baseWriteLen = Math.min(ext.length, this.extBaseLength - 2);
671
+ ext.copy(lineBuf, this.extOffset + 2, 0, baseWriteLen);
672
+ lineBuf.fill(0, this.extOffset + 2 + baseWriteLen, this.extOffset + this.extBaseLength); // clean trailing ghosts
673
+
674
+ await this._writeLineBuffer(line, lineBuf);
675
+
676
+ for (let i = 1; i < linesNeeded; i++) {
677
+ nextLine = await this._getLastEmptyLine(nextLine);
678
+ const extBuf = Buffer.alloc(this.lineLength);
679
+ extBuf[0] = (extBuf[0] & 0x3F) | (0b01 << 6); // extended type toggle preserving 14-bit flag bounds
680
+ extBuf.writeUInt32LE((linesNeeded - 1 > i) ? nextLine : 0xFFFFFFFF, 2);
681
+
682
+ const partLen = Math.min(ext.length - offset, this.lineLength - 6);
683
+ ext.copy(extBuf, 6, offset, offset + partLen);
684
+ offset += partLen;
685
+
686
+ await this._appendLineBuffer(extBuf);
687
+ }
688
+ }, skipQueue);
689
+ }
690
+
691
+ setExtByField(field, query, ext, skipQueue) {
692
+ return this._doTask(async () => {
693
+ const line = await this.getLineByField(field, query, true);
694
+ if (line !== undefined) await this.setExt(line, ext, true);
695
+ else throw new Error(`JDB (DBLE): Line not found by field "${field}".`);
696
+ }, skipQueue);
697
+ }
698
+
699
+ async sync(skipQueue) {
700
+ return this._doTask(async () => {
701
+ await this.jdb.sync();
702
+ }, skipQueue);
703
+ }
704
+
705
+ async datasync(skipQueue) {
706
+ return this._doTask(async () => {
707
+ await this.jdb.datasync();
708
+ }, skipQueue);
709
+ }
710
+ }
711
+
712
+ // DBLE field
713
+ class DBLEField {
714
+ constructor(length = 0, type = 'null', name = 'null', isKey = false, isRelative = false) {
715
+ this.length = length;
716
+ this.type = type;
717
+ this.name = name;
718
+ this.isKey = isKey;
719
+ this.isRelative = isRelative;
720
+ }
721
+
722
+ parse(buf, offset) {
723
+ return buf.subarray(offset, offset + this.length);
724
+ }
725
+
726
+ write(data = Buffer.alloc(this.length), buf = Buffer.alloc(this.length), offset = 0) {
727
+ data.copy(buf, offset, 0, this.length);
728
+ return buf;
729
+ }
730
+
731
+ default(relative) {
732
+ return;
733
+ }
734
+
735
+ toDefinitionBuffer(isLast) {
736
+ const typeBuf = Buffer.from(this.type, 'utf8');
737
+ const nameBuf = Buffer.from(this.name, 'utf8');
738
+
739
+ // safety check
740
+ if (typeBuf.length > 255) throw new Error('Field type length must not over 255 bytes.');
741
+ if (nameBuf.length > 255) throw new Error('Field name length must not over 255 bytes.');
742
+
743
+ const definitionBuf = Buffer.alloc(6 + typeBuf.length + nameBuf.length + (this.isRelative ? this.length : 0));
744
+
745
+ definitionBuf.writeUInt16BE(
746
+ (isLast ? 1 : 0) | // isLast
747
+ (this.isKey ? 1 << 1 : 0) | // isKey
748
+ (this.isRelative ? 1 << 2 : 0) // isRelative
749
+ ); // flag
750
+ definitionBuf.writeUInt16LE(this.length, 2); // field length
751
+ definitionBuf.writeUInt8(typeBuf.length, 4); // field type length
752
+ typeBuf.copy(definitionBuf, 5); // field type
753
+ definitionBuf.writeUInt8(nameBuf.length, 5 + typeBuf.length); // field name length
754
+ nameBuf.copy(definitionBuf, 6 + typeBuf.length); // field name
755
+
756
+ return definitionBuf;
757
+ }
758
+ }
759
+
760
+ // DBLE field types
761
+
762
+ // Int8 (1 byte)
763
+ class DBLEInt8Field extends DBLEField {
764
+ constructor(name, isKey, isRelative) {
765
+ super(1, 'Int8', name, isKey, isRelative);
766
+ }
767
+
768
+ parse(buf, offset) {
769
+ return buf.readInt8(offset);
770
+ }
771
+
772
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
773
+ buf.writeInt8(data, offset);
774
+ return buf;
775
+ }
776
+
777
+ default(relative = -1) {
778
+ return relative + 1;
779
+ }
780
+ }
781
+
782
+ // UInt8 (1 byte)
783
+ class DBLEUInt8Field extends DBLEField {
784
+ constructor(name, isKey, isRelative) {
785
+ super(1, 'UInt8', name, isKey, isRelative);
786
+ }
787
+
788
+ parse(buf, offset) {
789
+ return buf.readUInt8(offset);
790
+ }
791
+
792
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
793
+ buf.writeUInt8(data, offset);
794
+ return buf;
795
+ }
796
+
797
+ default(relative = -1) {
798
+ return relative + 1;
799
+ }
800
+ }
801
+
802
+ // Int16 (2 bytes)
803
+ class DBLEInt16Field extends DBLEField {
804
+ constructor(name, isKey, isRelative) {
805
+ super(2, 'Int16', name, isKey, isRelative);
806
+ }
807
+
808
+ parse(buf, offset) {
809
+ return buf.readInt16LE(offset);
810
+ }
811
+
812
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
813
+ buf.writeInt16LE(data, offset);
814
+ return buf;
815
+ }
816
+
817
+ default(relative = -1) {
818
+ return relative + 1;
819
+ }
820
+ }
821
+
822
+ // UInt16 (2 bytes)
823
+ class DBLEUInt16Field extends DBLEField {
824
+ constructor(name, isKey, isRelative) {
825
+ super(2, 'UInt16', name, isKey, isRelative);
826
+ }
827
+
828
+ parse(buf, offset) {
829
+ return buf.readUInt16LE(offset);
830
+ }
831
+
832
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
833
+ buf.writeUInt16LE(data, offset);
834
+ return buf;
835
+ }
836
+
837
+ default(relative = -1) {
838
+ return relative + 1;
839
+ }
840
+ }
841
+
842
+ // Int32 (4 bytes)
843
+ class DBLEInt32Field extends DBLEField {
844
+ constructor(name, isKey, isRelative) {
845
+ super(4, 'Int32', name, isKey, isRelative);
846
+ }
847
+
848
+ parse(buf, offset) {
849
+ return buf.readInt32LE(offset);
850
+ }
851
+
852
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
853
+ buf.writeInt32LE(data, offset);
854
+ return buf;
855
+ }
856
+
857
+ default(relative = -1) {
858
+ return relative + 1;
859
+ }
860
+ }
861
+
862
+ // UInt32 (4 bytes)
863
+ class DBLEUInt32Field extends DBLEField {
864
+ constructor(name, isKey, isRelative) {
865
+ super(4, 'UInt32', name, isKey, isRelative);
866
+ }
867
+
868
+ parse(buf, offset) {
869
+ return buf.readUInt32LE(offset);
870
+ }
871
+
872
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
873
+ buf.writeUInt32LE(data, offset);
874
+ return buf;
875
+ }
876
+
877
+ default(relative = -1) {
878
+ return relative + 1;
879
+ }
880
+ }
881
+
882
+ // BigInt64 (8 bytes)
883
+ class DBLEBigInt64Field extends DBLEField {
884
+ constructor(name, isKey, isRelative) {
885
+ super(8, 'BigInt64', name, isKey, isRelative);
886
+ }
887
+
888
+ parse(buf, offset) {
889
+ return buf.readBigInt64LE(offset);
890
+ }
891
+
892
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
893
+ buf.writeBigInt64LE(BigInt(data), offset);
894
+ return buf;
895
+ }
896
+
897
+ default(relative = -1n) {
898
+ return relative + 1n;
899
+ }
900
+ }
901
+
902
+ // BigUInt64 (8 bytes)
903
+ class DBLEBigUInt64Field extends DBLEField {
904
+ constructor(name, isKey, isRelative) {
905
+ super(8, 'BigUInt64', name, isKey, isRelative);
906
+ }
907
+
908
+ parse(buf, offset) {
909
+ return buf.readBigUInt64LE(offset);
910
+ }
911
+
912
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
913
+ buf.writeBigUInt64LE(BigInt(data), offset);
914
+ return buf;
915
+ }
916
+
917
+ default(relative = -1n) {
918
+ return relative + 1n;
919
+ }
920
+ }
921
+
922
+ // Float (4 bytes)
923
+ class DBLEFloatField extends DBLEField {
924
+ constructor(name, isKey, isRelative) {
925
+ super(4, 'Float', name, isKey, isRelative);
926
+ }
927
+
928
+ parse(buf, offset) {
929
+ return buf.readFloatLE(offset);
930
+ }
931
+
932
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
933
+ buf.writeFloatLE(data, offset);
934
+ return buf;
935
+ }
936
+
937
+ default(relative = -1) {
938
+ return relative + 1;
939
+ }
940
+ }
941
+
942
+ // Double (8 bytes)
943
+ class DBLEDoubleField extends DBLEField {
944
+ constructor(name, isKey, isRelative) {
945
+ super(8, 'Double', name, isKey, isRelative);
946
+ }
947
+
948
+ parse(buf, offset) {
949
+ return buf.readDoubleLE(offset);
950
+ }
951
+
952
+ write(data, buf = Buffer.alloc(this.length), offset = 0) {
953
+ buf.writeDoubleLE(data, offset);
954
+ return buf;
955
+ }
956
+
957
+ default(relative = -1) {
958
+ return relative + 1;
959
+ }
960
+ }
961
+
962
+ // String (2 + n Bytes)
963
+ class DBLEStringField extends DBLEField {
964
+ constructor(maxLength, name, isKey, isRelative) {
965
+ super(2 + maxLength, 'String', name, isKey, isRelative);
966
+ }
967
+
968
+ parse(buf, offset) {
969
+ const len = buf.readUInt16LE(offset);
970
+ return buf.toString('utf8', offset + 2, offset + 2 + len);
971
+ }
972
+
973
+ write(data, buf = Buffer.allocUnsafe(this.length), offset = 0) {
974
+ const strBuf = Buffer.from(data, 'utf8');
975
+ const len = Math.min(strBuf.length, this.length - 2);
976
+
977
+ buf.writeUInt16LE(len, offset);
978
+ strBuf.copy(buf, offset + 2, 0, len);
979
+ buf.fill(0, offset + 2 + len, offset + this.length);
980
+ return buf;
981
+ }
982
+ }
983
+
984
+ // Buffer (2 + n Bytes)
985
+ class DBLEBufferField extends DBLEField {
986
+ constructor(maxLength, name, isKey, isRelative) {
987
+ super(2 + maxLength, 'Buffer', name, isKey, isRelative);
988
+ }
989
+
990
+ parse(buf, offset) {
991
+ const len = buf.readUInt16LE(offset);
992
+ return buf.subarray(offset + 2, offset + 2 + len);
993
+ }
994
+
995
+ write(data, buf = Buffer.allocUnsafe(this.length), offset = 0) {
996
+ const source = Buffer.isBuffer(data) ? data : Buffer.from(data);
997
+ const len = Math.min(source.length, this.length - 2);
998
+
999
+ buf.writeUInt16LE(len, offset);
1000
+ source.copy(buf, offset + 2, 0, len);
1001
+ buf.fill(0, offset + 2 + len, offset + this.length);
1002
+ return buf;
1003
+ }
1004
+ }
1005
+
1006
+ class DBLEAnyField extends DBLEField {
1007
+ constructor(length, name, isKey, isRelative) {
1008
+ super(length, 'Any', name, isKey, isRelative);
1009
+ }
1010
+ }
1011
+
1012
+ // dble default types
1013
+ const defaultDBLETypes = {
1014
+ Int8: DBLEInt8Field, i8: DBLEInt8Field,
1015
+ UInt8: DBLEUInt8Field, u8: DBLEUInt8Field, byte: DBLEUInt8Field,
1016
+ Int16: DBLEInt16Field, i16: DBLEInt16Field,
1017
+ UInt16: DBLEUInt16Field, u16: DBLEUInt16Field,
1018
+ Int32: DBLEInt32Field, i32: DBLEInt32Field,
1019
+ UInt32: DBLEUInt32Field, u32: DBLEUInt32Field,
1020
+ BigInt64: DBLEBigInt64Field, i64: DBLEBigInt64Field,
1021
+ BigUInt64: DBLEBigUInt64Field, u64: DBLEBigUInt64Field,
1022
+ Float: DBLEFloatField, f32: DBLEFloatField,
1023
+ Double: DBLEDoubleField, f64: DBLEDoubleField,
1024
+ String: DBLEStringField, str: DBLEStringField,
1025
+ Buffer: DBLEBufferField, buf: DBLEBufferField,
1026
+ Any: DBLEAnyField, any: DBLEBufferField
1027
+ };
1028
+
1029
+ // export
1030
+ module.exports = {
1031
+ DBLEFile,
1032
+ DBLEField,
1033
+ DBLEInt8Field, DBLEUInt8Field, DBLEInt16Field, DBLEUInt16Field, DBLEInt32Field, DBLEUInt32Field,
1034
+ DBLEBigInt64Field, DBLEBigUInt64Field,
1035
+ DBLEFloatField, DBLEDoubleField,
1036
+ DBLEStringField, DBLEBufferField,
1037
+ DBLEAnyField,
1038
+ defaultDBLETypes
1039
+ };