edinburgh 0.1.2 → 0.3.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.
@@ -0,0 +1,655 @@
1
+ const encoder = new TextEncoder();
2
+ const decoder = new TextDecoder('utf-8', { fatal: true });
3
+
4
+ // EOD marker for container parsing
5
+ const EOD = Symbol('EOD');
6
+
7
+ const BASE64_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$';
8
+ const BASE64_LOOKUP = new Uint8Array(128).fill(255); // Use 255 as invalid marker;
9
+ for (let i = 0; i < BASE64_CHARS.length; ++i) BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
10
+
11
+ const COLORS = ['\x1b[32m', '\x1b[33m', '\x1b[34m', '\x1b[35m']; // green, yellow, blue, magenta
12
+ const RESET_COLOR = '\x1b[0m';
13
+ const ERROR_COLOR = '\x1b[31m'; // red
14
+
15
+ let toStringTermCount = 0;
16
+ let useExtendedLogging = !!process.env.DATAPACK_EXTENDED_LOGGING;
17
+
18
+ /**
19
+ * A byte buffer for efficient reading and writing of primitive values and bit sequences.
20
+ *
21
+ * The DataPack class provides methods for serializing and deserializing various data types
22
+ * including numbers, strings, bit sequences, and other primitive values to/from byte buffers.
23
+ * It supports both reading and writing operations with automatic buffer management.
24
+ */
25
+
26
+ export class DataPack {
27
+ private buffer: Uint8Array;
28
+ private dataView?: DataView;
29
+
30
+ public readPos: number = 0;
31
+ public writePos: number = 0;
32
+
33
+ /**
34
+ * Backward compatibility: Access to the internal buffer
35
+ */
36
+ get _buffer(): Uint8Array {
37
+ return this.buffer;
38
+ }
39
+
40
+ /**
41
+ * Create a new DataPack instance.
42
+ * @param data - Optional initial data as Uint8Array or buffer size as number.
43
+ */
44
+ constructor(data: Uint8Array | number = 3900) {
45
+ if (data instanceof Uint8Array) {
46
+ this.buffer = data;
47
+ this.writePos = data.length;
48
+ } else {
49
+ this.buffer = new Uint8Array(data);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Helper function to write a multi-byte integer with length prefix
55
+ * @param value - The value to write
56
+ * @param headerType - The type bits (0-7) for the header
57
+ * @param invertBytes - Whether to invert bytes (for negative numbers)
58
+ * @param invertByteCount - Whether to invert the byte count (for type 0)
59
+ */
60
+ private writeMultiByteNumber(value: number, headerType: number, invertBytes: boolean = false, invertByteCount: boolean = false): void {
61
+ let byteCount = 0;
62
+ let temp = value;
63
+ while (temp > 0) {
64
+ byteCount++;
65
+ temp = Math.floor(temp / 256);
66
+ }
67
+
68
+ const encodedByteCount = invertByteCount ? ((~byteCount) & 0x1F) : byteCount;
69
+ this.buffer[this.writePos++] = (headerType << 5) | encodedByteCount;
70
+
71
+ let max = 1;
72
+ for (let j = 0; j < byteCount; j++) max *= 256;
73
+ for (let i = 0; i < byteCount; i++) {
74
+ max = Math.floor(max / 256);
75
+ let byte = Math.floor(value / max) % 256;
76
+ if (invertBytes) byte ^= 0xFF;
77
+ this.buffer[this.writePos++] = byte;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Helper function to read a multi-byte integer with length prefix
83
+ * @param byteCount - Number of bytes to read
84
+ * @param invertBytes - Whether to invert bytes (for negative numbers)
85
+ * @returns The read value
86
+ */
87
+ private readMultiByteNumber(byteCount: number, invertBytes: boolean = false): number {
88
+ if (this.readPos + byteCount > this.writePos) this.notEnoughData('number');
89
+ let value = 0;
90
+ let multiplier = 1;
91
+ for (let i = byteCount - 1; i >= 0; i--) {
92
+ let byte = this.buffer[this.readPos + i];
93
+ if (invertBytes) byte ^= 0xFF;
94
+ value += byte * multiplier;
95
+ multiplier *= 256;
96
+ }
97
+ this.readPos += byteCount;
98
+ return value;
99
+ }
100
+
101
+ private notEnoughData(type: string): never {
102
+ throw new Error(`Not enough data to read a ${type} at position ${this.readPos} in\n${this.toString(true)}`);
103
+ }
104
+
105
+ /**
106
+ * Each data item starts with a single byte. The high 3 bits indicate the type.
107
+ * The low 5 bits indicate a size or a subtype, depending on the type.
108
+ *
109
+ * 0: negative integer (byte count encoded as bitwise NOT in lower bits)
110
+ * 1: small integer 0..31 (encoded in lower bits)
111
+ * 2: small integer 32...63 (encoded in lower bits)
112
+ * 3: integer (byte count encoded in lower bits)
113
+ * 4:
114
+ * 0: float64 (8 bytes follow)
115
+ * 1: undefined
116
+ * 2: null
117
+ * 3: true
118
+ * 4: false
119
+ * 5: array start (followed by a items until EOD)
120
+ * 6: object start (followed by key+value pairs until EOD)
121
+ * 7: map start (followed by key+value pairs until EOD)
122
+ * 8: set start (followed by items until EOD)
123
+ * 9: EOD
124
+ * 10: identifier (6 byte positive int follows, represented as a base64 string of length 8)
125
+ * 11: null-terminated string
126
+ * 5: short string (length in lower bits, 0-31)
127
+ * 6: string (byte count of length in lower bits)
128
+ * 7: blob (byte count of length in lower bits)
129
+ */
130
+ write(data: any): DataPack {
131
+ // Pessimistic capacity allocation - covers most cases
132
+ this.ensureCapacity(33); // 1 byte header + up to 32 bytes for large integers/length encoding
133
+
134
+ if (typeof data === 'number') {
135
+ if (Number.isInteger(data) && data <= Number.MAX_SAFE_INTEGER && data >= Number.MIN_SAFE_INTEGER) {
136
+ if (data >= 0) {
137
+ if (data < 32) { // Type 1: small positive integer 0...31
138
+ this.buffer[this.writePos++] = (1 << 5) | data;
139
+ } else if (data < 64) { // Type 2: small positive integer 32...63
140
+ this.buffer[this.writePos++] = (2 << 5) | (data-32);
141
+ } else { // Type 3: positive integer (byte count in lower bits)
142
+ this.writeMultiByteNumber(data-64, 3);
143
+ }
144
+ } else {
145
+ // Type 0: negative integer (byte count as bitwise NOT in lower bits)
146
+ this.writeMultiByteNumber(-data, 0, true, true);
147
+ }
148
+ } else {
149
+ // Type 4, subtype 0: float64
150
+ this.buffer[this.writePos++] = (4 << 5) | 0;
151
+ this.dataView ||= new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
152
+ this.dataView.setFloat64(this.writePos, data);
153
+ this.writePos += 8;
154
+ }
155
+ } else if (data === undefined) {
156
+ this.buffer[this.writePos++] = (4 << 5) | 1;
157
+ } else if (data === null) {
158
+ this.buffer[this.writePos++] = (4 << 5) | 2;
159
+ } else if (data === true) {
160
+ this.buffer[this.writePos++] = (4 << 5) | 3;
161
+ } else if (data === false) {
162
+ this.buffer[this.writePos++] = (4 << 5) | 4;
163
+ } else if (typeof data === 'string') {
164
+ const encoded = encoder.encode(data);
165
+ if (encoded.length < 32) {
166
+ // Type 5: short string (length in lower bits, 0-31)
167
+ this.buffer[this.writePos++] = (5 << 5) | encoded.length;
168
+ } else {
169
+ // Type 6: string (byte count of length in lower bits)
170
+ this.writeMultiByteNumber(encoded.length, 6);
171
+ }
172
+ this.ensureCapacity(encoded.length);
173
+ this.buffer.set(encoded, this.writePos);
174
+ this.writePos += encoded.length;
175
+ } else if (data instanceof Uint8Array) {
176
+ // Type 7: blob (byte count of length in lower bits)
177
+ this.writeMultiByteNumber(data.length, 7);
178
+ this.ensureCapacity(data.length);
179
+ this.buffer.set(data, this.writePos);
180
+ this.writePos += data.length;
181
+ } else if (Array.isArray(data)) {
182
+ // Type 4, subtype 5: array start
183
+ this.buffer[this.writePos++] = (4 << 5) | 5;
184
+ for (const item of data) {
185
+ this.write(item);
186
+ }
187
+ this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
188
+ } else if (data instanceof Map) {
189
+ // Type 4, subtype 7: map start
190
+ this.buffer[this.writePos++] = (4 << 5) | 7;
191
+ for (const [key, value] of data) {
192
+ this.write(key);
193
+ this.write(value);
194
+ }
195
+ this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
196
+ } else if (data instanceof Set) {
197
+ // Type 4, subtype 8: set start
198
+ this.buffer[this.writePos++] = (4 << 5) | 8;
199
+ for (const item of data) {
200
+ this.write(item);
201
+ }
202
+ this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
203
+ } else if (data instanceof Date) {
204
+ // Type 4, subtype 12: Date/Time
205
+ this.buffer[this.writePos++] = (4 << 5) | 12;
206
+ // Write a varint -- whole seconds should be plenty of resolution
207
+ this.write(Math.floor(data.getTime()/1000));
208
+ } else if (typeof data === 'object' && data.constructor === Object) {
209
+ // Type 4, subtype 6: object start
210
+ this.buffer[this.writePos++] = (4 << 5) | 6;
211
+ for (const [key, value] of Object.entries(data)) {
212
+ this.write(key);
213
+ this.write(value);
214
+ }
215
+ this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
216
+ } else {
217
+ throw new Error(`Unsupported data type: ${typeof data}`);
218
+ }
219
+ return this;
220
+ }
221
+
222
+ read(): any {
223
+ if (this.readPos > this.writePos) {
224
+ throw new Error('Not enough data');
225
+ }
226
+
227
+ const header = this.buffer[this.readPos++];
228
+ const type = (header >> 5) & 0x07;
229
+ const subtype = header & 0x1F;
230
+
231
+ switch (type) {
232
+ case 0: {
233
+ // Negative integer (byte count as bitwise NOT in lower bits)
234
+ const byteCount = (~subtype) & 0x1F;
235
+ return -this.readMultiByteNumber(byteCount, true);
236
+ }
237
+
238
+ case 1: {
239
+ // Small positive integer 0...31
240
+ return subtype;
241
+ }
242
+
243
+ case 2: {
244
+ // Small positive integer 32..63
245
+ return subtype + 32;
246
+ }
247
+
248
+ case 3: {
249
+ // Positive integer (byte count in lower bits)
250
+ return this.readMultiByteNumber(subtype) + 64;
251
+ }
252
+
253
+ case 4: {
254
+ // Special values and container starts
255
+ switch (subtype) {
256
+ case 0: {
257
+ // float64
258
+ if (this.readPos + 8 > this.writePos) this.notEnoughData('float64');
259
+ this.dataView ||= new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
260
+ const result = this.dataView.getFloat64(this.readPos);
261
+ this.readPos += 8;
262
+ return result;
263
+ }
264
+ case 1: return undefined;
265
+ case 2: return null;
266
+ case 3: return true;
267
+ case 4: return false;
268
+ case 5: {
269
+ // Array start
270
+ const result = [];
271
+ while (true) {
272
+ const nextValue = this.read();
273
+ if (nextValue === EOD) break;
274
+ result.push(nextValue);
275
+ }
276
+ return result;
277
+ }
278
+ case 6: {
279
+ // Object start
280
+ const result: any = {};
281
+ while (true) {
282
+ const key = this.read();
283
+ if (key === EOD) break;
284
+ const value = this.read();
285
+ result[key] = value;
286
+ }
287
+ return result;
288
+ }
289
+ case 7: {
290
+ // Map start
291
+ const result = new Map();
292
+ while (true) {
293
+ const key = this.read();
294
+ if (key === EOD) break;
295
+ const value = this.read();
296
+ result.set(key, value);
297
+ }
298
+ return result;
299
+ }
300
+ case 8: {
301
+ // Set start
302
+ const result = new Set();
303
+ while (true) {
304
+ const value = this.read();
305
+ if (value === EOD) break;
306
+ result.add(value);
307
+ }
308
+ return result;
309
+ }
310
+ case 9: return EOD;
311
+ case 10: {
312
+ // Identifier (6 byte positive int follows, represented as a base64 string of length 8)
313
+ --this.readPos;
314
+ return this.readIdentifier();
315
+ }
316
+ case 11: {
317
+ // Null-terminated string
318
+ const start = this.readPos;
319
+ let end = start;
320
+ while (true) {
321
+ if (end >= this.writePos) this.notEnoughData('null-terminated string');
322
+ if (this.buffer[end] === 0) break;
323
+ end++;
324
+ }
325
+ this.readPos = end + 1; // Skip the null terminator
326
+ return decoder.decode(this.buffer.subarray(start, end));
327
+ }
328
+ case 12: {
329
+ // Date/Time (varint with whole seconds since epoch follows)
330
+ const seconds = this.readPositiveInt();
331
+ return new Date(seconds * 1000);
332
+ }
333
+ default: throw new Error(`Unknown type 4 subtype: ${subtype}`);
334
+ }
335
+ }
336
+
337
+ case 5: {
338
+ // Short string (length in lower bits, 0-31)
339
+ const length = subtype;
340
+ if (this.readPos + length > this.writePos) this.notEnoughData('short string');
341
+ const bytes = this.buffer.subarray(this.readPos, this.readPos + length);
342
+ this.readPos += length;
343
+ return decoder.decode(bytes);
344
+ }
345
+
346
+ case 6: {
347
+ // String (byte count of length in lower bits)
348
+ const length = this.readMultiByteNumber(subtype);
349
+ if (this.readPos + length > this.writePos) this.notEnoughData('string');
350
+ const bytes = this.buffer.subarray(this.readPos, this.readPos + length);
351
+ this.readPos += length;
352
+ return decoder.decode(bytes);
353
+ }
354
+
355
+ case 7: {
356
+ // Blob (byte count of length in lower bits)
357
+ const length = this.readMultiByteNumber(subtype);
358
+ if (this.readPos + length > this.writePos) this.notEnoughData('blob');
359
+ // This makes an actual copy of the underlying buffer data
360
+ const result = this.buffer.slice(this.readPos, this.readPos + length);
361
+ this.readPos += length;
362
+ return new Uint8Array(result); // Return a copy
363
+ }
364
+
365
+ default: throw new Error(`Unknown type: ${type}`);
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Ensure the buffer has capacity for additional bytes.
371
+ * @param bytesNeeded - Number of additional bytes needed.
372
+ */
373
+ private ensureCapacity(bytesNeeded: number): void {
374
+ const needed = this.writePos + bytesNeeded;
375
+ if (needed <= this.buffer.length) return;
376
+
377
+ // Grow by 1.5x or the needed amount, whichever is larger
378
+ const newCapacity = Math.max(needed, Math.floor(this.buffer.length * 1.5));
379
+ const newBuffer = new Uint8Array(newCapacity);
380
+ newBuffer.set(this.buffer.subarray(0, this.writePos));
381
+ this.buffer = newBuffer;
382
+ delete this.dataView;
383
+ }
384
+
385
+ readNumber(): number {
386
+ const result = this.read();
387
+ if (typeof result !== 'number') {
388
+ throw new Error('Expected number but got ' + typeof result);
389
+ }
390
+ return result;
391
+ }
392
+
393
+ readDate(): Date {
394
+ const result = this.read();
395
+ if (!(result instanceof Date)) {
396
+ throw new Error('Expected Date but got ' + typeof result);
397
+ }
398
+ return result;
399
+ }
400
+
401
+ readPositiveInt(limit?: number): number {
402
+ const result = this.read();
403
+ if (typeof result !== 'number' || !Number.isInteger(result) || result < 0 || (limit !== undefined && result >= limit)) {
404
+ throw new Error(`Expected positive integer < ${limit} but got ${result}`);
405
+ }
406
+ return result;
407
+ }
408
+
409
+ readBoolean(): boolean {
410
+ const result = this.read();
411
+ if (typeof result !== 'boolean') {
412
+ throw new Error('Expected boolean but got ' + typeof result);
413
+ }
414
+ return result;
415
+ }
416
+
417
+ readUint8Array(): Uint8Array {
418
+ const result = this.read();
419
+ if (!(result instanceof Uint8Array)) {
420
+ throw new Error('Expected Uint8Array but got ' + typeof result);
421
+ }
422
+ return result;
423
+ }
424
+
425
+ readString(): string {
426
+ const result = this.read();
427
+ if (typeof result !== 'string') {
428
+ throw new Error('Expected string but got ' + typeof result);
429
+ }
430
+ return result;
431
+ }
432
+
433
+ writeIdentifier(id: string): DataPack {
434
+ if (id.length !== 8) {
435
+ throw new Error(`Identifier must be exactly 8 characters, got ${id.length}`);
436
+ }
437
+
438
+ // Convert base64 string to 48-bit number
439
+ let value, num = 0;
440
+ for (let i = 0; i < 8; i++) {
441
+ const char = id.charCodeAt(i);
442
+ if (char > 127 || (value = BASE64_LOOKUP[char]) === 255) {
443
+ throw new Error(`Invalid base64 character: ${id[i]}`);
444
+ }
445
+ num = num * 64 + value;
446
+ }
447
+
448
+ // Write type 4, subtype 10 header
449
+ this.ensureCapacity(7); // 1 byte header + 6 bytes for the number
450
+ this.buffer[this.writePos++] = (4 << 5) | 10;
451
+
452
+ // Write the 6-byte number in big-endian format
453
+ for (let i = 5; i >= 0; i--) {
454
+ this.buffer[this.writePos++] = Math.floor(num / Math.pow(256, i)) & 0xFF;
455
+ }
456
+
457
+ return this;
458
+ }
459
+
460
+ readIdentifier(): string {
461
+ // Read the 6-byte number in big-endian format
462
+ if (this.readPos + 7 > this.writePos) this.notEnoughData('identifier');
463
+
464
+ const header = this.buffer[this.readPos++];
465
+ if (header !== ((4 << 5) | 10)) {
466
+ throw new Error('Invalid identifier header');
467
+ }
468
+
469
+ let num = 0;
470
+ for (let i = 0; i < 6; i++) {
471
+ num = (num * 256) + this.buffer[this.readPos++];
472
+ }
473
+
474
+ // Convert 48-bit number back to 8-character base64 string
475
+ let id = '';
476
+ for (let i = 0; i < 8; i++) {
477
+ id = BASE64_CHARS[num % 64] + id;
478
+ num = Math.floor(num / 64);
479
+ }
480
+
481
+ return id;
482
+ }
483
+
484
+ /**
485
+ * Like writeString but writes without a length prefix and with a null terminator, for ordered storage.
486
+ * Can be read with {@link read} or {@link readString} just like any other string.
487
+ * @param str - The string to write. May not contain null characters.
488
+ */
489
+ writeOrderedString(str: string): DataPack {
490
+ const utf8Bytes = new TextEncoder().encode(str);
491
+ if (utf8Bytes.includes(0)) {
492
+ throw new Error('String contains null character');
493
+ }
494
+ this.ensureCapacity(utf8Bytes.length + 2);
495
+ this.buffer[this.writePos++] = (4 << 5) | 11; // Null-terminated string
496
+ this.buffer.set(utf8Bytes, this.writePos);
497
+ this.writePos += utf8Bytes.length;
498
+ this.buffer[this.writePos++] = 0; // Null terminator
499
+ return this;
500
+ }
501
+
502
+ toUint8Array(copyBuffer: boolean = true, startPos: number = 0, endPos: number = this.writePos): Uint8Array {
503
+ return copyBuffer ? this.buffer.slice(startPos, endPos) : this.buffer.subarray(startPos, endPos);
504
+ }
505
+
506
+ clone(copyBuffer: boolean, readPos: number = 0, writePos: number = this.writePos): DataPack {
507
+ if (copyBuffer) {
508
+ return new DataPack(this.buffer.slice(readPos, writePos));
509
+ } else {
510
+ const pack = new DataPack(this.buffer);
511
+ pack.readPos = readPos;
512
+ pack.writePos = writePos;
513
+ return pack;
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Write a collection (array, set, object, or map) using a callback to add items/fields.
519
+ * @param type 'array' | 'set' | 'object' | 'map'
520
+ * @param bodyFunc Callback function to add items/fields. It accepts a function to add values or key-value pairs (depending on the collection type).
521
+ * @returns The DataPack instance for chaining.
522
+ * @example
523
+ * // Writing an array
524
+ * pack.writeCollection('array', add => {
525
+ * add(1);
526
+ * add(2);
527
+ * add(3);
528
+ * });
529
+ *
530
+ * // Writing an object
531
+ * pack.writeCollection('object', (add) => {
532
+ * add('key1', 'value1');
533
+ * add('key2', 42);
534
+ * });
535
+ */
536
+ writeCollection(type: 'array' | 'set', bodyFunc: (addField: (value: any) => void) => void): DataPack;
537
+ writeCollection(type: 'object', bodyFunc: (addField: (field: number|string|symbol, value: any) => void) => void): DataPack;
538
+ writeCollection(type: 'map', bodyFunc: (addField: (field: any, value: any) => void) => void): DataPack;
539
+
540
+ writeCollection(type: string, bodyFunc: (addField: (a: any, b?: any) => void) => void): DataPack {
541
+ let subType = {'array': 5, 'object': 6, 'map': 7, 'set': 8}[type];
542
+ if (!subType) throw new Error(`Invalid collection type: ${type}`);
543
+ this.buffer[this.writePos++] = (4 << 5) | subType; // Collection start
544
+
545
+ bodyFunc((type === 'array' || type === 'set') ? (value: any) => {
546
+ this.write(value);
547
+ } : (name, value) => {
548
+ this.write(name);
549
+ this.write(value);
550
+ });
551
+ this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
552
+ return this;
553
+ }
554
+
555
+ /**
556
+ * Increment the last byte of the buffer. If it was already 255 set it to 0 and
557
+ * increment the previous byte, and so on. If all bytes were 255, return undefined.
558
+ * This is useful for creating an exclusive end key for range scans.
559
+ * This may result in a DataPack instance that cannot be parsed (or represented by
560
+ * {@link toString}).
561
+ */
562
+ increment(): DataPack | undefined {
563
+ for(let byte=this.writePos-1; byte >= 0; byte--) {
564
+ if (this.buffer[byte] === 255) {
565
+ // Byte is all 1s, set to 0 and continue
566
+ this.buffer[byte] = 0;
567
+ } else {
568
+ this.buffer[byte]++;
569
+ return this;
570
+ }
571
+ }
572
+ return undefined; // All bytes were 255 (and are now 0)
573
+ }
574
+
575
+ toString(extended: boolean | undefined = undefined, startPos: number = 0, endPos: number = this.writePos): string {
576
+ if (extended === undefined) extended = useExtendedLogging;
577
+ let oldReadPos = this.readPos;
578
+ this.readPos = startPos;
579
+
580
+ let lastPos = 0;
581
+ let vals = '';
582
+ let hexs = '';
583
+ const orgTermCount = toStringTermCount;
584
+ const resetColor = orgTermCount > 0 ? COLORS[(orgTermCount-1) % COLORS.length] : RESET_COLOR;
585
+
586
+ try {
587
+ while(this.readPos < endPos) {
588
+ const color = COLORS[toStringTermCount++ % COLORS.length];
589
+ vals += color + toText(this.read()) + ' ';
590
+ if (extended) {
591
+ hexs += color;
592
+ while(lastPos < this.readPos) {
593
+ hexs += this.buffer[lastPos].toString(16).padStart(2, '0') + ' ';
594
+ lastPos++;
595
+ }
596
+ }
597
+ }
598
+ } catch(e) {
599
+ if (!extended) {
600
+ this.readPos = oldReadPos;
601
+ toStringTermCount = orgTermCount;
602
+ return this.toString(true, startPos, endPos);
603
+ }
604
+ vals += ERROR_COLOR + "ERROR ";
605
+ hexs += ERROR_COLOR;
606
+ while(lastPos < this.writePos) {
607
+ hexs += this.buffer[lastPos].toString(16).padStart(2, '0') + ' ';
608
+ lastPos++;
609
+ }
610
+ }
611
+ this.readPos = oldReadPos;
612
+
613
+ if (!orgTermCount) toStringTermCount = 0;
614
+
615
+ if (extended) {
616
+ return "DataPack{" + hexs + resetColor + "→ " + vals.trimEnd() + resetColor + "}";
617
+ } else {
618
+ return "DataPack{" + vals.trimEnd() + resetColor + "}";
619
+ }
620
+ }
621
+
622
+ [Symbol.for('nodejs.util.inspect.custom')](): string {
623
+ return this.toString();
624
+ }
625
+
626
+ readAvailable(): boolean {
627
+ return this.readPos < this.writePos;
628
+ }
629
+
630
+ static generateIdentifier(): string {
631
+ // Combine a timestamp with randomness, to create locality of reference as well as a high chance of uniqueness.
632
+ // Bits 9...48 are the date in ms (wrapping about every 34 years), providing some `locality of reference
633
+ // Bit 0...14 are random bits (partly overlapping with the date, adding up to 62ms of jitter)
634
+ let num = Math.floor(+new Date() * (1<<8) + Math.random() * (1<<14));
635
+
636
+ let id = '';
637
+ for(let i = 0; i < 8; i++) {
638
+ id = BASE64_CHARS[num & 0x3f] + id;
639
+ num = Math.floor(num / 64);
640
+ }
641
+ return id;
642
+ }
643
+ }
644
+
645
+ function toText(v: any): string {
646
+ if (v===undefined) return 'undefined';
647
+ if (typeof v === 'object' && v !== null) {
648
+ if (v instanceof Uint8Array) return new DataPack(v).toString();
649
+ if (v instanceof Array) return '[' + v.map(vv => toText(vv)).join(', ') + ']';
650
+ if (v instanceof Map) return 'Map{' + Array.from(v.entries()).map(([k, val]) => `${toText(k)}=>${toText(val)}`).join(', ') + '}';
651
+ if (v instanceof Set) return 'Set{' + Array.from(v.values()).map(vv => toText(vv)).join(', ') + '}';
652
+ if (typeof v === 'object' && v !== null) return '{' + Object.entries(v).map(([k, val]) => `${k}:${toText(val)}`).join(', ') + '}';
653
+ }
654
+ return JSON.stringify(v);
655
+ }