playsocketjs 3.1.2 → 4.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.
@@ -1,2614 +0,0 @@
1
- function utf8Count(str) {
2
- const strLength = str.length;
3
- let byteLength = 0;
4
- let pos = 0;
5
- while (pos < strLength) {
6
- let value = str.charCodeAt(pos++);
7
- if ((value & 0xffffff80) === 0) {
8
- // 1-byte
9
- byteLength++;
10
- continue;
11
- }
12
- else if ((value & 0xfffff800) === 0) {
13
- // 2-bytes
14
- byteLength += 2;
15
- }
16
- else {
17
- // handle surrogate pair
18
- if (value >= 0xd800 && value <= 0xdbff) {
19
- // high surrogate
20
- if (pos < strLength) {
21
- const extra = str.charCodeAt(pos);
22
- if ((extra & 0xfc00) === 0xdc00) {
23
- ++pos;
24
- value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
25
- }
26
- }
27
- }
28
- if ((value & 0xffff0000) === 0) {
29
- // 3-byte
30
- byteLength += 3;
31
- }
32
- else {
33
- // 4-byte
34
- byteLength += 4;
35
- }
36
- }
37
- }
38
- return byteLength;
39
- }
40
- function utf8EncodeJs(str, output, outputOffset) {
41
- const strLength = str.length;
42
- let offset = outputOffset;
43
- let pos = 0;
44
- while (pos < strLength) {
45
- let value = str.charCodeAt(pos++);
46
- if ((value & 0xffffff80) === 0) {
47
- // 1-byte
48
- output[offset++] = value;
49
- continue;
50
- }
51
- else if ((value & 0xfffff800) === 0) {
52
- // 2-bytes
53
- output[offset++] = ((value >> 6) & 0x1f) | 0xc0;
54
- }
55
- else {
56
- // handle surrogate pair
57
- if (value >= 0xd800 && value <= 0xdbff) {
58
- // high surrogate
59
- if (pos < strLength) {
60
- const extra = str.charCodeAt(pos);
61
- if ((extra & 0xfc00) === 0xdc00) {
62
- ++pos;
63
- value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
64
- }
65
- }
66
- }
67
- if ((value & 0xffff0000) === 0) {
68
- // 3-byte
69
- output[offset++] = ((value >> 12) & 0x0f) | 0xe0;
70
- output[offset++] = ((value >> 6) & 0x3f) | 0x80;
71
- }
72
- else {
73
- // 4-byte
74
- output[offset++] = ((value >> 18) & 0x07) | 0xf0;
75
- output[offset++] = ((value >> 12) & 0x3f) | 0x80;
76
- output[offset++] = ((value >> 6) & 0x3f) | 0x80;
77
- }
78
- }
79
- output[offset++] = (value & 0x3f) | 0x80;
80
- }
81
- }
82
- // TextEncoder and TextDecoder are standardized in whatwg encoding:
83
- // https://encoding.spec.whatwg.org/
84
- // and available in all the modern browsers:
85
- // https://caniuse.com/textencoder
86
- // They are available in Node.js since v12 LTS as well:
87
- // https://nodejs.org/api/globals.html#textencoder
88
- const sharedTextEncoder = new TextEncoder();
89
- // This threshold should be determined by benchmarking, which might vary in engines and input data.
90
- // Run `npx ts-node benchmark/encode-string.ts` for details.
91
- const TEXT_ENCODER_THRESHOLD = 50;
92
- function utf8EncodeTE(str, output, outputOffset) {
93
- sharedTextEncoder.encodeInto(str, output.subarray(outputOffset));
94
- }
95
- function utf8Encode(str, output, outputOffset) {
96
- if (str.length > TEXT_ENCODER_THRESHOLD) {
97
- utf8EncodeTE(str, output, outputOffset);
98
- }
99
- else {
100
- utf8EncodeJs(str, output, outputOffset);
101
- }
102
- }
103
- const CHUNK_SIZE = 4096;
104
- function utf8DecodeJs(bytes, inputOffset, byteLength) {
105
- let offset = inputOffset;
106
- const end = offset + byteLength;
107
- const units = [];
108
- let result = "";
109
- while (offset < end) {
110
- const byte1 = bytes[offset++];
111
- if ((byte1 & 0x80) === 0) {
112
- // 1 byte
113
- units.push(byte1);
114
- }
115
- else if ((byte1 & 0xe0) === 0xc0) {
116
- // 2 bytes
117
- const byte2 = bytes[offset++] & 0x3f;
118
- units.push(((byte1 & 0x1f) << 6) | byte2);
119
- }
120
- else if ((byte1 & 0xf0) === 0xe0) {
121
- // 3 bytes
122
- const byte2 = bytes[offset++] & 0x3f;
123
- const byte3 = bytes[offset++] & 0x3f;
124
- units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3);
125
- }
126
- else if ((byte1 & 0xf8) === 0xf0) {
127
- // 4 bytes
128
- const byte2 = bytes[offset++] & 0x3f;
129
- const byte3 = bytes[offset++] & 0x3f;
130
- const byte4 = bytes[offset++] & 0x3f;
131
- let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
132
- if (unit > 0xffff) {
133
- unit -= 0x10000;
134
- units.push(((unit >>> 10) & 0x3ff) | 0xd800);
135
- unit = 0xdc00 | (unit & 0x3ff);
136
- }
137
- units.push(unit);
138
- }
139
- else {
140
- units.push(byte1);
141
- }
142
- if (units.length >= CHUNK_SIZE) {
143
- result += String.fromCharCode(...units);
144
- units.length = 0;
145
- }
146
- }
147
- if (units.length > 0) {
148
- result += String.fromCharCode(...units);
149
- }
150
- return result;
151
- }
152
- const sharedTextDecoder = new TextDecoder();
153
- // This threshold should be determined by benchmarking, which might vary in engines and input data.
154
- // Run `npx ts-node benchmark/decode-string.ts` for details.
155
- const TEXT_DECODER_THRESHOLD = 200;
156
- function utf8DecodeTD(bytes, inputOffset, byteLength) {
157
- const stringBytes = bytes.subarray(inputOffset, inputOffset + byteLength);
158
- return sharedTextDecoder.decode(stringBytes);
159
- }
160
- function utf8Decode(bytes, inputOffset, byteLength) {
161
- if (byteLength > TEXT_DECODER_THRESHOLD) {
162
- return utf8DecodeTD(bytes, inputOffset, byteLength);
163
- }
164
- else {
165
- return utf8DecodeJs(bytes, inputOffset, byteLength);
166
- }
167
- }
168
-
169
- /**
170
- * ExtData is used to handle Extension Types that are not registered to ExtensionCodec.
171
- */
172
- class ExtData {
173
- type;
174
- data;
175
- constructor(type, data) {
176
- this.type = type;
177
- this.data = data;
178
- }
179
- }
180
-
181
- class DecodeError extends Error {
182
- constructor(message) {
183
- super(message);
184
- // fix the prototype chain in a cross-platform way
185
- const proto = Object.create(DecodeError.prototype);
186
- Object.setPrototypeOf(this, proto);
187
- Object.defineProperty(this, "name", {
188
- configurable: true,
189
- enumerable: false,
190
- value: DecodeError.name,
191
- });
192
- }
193
- }
194
-
195
- // Integer Utility
196
- const UINT32_MAX = 4294967295;
197
- // DataView extension to handle int64 / uint64,
198
- // where the actual range is 53-bits integer (a.k.a. safe integer)
199
- function setUint64(view, offset, value) {
200
- const high = value / 4294967296;
201
- const low = value; // high bits are truncated by DataView
202
- view.setUint32(offset, high);
203
- view.setUint32(offset + 4, low);
204
- }
205
- function setInt64(view, offset, value) {
206
- const high = Math.floor(value / 4294967296);
207
- const low = value; // high bits are truncated by DataView
208
- view.setUint32(offset, high);
209
- view.setUint32(offset + 4, low);
210
- }
211
- function getInt64(view, offset) {
212
- const high = view.getInt32(offset);
213
- const low = view.getUint32(offset + 4);
214
- return high * 4294967296 + low;
215
- }
216
- function getUint64(view, offset) {
217
- const high = view.getUint32(offset);
218
- const low = view.getUint32(offset + 4);
219
- return high * 4294967296 + low;
220
- }
221
-
222
- // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
223
- const EXT_TIMESTAMP = -1;
224
- const TIMESTAMP32_MAX_SEC = 0x100000000 - 1; // 32-bit unsigned int
225
- const TIMESTAMP64_MAX_SEC = 0x400000000 - 1; // 34-bit unsigned int
226
- function encodeTimeSpecToTimestamp({ sec, nsec }) {
227
- if (sec >= 0 && nsec >= 0 && sec <= TIMESTAMP64_MAX_SEC) {
228
- // Here sec >= 0 && nsec >= 0
229
- if (nsec === 0 && sec <= TIMESTAMP32_MAX_SEC) {
230
- // timestamp 32 = { sec32 (unsigned) }
231
- const rv = new Uint8Array(4);
232
- const view = new DataView(rv.buffer);
233
- view.setUint32(0, sec);
234
- return rv;
235
- }
236
- else {
237
- // timestamp 64 = { nsec30 (unsigned), sec34 (unsigned) }
238
- const secHigh = sec / 0x100000000;
239
- const secLow = sec & 0xffffffff;
240
- const rv = new Uint8Array(8);
241
- const view = new DataView(rv.buffer);
242
- // nsec30 | secHigh2
243
- view.setUint32(0, (nsec << 2) | (secHigh & 0x3));
244
- // secLow32
245
- view.setUint32(4, secLow);
246
- return rv;
247
- }
248
- }
249
- else {
250
- // timestamp 96 = { nsec32 (unsigned), sec64 (signed) }
251
- const rv = new Uint8Array(12);
252
- const view = new DataView(rv.buffer);
253
- view.setUint32(0, nsec);
254
- setInt64(view, 4, sec);
255
- return rv;
256
- }
257
- }
258
- function encodeDateToTimeSpec(date) {
259
- const msec = date.getTime();
260
- const sec = Math.floor(msec / 1e3);
261
- const nsec = (msec - sec * 1e3) * 1e6;
262
- // Normalizes { sec, nsec } to ensure nsec is unsigned.
263
- const nsecInSec = Math.floor(nsec / 1e9);
264
- return {
265
- sec: sec + nsecInSec,
266
- nsec: nsec - nsecInSec * 1e9,
267
- };
268
- }
269
- function encodeTimestampExtension(object) {
270
- if (object instanceof Date) {
271
- const timeSpec = encodeDateToTimeSpec(object);
272
- return encodeTimeSpecToTimestamp(timeSpec);
273
- }
274
- else {
275
- return null;
276
- }
277
- }
278
- function decodeTimestampToTimeSpec(data) {
279
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
280
- // data may be 32, 64, or 96 bits
281
- switch (data.byteLength) {
282
- case 4: {
283
- // timestamp 32 = { sec32 }
284
- const sec = view.getUint32(0);
285
- const nsec = 0;
286
- return { sec, nsec };
287
- }
288
- case 8: {
289
- // timestamp 64 = { nsec30, sec34 }
290
- const nsec30AndSecHigh2 = view.getUint32(0);
291
- const secLow32 = view.getUint32(4);
292
- const sec = (nsec30AndSecHigh2 & 0x3) * 0x100000000 + secLow32;
293
- const nsec = nsec30AndSecHigh2 >>> 2;
294
- return { sec, nsec };
295
- }
296
- case 12: {
297
- // timestamp 96 = { nsec32 (unsigned), sec64 (signed) }
298
- const sec = getInt64(view, 4);
299
- const nsec = view.getUint32(0);
300
- return { sec, nsec };
301
- }
302
- default:
303
- throw new DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${data.length}`);
304
- }
305
- }
306
- function decodeTimestampExtension(data) {
307
- const timeSpec = decodeTimestampToTimeSpec(data);
308
- return new Date(timeSpec.sec * 1e3 + timeSpec.nsec / 1e6);
309
- }
310
- const timestampExtension = {
311
- type: EXT_TIMESTAMP,
312
- encode: encodeTimestampExtension,
313
- decode: decodeTimestampExtension,
314
- };
315
-
316
- // ExtensionCodec to handle MessagePack extensions
317
- class ExtensionCodec {
318
- static defaultCodec = new ExtensionCodec();
319
- // ensures ExtensionCodecType<X> matches ExtensionCodec<X>
320
- // this will make type errors a lot more clear
321
- // eslint-disable-next-line @typescript-eslint/naming-convention
322
- __brand;
323
- // built-in extensions
324
- builtInEncoders = [];
325
- builtInDecoders = [];
326
- // custom extensions
327
- encoders = [];
328
- decoders = [];
329
- constructor() {
330
- this.register(timestampExtension);
331
- }
332
- register({ type, encode, decode, }) {
333
- if (type >= 0) {
334
- // custom extensions
335
- this.encoders[type] = encode;
336
- this.decoders[type] = decode;
337
- }
338
- else {
339
- // built-in extensions
340
- const index = -1 - type;
341
- this.builtInEncoders[index] = encode;
342
- this.builtInDecoders[index] = decode;
343
- }
344
- }
345
- tryToEncode(object, context) {
346
- // built-in extensions
347
- for (let i = 0; i < this.builtInEncoders.length; i++) {
348
- const encodeExt = this.builtInEncoders[i];
349
- if (encodeExt != null) {
350
- const data = encodeExt(object, context);
351
- if (data != null) {
352
- const type = -1 - i;
353
- return new ExtData(type, data);
354
- }
355
- }
356
- }
357
- // custom extensions
358
- for (let i = 0; i < this.encoders.length; i++) {
359
- const encodeExt = this.encoders[i];
360
- if (encodeExt != null) {
361
- const data = encodeExt(object, context);
362
- if (data != null) {
363
- const type = i;
364
- return new ExtData(type, data);
365
- }
366
- }
367
- }
368
- if (object instanceof ExtData) {
369
- // to keep ExtData as is
370
- return object;
371
- }
372
- return null;
373
- }
374
- decode(data, type, context) {
375
- const decodeExt = type < 0 ? this.builtInDecoders[-1 - type] : this.decoders[type];
376
- if (decodeExt) {
377
- return decodeExt(data, type, context);
378
- }
379
- else {
380
- // decode() does not fail, returns ExtData instead.
381
- return new ExtData(type, data);
382
- }
383
- }
384
- }
385
-
386
- function isArrayBufferLike(buffer) {
387
- return (buffer instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer));
388
- }
389
- function ensureUint8Array(buffer) {
390
- if (buffer instanceof Uint8Array) {
391
- return buffer;
392
- }
393
- else if (ArrayBuffer.isView(buffer)) {
394
- return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
395
- }
396
- else if (isArrayBufferLike(buffer)) {
397
- return new Uint8Array(buffer);
398
- }
399
- else {
400
- // ArrayLike<number>
401
- return Uint8Array.from(buffer);
402
- }
403
- }
404
-
405
- const DEFAULT_MAX_DEPTH = 100;
406
- const DEFAULT_INITIAL_BUFFER_SIZE = 2048;
407
- class Encoder {
408
- extensionCodec;
409
- context;
410
- useBigInt64;
411
- maxDepth;
412
- initialBufferSize;
413
- sortKeys;
414
- forceFloat32;
415
- ignoreUndefined;
416
- forceIntegerToFloat;
417
- pos;
418
- view;
419
- bytes;
420
- entered = false;
421
- constructor(options) {
422
- this.extensionCodec = options?.extensionCodec ?? ExtensionCodec.defaultCodec;
423
- this.context = options?.context; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
424
- this.useBigInt64 = options?.useBigInt64 ?? false;
425
- this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
426
- this.initialBufferSize = options?.initialBufferSize ?? DEFAULT_INITIAL_BUFFER_SIZE;
427
- this.sortKeys = options?.sortKeys ?? false;
428
- this.forceFloat32 = options?.forceFloat32 ?? false;
429
- this.ignoreUndefined = options?.ignoreUndefined ?? false;
430
- this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false;
431
- this.pos = 0;
432
- this.view = new DataView(new ArrayBuffer(this.initialBufferSize));
433
- this.bytes = new Uint8Array(this.view.buffer);
434
- }
435
- clone() {
436
- // Because of slightly special argument `context`,
437
- // type assertion is needed.
438
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
439
- return new Encoder({
440
- extensionCodec: this.extensionCodec,
441
- context: this.context,
442
- useBigInt64: this.useBigInt64,
443
- maxDepth: this.maxDepth,
444
- initialBufferSize: this.initialBufferSize,
445
- sortKeys: this.sortKeys,
446
- forceFloat32: this.forceFloat32,
447
- ignoreUndefined: this.ignoreUndefined,
448
- forceIntegerToFloat: this.forceIntegerToFloat,
449
- });
450
- }
451
- reinitializeState() {
452
- this.pos = 0;
453
- }
454
- /**
455
- * This is almost equivalent to {@link Encoder#encode}, but it returns an reference of the encoder's internal buffer and thus much faster than {@link Encoder#encode}.
456
- *
457
- * @returns Encodes the object and returns a shared reference the encoder's internal buffer.
458
- */
459
- encodeSharedRef(object) {
460
- if (this.entered) {
461
- const instance = this.clone();
462
- return instance.encodeSharedRef(object);
463
- }
464
- try {
465
- this.entered = true;
466
- this.reinitializeState();
467
- this.doEncode(object, 1);
468
- return this.bytes.subarray(0, this.pos);
469
- }
470
- finally {
471
- this.entered = false;
472
- }
473
- }
474
- /**
475
- * @returns Encodes the object and returns a copy of the encoder's internal buffer.
476
- */
477
- encode(object) {
478
- if (this.entered) {
479
- const instance = this.clone();
480
- return instance.encode(object);
481
- }
482
- try {
483
- this.entered = true;
484
- this.reinitializeState();
485
- this.doEncode(object, 1);
486
- return this.bytes.slice(0, this.pos);
487
- }
488
- finally {
489
- this.entered = false;
490
- }
491
- }
492
- doEncode(object, depth) {
493
- if (depth > this.maxDepth) {
494
- throw new Error(`Too deep objects in depth ${depth}`);
495
- }
496
- if (object == null) {
497
- this.encodeNil();
498
- }
499
- else if (typeof object === "boolean") {
500
- this.encodeBoolean(object);
501
- }
502
- else if (typeof object === "number") {
503
- if (!this.forceIntegerToFloat) {
504
- this.encodeNumber(object);
505
- }
506
- else {
507
- this.encodeNumberAsFloat(object);
508
- }
509
- }
510
- else if (typeof object === "string") {
511
- this.encodeString(object);
512
- }
513
- else if (this.useBigInt64 && typeof object === "bigint") {
514
- this.encodeBigInt64(object);
515
- }
516
- else {
517
- this.encodeObject(object, depth);
518
- }
519
- }
520
- ensureBufferSizeToWrite(sizeToWrite) {
521
- const requiredSize = this.pos + sizeToWrite;
522
- if (this.view.byteLength < requiredSize) {
523
- this.resizeBuffer(requiredSize * 2);
524
- }
525
- }
526
- resizeBuffer(newSize) {
527
- const newBuffer = new ArrayBuffer(newSize);
528
- const newBytes = new Uint8Array(newBuffer);
529
- const newView = new DataView(newBuffer);
530
- newBytes.set(this.bytes);
531
- this.view = newView;
532
- this.bytes = newBytes;
533
- }
534
- encodeNil() {
535
- this.writeU8(0xc0);
536
- }
537
- encodeBoolean(object) {
538
- if (object === false) {
539
- this.writeU8(0xc2);
540
- }
541
- else {
542
- this.writeU8(0xc3);
543
- }
544
- }
545
- encodeNumber(object) {
546
- if (!this.forceIntegerToFloat && Number.isSafeInteger(object)) {
547
- if (object >= 0) {
548
- if (object < 0x80) {
549
- // positive fixint
550
- this.writeU8(object);
551
- }
552
- else if (object < 0x100) {
553
- // uint 8
554
- this.writeU8(0xcc);
555
- this.writeU8(object);
556
- }
557
- else if (object < 0x10000) {
558
- // uint 16
559
- this.writeU8(0xcd);
560
- this.writeU16(object);
561
- }
562
- else if (object < 0x100000000) {
563
- // uint 32
564
- this.writeU8(0xce);
565
- this.writeU32(object);
566
- }
567
- else if (!this.useBigInt64) {
568
- // uint 64
569
- this.writeU8(0xcf);
570
- this.writeU64(object);
571
- }
572
- else {
573
- this.encodeNumberAsFloat(object);
574
- }
575
- }
576
- else {
577
- if (object >= -32) {
578
- // negative fixint
579
- this.writeU8(0xe0 | (object + 0x20));
580
- }
581
- else if (object >= -128) {
582
- // int 8
583
- this.writeU8(0xd0);
584
- this.writeI8(object);
585
- }
586
- else if (object >= -32768) {
587
- // int 16
588
- this.writeU8(0xd1);
589
- this.writeI16(object);
590
- }
591
- else if (object >= -2147483648) {
592
- // int 32
593
- this.writeU8(0xd2);
594
- this.writeI32(object);
595
- }
596
- else if (!this.useBigInt64) {
597
- // int 64
598
- this.writeU8(0xd3);
599
- this.writeI64(object);
600
- }
601
- else {
602
- this.encodeNumberAsFloat(object);
603
- }
604
- }
605
- }
606
- else {
607
- this.encodeNumberAsFloat(object);
608
- }
609
- }
610
- encodeNumberAsFloat(object) {
611
- if (this.forceFloat32) {
612
- // float 32
613
- this.writeU8(0xca);
614
- this.writeF32(object);
615
- }
616
- else {
617
- // float 64
618
- this.writeU8(0xcb);
619
- this.writeF64(object);
620
- }
621
- }
622
- encodeBigInt64(object) {
623
- if (object >= BigInt(0)) {
624
- // uint 64
625
- this.writeU8(0xcf);
626
- this.writeBigUint64(object);
627
- }
628
- else {
629
- // int 64
630
- this.writeU8(0xd3);
631
- this.writeBigInt64(object);
632
- }
633
- }
634
- writeStringHeader(byteLength) {
635
- if (byteLength < 32) {
636
- // fixstr
637
- this.writeU8(0xa0 + byteLength);
638
- }
639
- else if (byteLength < 0x100) {
640
- // str 8
641
- this.writeU8(0xd9);
642
- this.writeU8(byteLength);
643
- }
644
- else if (byteLength < 0x10000) {
645
- // str 16
646
- this.writeU8(0xda);
647
- this.writeU16(byteLength);
648
- }
649
- else if (byteLength < 0x100000000) {
650
- // str 32
651
- this.writeU8(0xdb);
652
- this.writeU32(byteLength);
653
- }
654
- else {
655
- throw new Error(`Too long string: ${byteLength} bytes in UTF-8`);
656
- }
657
- }
658
- encodeString(object) {
659
- const maxHeaderSize = 1 + 4;
660
- const byteLength = utf8Count(object);
661
- this.ensureBufferSizeToWrite(maxHeaderSize + byteLength);
662
- this.writeStringHeader(byteLength);
663
- utf8Encode(object, this.bytes, this.pos);
664
- this.pos += byteLength;
665
- }
666
- encodeObject(object, depth) {
667
- // try to encode objects with custom codec first of non-primitives
668
- const ext = this.extensionCodec.tryToEncode(object, this.context);
669
- if (ext != null) {
670
- this.encodeExtension(ext);
671
- }
672
- else if (Array.isArray(object)) {
673
- this.encodeArray(object, depth);
674
- }
675
- else if (ArrayBuffer.isView(object)) {
676
- this.encodeBinary(object);
677
- }
678
- else if (typeof object === "object") {
679
- this.encodeMap(object, depth);
680
- }
681
- else {
682
- // symbol, function and other special object come here unless extensionCodec handles them.
683
- throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(object)}`);
684
- }
685
- }
686
- encodeBinary(object) {
687
- const size = object.byteLength;
688
- if (size < 0x100) {
689
- // bin 8
690
- this.writeU8(0xc4);
691
- this.writeU8(size);
692
- }
693
- else if (size < 0x10000) {
694
- // bin 16
695
- this.writeU8(0xc5);
696
- this.writeU16(size);
697
- }
698
- else if (size < 0x100000000) {
699
- // bin 32
700
- this.writeU8(0xc6);
701
- this.writeU32(size);
702
- }
703
- else {
704
- throw new Error(`Too large binary: ${size}`);
705
- }
706
- const bytes = ensureUint8Array(object);
707
- this.writeU8a(bytes);
708
- }
709
- encodeArray(object, depth) {
710
- const size = object.length;
711
- if (size < 16) {
712
- // fixarray
713
- this.writeU8(0x90 + size);
714
- }
715
- else if (size < 0x10000) {
716
- // array 16
717
- this.writeU8(0xdc);
718
- this.writeU16(size);
719
- }
720
- else if (size < 0x100000000) {
721
- // array 32
722
- this.writeU8(0xdd);
723
- this.writeU32(size);
724
- }
725
- else {
726
- throw new Error(`Too large array: ${size}`);
727
- }
728
- for (const item of object) {
729
- this.doEncode(item, depth + 1);
730
- }
731
- }
732
- countWithoutUndefined(object, keys) {
733
- let count = 0;
734
- for (const key of keys) {
735
- if (object[key] !== undefined) {
736
- count++;
737
- }
738
- }
739
- return count;
740
- }
741
- encodeMap(object, depth) {
742
- const keys = Object.keys(object);
743
- if (this.sortKeys) {
744
- keys.sort();
745
- }
746
- const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length;
747
- if (size < 16) {
748
- // fixmap
749
- this.writeU8(0x80 + size);
750
- }
751
- else if (size < 0x10000) {
752
- // map 16
753
- this.writeU8(0xde);
754
- this.writeU16(size);
755
- }
756
- else if (size < 0x100000000) {
757
- // map 32
758
- this.writeU8(0xdf);
759
- this.writeU32(size);
760
- }
761
- else {
762
- throw new Error(`Too large map object: ${size}`);
763
- }
764
- for (const key of keys) {
765
- const value = object[key];
766
- if (!(this.ignoreUndefined && value === undefined)) {
767
- this.encodeString(key);
768
- this.doEncode(value, depth + 1);
769
- }
770
- }
771
- }
772
- encodeExtension(ext) {
773
- if (typeof ext.data === "function") {
774
- const data = ext.data(this.pos + 6);
775
- const size = data.length;
776
- if (size >= 0x100000000) {
777
- throw new Error(`Too large extension object: ${size}`);
778
- }
779
- this.writeU8(0xc9);
780
- this.writeU32(size);
781
- this.writeI8(ext.type);
782
- this.writeU8a(data);
783
- return;
784
- }
785
- const size = ext.data.length;
786
- if (size === 1) {
787
- // fixext 1
788
- this.writeU8(0xd4);
789
- }
790
- else if (size === 2) {
791
- // fixext 2
792
- this.writeU8(0xd5);
793
- }
794
- else if (size === 4) {
795
- // fixext 4
796
- this.writeU8(0xd6);
797
- }
798
- else if (size === 8) {
799
- // fixext 8
800
- this.writeU8(0xd7);
801
- }
802
- else if (size === 16) {
803
- // fixext 16
804
- this.writeU8(0xd8);
805
- }
806
- else if (size < 0x100) {
807
- // ext 8
808
- this.writeU8(0xc7);
809
- this.writeU8(size);
810
- }
811
- else if (size < 0x10000) {
812
- // ext 16
813
- this.writeU8(0xc8);
814
- this.writeU16(size);
815
- }
816
- else if (size < 0x100000000) {
817
- // ext 32
818
- this.writeU8(0xc9);
819
- this.writeU32(size);
820
- }
821
- else {
822
- throw new Error(`Too large extension object: ${size}`);
823
- }
824
- this.writeI8(ext.type);
825
- this.writeU8a(ext.data);
826
- }
827
- writeU8(value) {
828
- this.ensureBufferSizeToWrite(1);
829
- this.view.setUint8(this.pos, value);
830
- this.pos++;
831
- }
832
- writeU8a(values) {
833
- const size = values.length;
834
- this.ensureBufferSizeToWrite(size);
835
- this.bytes.set(values, this.pos);
836
- this.pos += size;
837
- }
838
- writeI8(value) {
839
- this.ensureBufferSizeToWrite(1);
840
- this.view.setInt8(this.pos, value);
841
- this.pos++;
842
- }
843
- writeU16(value) {
844
- this.ensureBufferSizeToWrite(2);
845
- this.view.setUint16(this.pos, value);
846
- this.pos += 2;
847
- }
848
- writeI16(value) {
849
- this.ensureBufferSizeToWrite(2);
850
- this.view.setInt16(this.pos, value);
851
- this.pos += 2;
852
- }
853
- writeU32(value) {
854
- this.ensureBufferSizeToWrite(4);
855
- this.view.setUint32(this.pos, value);
856
- this.pos += 4;
857
- }
858
- writeI32(value) {
859
- this.ensureBufferSizeToWrite(4);
860
- this.view.setInt32(this.pos, value);
861
- this.pos += 4;
862
- }
863
- writeF32(value) {
864
- this.ensureBufferSizeToWrite(4);
865
- this.view.setFloat32(this.pos, value);
866
- this.pos += 4;
867
- }
868
- writeF64(value) {
869
- this.ensureBufferSizeToWrite(8);
870
- this.view.setFloat64(this.pos, value);
871
- this.pos += 8;
872
- }
873
- writeU64(value) {
874
- this.ensureBufferSizeToWrite(8);
875
- setUint64(this.view, this.pos, value);
876
- this.pos += 8;
877
- }
878
- writeI64(value) {
879
- this.ensureBufferSizeToWrite(8);
880
- setInt64(this.view, this.pos, value);
881
- this.pos += 8;
882
- }
883
- writeBigUint64(value) {
884
- this.ensureBufferSizeToWrite(8);
885
- this.view.setBigUint64(this.pos, value);
886
- this.pos += 8;
887
- }
888
- writeBigInt64(value) {
889
- this.ensureBufferSizeToWrite(8);
890
- this.view.setBigInt64(this.pos, value);
891
- this.pos += 8;
892
- }
893
- }
894
-
895
- /**
896
- * It encodes `value` in the MessagePack format and
897
- * returns a byte buffer.
898
- *
899
- * The returned buffer is a slice of a larger `ArrayBuffer`, so you have to use its `#byteOffset` and `#byteLength` in order to convert it to another typed arrays including NodeJS `Buffer`.
900
- */
901
- function encode(value, options) {
902
- const encoder = new Encoder(options);
903
- return encoder.encodeSharedRef(value);
904
- }
905
-
906
- function prettyByte(byte) {
907
- return `${byte < 0 ? "-" : ""}0x${Math.abs(byte).toString(16).padStart(2, "0")}`;
908
- }
909
-
910
- const DEFAULT_MAX_KEY_LENGTH = 16;
911
- const DEFAULT_MAX_LENGTH_PER_KEY = 16;
912
- class CachedKeyDecoder {
913
- hit = 0;
914
- miss = 0;
915
- caches;
916
- maxKeyLength;
917
- maxLengthPerKey;
918
- constructor(maxKeyLength = DEFAULT_MAX_KEY_LENGTH, maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY) {
919
- this.maxKeyLength = maxKeyLength;
920
- this.maxLengthPerKey = maxLengthPerKey;
921
- // avoid `new Array(N)`, which makes a sparse array,
922
- // because a sparse array is typically slower than a non-sparse array.
923
- this.caches = [];
924
- for (let i = 0; i < this.maxKeyLength; i++) {
925
- this.caches.push([]);
926
- }
927
- }
928
- canBeCached(byteLength) {
929
- return byteLength > 0 && byteLength <= this.maxKeyLength;
930
- }
931
- find(bytes, inputOffset, byteLength) {
932
- const records = this.caches[byteLength - 1];
933
- FIND_CHUNK: for (const record of records) {
934
- const recordBytes = record.bytes;
935
- for (let j = 0; j < byteLength; j++) {
936
- if (recordBytes[j] !== bytes[inputOffset + j]) {
937
- continue FIND_CHUNK;
938
- }
939
- }
940
- return record.str;
941
- }
942
- return null;
943
- }
944
- store(bytes, value) {
945
- const records = this.caches[bytes.length - 1];
946
- const record = { bytes, str: value };
947
- if (records.length >= this.maxLengthPerKey) {
948
- // `records` are full!
949
- // Set `record` to an arbitrary position.
950
- records[(Math.random() * records.length) | 0] = record;
951
- }
952
- else {
953
- records.push(record);
954
- }
955
- }
956
- decode(bytes, inputOffset, byteLength) {
957
- const cachedValue = this.find(bytes, inputOffset, byteLength);
958
- if (cachedValue != null) {
959
- this.hit++;
960
- return cachedValue;
961
- }
962
- this.miss++;
963
- const str = utf8DecodeJs(bytes, inputOffset, byteLength);
964
- // Ensure to copy a slice of bytes because the bytes may be a NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer.
965
- const slicedCopyOfBytes = Uint8Array.prototype.slice.call(bytes, inputOffset, inputOffset + byteLength);
966
- this.store(slicedCopyOfBytes, str);
967
- return str;
968
- }
969
- }
970
-
971
- const STATE_ARRAY = "array";
972
- const STATE_MAP_KEY = "map_key";
973
- const STATE_MAP_VALUE = "map_value";
974
- const mapKeyConverter = (key) => {
975
- if (typeof key === "string" || typeof key === "number") {
976
- return key;
977
- }
978
- throw new DecodeError("The type of key must be string or number but " + typeof key);
979
- };
980
- class StackPool {
981
- stack = [];
982
- stackHeadPosition = -1;
983
- get length() {
984
- return this.stackHeadPosition + 1;
985
- }
986
- top() {
987
- return this.stack[this.stackHeadPosition];
988
- }
989
- pushArrayState(size) {
990
- const state = this.getUninitializedStateFromPool();
991
- state.type = STATE_ARRAY;
992
- state.position = 0;
993
- state.size = size;
994
- state.array = new Array(size);
995
- }
996
- pushMapState(size) {
997
- const state = this.getUninitializedStateFromPool();
998
- state.type = STATE_MAP_KEY;
999
- state.readCount = 0;
1000
- state.size = size;
1001
- state.map = {};
1002
- }
1003
- getUninitializedStateFromPool() {
1004
- this.stackHeadPosition++;
1005
- if (this.stackHeadPosition === this.stack.length) {
1006
- const partialState = {
1007
- type: undefined,
1008
- size: 0,
1009
- array: undefined,
1010
- position: 0,
1011
- readCount: 0,
1012
- map: undefined,
1013
- key: null,
1014
- };
1015
- this.stack.push(partialState);
1016
- }
1017
- return this.stack[this.stackHeadPosition];
1018
- }
1019
- release(state) {
1020
- const topStackState = this.stack[this.stackHeadPosition];
1021
- if (topStackState !== state) {
1022
- throw new Error("Invalid stack state. Released state is not on top of the stack.");
1023
- }
1024
- if (state.type === STATE_ARRAY) {
1025
- const partialState = state;
1026
- partialState.size = 0;
1027
- partialState.array = undefined;
1028
- partialState.position = 0;
1029
- partialState.type = undefined;
1030
- }
1031
- if (state.type === STATE_MAP_KEY || state.type === STATE_MAP_VALUE) {
1032
- const partialState = state;
1033
- partialState.size = 0;
1034
- partialState.map = undefined;
1035
- partialState.readCount = 0;
1036
- partialState.type = undefined;
1037
- }
1038
- this.stackHeadPosition--;
1039
- }
1040
- reset() {
1041
- this.stack.length = 0;
1042
- this.stackHeadPosition = -1;
1043
- }
1044
- }
1045
- const HEAD_BYTE_REQUIRED = -1;
1046
- const EMPTY_VIEW = new DataView(new ArrayBuffer(0));
1047
- const EMPTY_BYTES = new Uint8Array(EMPTY_VIEW.buffer);
1048
- try {
1049
- // IE11: The spec says it should throw RangeError,
1050
- // IE11: but in IE11 it throws TypeError.
1051
- EMPTY_VIEW.getInt8(0);
1052
- }
1053
- catch (e) {
1054
- if (!(e instanceof RangeError)) {
1055
- throw new Error("This module is not supported in the current JavaScript engine because DataView does not throw RangeError on out-of-bounds access");
1056
- }
1057
- }
1058
- const MORE_DATA = new RangeError("Insufficient data");
1059
- const sharedCachedKeyDecoder = new CachedKeyDecoder();
1060
- class Decoder {
1061
- extensionCodec;
1062
- context;
1063
- useBigInt64;
1064
- rawStrings;
1065
- maxStrLength;
1066
- maxBinLength;
1067
- maxArrayLength;
1068
- maxMapLength;
1069
- maxExtLength;
1070
- keyDecoder;
1071
- mapKeyConverter;
1072
- totalPos = 0;
1073
- pos = 0;
1074
- view = EMPTY_VIEW;
1075
- bytes = EMPTY_BYTES;
1076
- headByte = HEAD_BYTE_REQUIRED;
1077
- stack = new StackPool();
1078
- entered = false;
1079
- constructor(options) {
1080
- this.extensionCodec = options?.extensionCodec ?? ExtensionCodec.defaultCodec;
1081
- this.context = options?.context; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
1082
- this.useBigInt64 = options?.useBigInt64 ?? false;
1083
- this.rawStrings = options?.rawStrings ?? false;
1084
- this.maxStrLength = options?.maxStrLength ?? UINT32_MAX;
1085
- this.maxBinLength = options?.maxBinLength ?? UINT32_MAX;
1086
- this.maxArrayLength = options?.maxArrayLength ?? UINT32_MAX;
1087
- this.maxMapLength = options?.maxMapLength ?? UINT32_MAX;
1088
- this.maxExtLength = options?.maxExtLength ?? UINT32_MAX;
1089
- this.keyDecoder = options?.keyDecoder !== undefined ? options.keyDecoder : sharedCachedKeyDecoder;
1090
- this.mapKeyConverter = options?.mapKeyConverter ?? mapKeyConverter;
1091
- }
1092
- clone() {
1093
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
1094
- return new Decoder({
1095
- extensionCodec: this.extensionCodec,
1096
- context: this.context,
1097
- useBigInt64: this.useBigInt64,
1098
- rawStrings: this.rawStrings,
1099
- maxStrLength: this.maxStrLength,
1100
- maxBinLength: this.maxBinLength,
1101
- maxArrayLength: this.maxArrayLength,
1102
- maxMapLength: this.maxMapLength,
1103
- maxExtLength: this.maxExtLength,
1104
- keyDecoder: this.keyDecoder,
1105
- });
1106
- }
1107
- reinitializeState() {
1108
- this.totalPos = 0;
1109
- this.headByte = HEAD_BYTE_REQUIRED;
1110
- this.stack.reset();
1111
- // view, bytes, and pos will be re-initialized in setBuffer()
1112
- }
1113
- setBuffer(buffer) {
1114
- const bytes = ensureUint8Array(buffer);
1115
- this.bytes = bytes;
1116
- this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1117
- this.pos = 0;
1118
- }
1119
- appendBuffer(buffer) {
1120
- if (this.headByte === HEAD_BYTE_REQUIRED && !this.hasRemaining(1)) {
1121
- this.setBuffer(buffer);
1122
- }
1123
- else {
1124
- const remainingData = this.bytes.subarray(this.pos);
1125
- const newData = ensureUint8Array(buffer);
1126
- // concat remainingData + newData
1127
- const newBuffer = new Uint8Array(remainingData.length + newData.length);
1128
- newBuffer.set(remainingData);
1129
- newBuffer.set(newData, remainingData.length);
1130
- this.setBuffer(newBuffer);
1131
- }
1132
- }
1133
- hasRemaining(size) {
1134
- return this.view.byteLength - this.pos >= size;
1135
- }
1136
- createExtraByteError(posToShow) {
1137
- const { view, pos } = this;
1138
- return new RangeError(`Extra ${view.byteLength - pos} of ${view.byteLength} byte(s) found at buffer[${posToShow}]`);
1139
- }
1140
- /**
1141
- * @throws {@link DecodeError}
1142
- * @throws {@link RangeError}
1143
- */
1144
- decode(buffer) {
1145
- if (this.entered) {
1146
- const instance = this.clone();
1147
- return instance.decode(buffer);
1148
- }
1149
- try {
1150
- this.entered = true;
1151
- this.reinitializeState();
1152
- this.setBuffer(buffer);
1153
- const object = this.doDecodeSync();
1154
- if (this.hasRemaining(1)) {
1155
- throw this.createExtraByteError(this.pos);
1156
- }
1157
- return object;
1158
- }
1159
- finally {
1160
- this.entered = false;
1161
- }
1162
- }
1163
- *decodeMulti(buffer) {
1164
- if (this.entered) {
1165
- const instance = this.clone();
1166
- yield* instance.decodeMulti(buffer);
1167
- return;
1168
- }
1169
- try {
1170
- this.entered = true;
1171
- this.reinitializeState();
1172
- this.setBuffer(buffer);
1173
- while (this.hasRemaining(1)) {
1174
- yield this.doDecodeSync();
1175
- }
1176
- }
1177
- finally {
1178
- this.entered = false;
1179
- }
1180
- }
1181
- async decodeAsync(stream) {
1182
- if (this.entered) {
1183
- const instance = this.clone();
1184
- return instance.decodeAsync(stream);
1185
- }
1186
- try {
1187
- this.entered = true;
1188
- let decoded = false;
1189
- let object;
1190
- for await (const buffer of stream) {
1191
- if (decoded) {
1192
- this.entered = false;
1193
- throw this.createExtraByteError(this.totalPos);
1194
- }
1195
- this.appendBuffer(buffer);
1196
- try {
1197
- object = this.doDecodeSync();
1198
- decoded = true;
1199
- }
1200
- catch (e) {
1201
- if (!(e instanceof RangeError)) {
1202
- throw e; // rethrow
1203
- }
1204
- // fallthrough
1205
- }
1206
- this.totalPos += this.pos;
1207
- }
1208
- if (decoded) {
1209
- if (this.hasRemaining(1)) {
1210
- throw this.createExtraByteError(this.totalPos);
1211
- }
1212
- return object;
1213
- }
1214
- const { headByte, pos, totalPos } = this;
1215
- throw new RangeError(`Insufficient data in parsing ${prettyByte(headByte)} at ${totalPos} (${pos} in the current buffer)`);
1216
- }
1217
- finally {
1218
- this.entered = false;
1219
- }
1220
- }
1221
- decodeArrayStream(stream) {
1222
- return this.decodeMultiAsync(stream, true);
1223
- }
1224
- decodeStream(stream) {
1225
- return this.decodeMultiAsync(stream, false);
1226
- }
1227
- async *decodeMultiAsync(stream, isArray) {
1228
- if (this.entered) {
1229
- const instance = this.clone();
1230
- yield* instance.decodeMultiAsync(stream, isArray);
1231
- return;
1232
- }
1233
- try {
1234
- this.entered = true;
1235
- let isArrayHeaderRequired = isArray;
1236
- let arrayItemsLeft = -1;
1237
- for await (const buffer of stream) {
1238
- if (isArray && arrayItemsLeft === 0) {
1239
- throw this.createExtraByteError(this.totalPos);
1240
- }
1241
- this.appendBuffer(buffer);
1242
- if (isArrayHeaderRequired) {
1243
- arrayItemsLeft = this.readArraySize();
1244
- isArrayHeaderRequired = false;
1245
- this.complete();
1246
- }
1247
- try {
1248
- while (true) {
1249
- yield this.doDecodeSync();
1250
- if (--arrayItemsLeft === 0) {
1251
- break;
1252
- }
1253
- }
1254
- }
1255
- catch (e) {
1256
- if (!(e instanceof RangeError)) {
1257
- throw e; // rethrow
1258
- }
1259
- // fallthrough
1260
- }
1261
- this.totalPos += this.pos;
1262
- }
1263
- }
1264
- finally {
1265
- this.entered = false;
1266
- }
1267
- }
1268
- doDecodeSync() {
1269
- DECODE: while (true) {
1270
- const headByte = this.readHeadByte();
1271
- let object;
1272
- if (headByte >= 0xe0) {
1273
- // negative fixint (111x xxxx) 0xe0 - 0xff
1274
- object = headByte - 0x100;
1275
- }
1276
- else if (headByte < 0xc0) {
1277
- if (headByte < 0x80) {
1278
- // positive fixint (0xxx xxxx) 0x00 - 0x7f
1279
- object = headByte;
1280
- }
1281
- else if (headByte < 0x90) {
1282
- // fixmap (1000 xxxx) 0x80 - 0x8f
1283
- const size = headByte - 0x80;
1284
- if (size !== 0) {
1285
- this.pushMapState(size);
1286
- this.complete();
1287
- continue DECODE;
1288
- }
1289
- else {
1290
- object = {};
1291
- }
1292
- }
1293
- else if (headByte < 0xa0) {
1294
- // fixarray (1001 xxxx) 0x90 - 0x9f
1295
- const size = headByte - 0x90;
1296
- if (size !== 0) {
1297
- this.pushArrayState(size);
1298
- this.complete();
1299
- continue DECODE;
1300
- }
1301
- else {
1302
- object = [];
1303
- }
1304
- }
1305
- else {
1306
- // fixstr (101x xxxx) 0xa0 - 0xbf
1307
- const byteLength = headByte - 0xa0;
1308
- object = this.decodeString(byteLength, 0);
1309
- }
1310
- }
1311
- else if (headByte === 0xc0) {
1312
- // nil
1313
- object = null;
1314
- }
1315
- else if (headByte === 0xc2) {
1316
- // false
1317
- object = false;
1318
- }
1319
- else if (headByte === 0xc3) {
1320
- // true
1321
- object = true;
1322
- }
1323
- else if (headByte === 0xca) {
1324
- // float 32
1325
- object = this.readF32();
1326
- }
1327
- else if (headByte === 0xcb) {
1328
- // float 64
1329
- object = this.readF64();
1330
- }
1331
- else if (headByte === 0xcc) {
1332
- // uint 8
1333
- object = this.readU8();
1334
- }
1335
- else if (headByte === 0xcd) {
1336
- // uint 16
1337
- object = this.readU16();
1338
- }
1339
- else if (headByte === 0xce) {
1340
- // uint 32
1341
- object = this.readU32();
1342
- }
1343
- else if (headByte === 0xcf) {
1344
- // uint 64
1345
- if (this.useBigInt64) {
1346
- object = this.readU64AsBigInt();
1347
- }
1348
- else {
1349
- object = this.readU64();
1350
- }
1351
- }
1352
- else if (headByte === 0xd0) {
1353
- // int 8
1354
- object = this.readI8();
1355
- }
1356
- else if (headByte === 0xd1) {
1357
- // int 16
1358
- object = this.readI16();
1359
- }
1360
- else if (headByte === 0xd2) {
1361
- // int 32
1362
- object = this.readI32();
1363
- }
1364
- else if (headByte === 0xd3) {
1365
- // int 64
1366
- if (this.useBigInt64) {
1367
- object = this.readI64AsBigInt();
1368
- }
1369
- else {
1370
- object = this.readI64();
1371
- }
1372
- }
1373
- else if (headByte === 0xd9) {
1374
- // str 8
1375
- const byteLength = this.lookU8();
1376
- object = this.decodeString(byteLength, 1);
1377
- }
1378
- else if (headByte === 0xda) {
1379
- // str 16
1380
- const byteLength = this.lookU16();
1381
- object = this.decodeString(byteLength, 2);
1382
- }
1383
- else if (headByte === 0xdb) {
1384
- // str 32
1385
- const byteLength = this.lookU32();
1386
- object = this.decodeString(byteLength, 4);
1387
- }
1388
- else if (headByte === 0xdc) {
1389
- // array 16
1390
- const size = this.readU16();
1391
- if (size !== 0) {
1392
- this.pushArrayState(size);
1393
- this.complete();
1394
- continue DECODE;
1395
- }
1396
- else {
1397
- object = [];
1398
- }
1399
- }
1400
- else if (headByte === 0xdd) {
1401
- // array 32
1402
- const size = this.readU32();
1403
- if (size !== 0) {
1404
- this.pushArrayState(size);
1405
- this.complete();
1406
- continue DECODE;
1407
- }
1408
- else {
1409
- object = [];
1410
- }
1411
- }
1412
- else if (headByte === 0xde) {
1413
- // map 16
1414
- const size = this.readU16();
1415
- if (size !== 0) {
1416
- this.pushMapState(size);
1417
- this.complete();
1418
- continue DECODE;
1419
- }
1420
- else {
1421
- object = {};
1422
- }
1423
- }
1424
- else if (headByte === 0xdf) {
1425
- // map 32
1426
- const size = this.readU32();
1427
- if (size !== 0) {
1428
- this.pushMapState(size);
1429
- this.complete();
1430
- continue DECODE;
1431
- }
1432
- else {
1433
- object = {};
1434
- }
1435
- }
1436
- else if (headByte === 0xc4) {
1437
- // bin 8
1438
- const size = this.lookU8();
1439
- object = this.decodeBinary(size, 1);
1440
- }
1441
- else if (headByte === 0xc5) {
1442
- // bin 16
1443
- const size = this.lookU16();
1444
- object = this.decodeBinary(size, 2);
1445
- }
1446
- else if (headByte === 0xc6) {
1447
- // bin 32
1448
- const size = this.lookU32();
1449
- object = this.decodeBinary(size, 4);
1450
- }
1451
- else if (headByte === 0xd4) {
1452
- // fixext 1
1453
- object = this.decodeExtension(1, 0);
1454
- }
1455
- else if (headByte === 0xd5) {
1456
- // fixext 2
1457
- object = this.decodeExtension(2, 0);
1458
- }
1459
- else if (headByte === 0xd6) {
1460
- // fixext 4
1461
- object = this.decodeExtension(4, 0);
1462
- }
1463
- else if (headByte === 0xd7) {
1464
- // fixext 8
1465
- object = this.decodeExtension(8, 0);
1466
- }
1467
- else if (headByte === 0xd8) {
1468
- // fixext 16
1469
- object = this.decodeExtension(16, 0);
1470
- }
1471
- else if (headByte === 0xc7) {
1472
- // ext 8
1473
- const size = this.lookU8();
1474
- object = this.decodeExtension(size, 1);
1475
- }
1476
- else if (headByte === 0xc8) {
1477
- // ext 16
1478
- const size = this.lookU16();
1479
- object = this.decodeExtension(size, 2);
1480
- }
1481
- else if (headByte === 0xc9) {
1482
- // ext 32
1483
- const size = this.lookU32();
1484
- object = this.decodeExtension(size, 4);
1485
- }
1486
- else {
1487
- throw new DecodeError(`Unrecognized type byte: ${prettyByte(headByte)}`);
1488
- }
1489
- this.complete();
1490
- const stack = this.stack;
1491
- while (stack.length > 0) {
1492
- // arrays and maps
1493
- const state = stack.top();
1494
- if (state.type === STATE_ARRAY) {
1495
- state.array[state.position] = object;
1496
- state.position++;
1497
- if (state.position === state.size) {
1498
- object = state.array;
1499
- stack.release(state);
1500
- }
1501
- else {
1502
- continue DECODE;
1503
- }
1504
- }
1505
- else if (state.type === STATE_MAP_KEY) {
1506
- if (object === "__proto__") {
1507
- throw new DecodeError("The key __proto__ is not allowed");
1508
- }
1509
- state.key = this.mapKeyConverter(object);
1510
- state.type = STATE_MAP_VALUE;
1511
- continue DECODE;
1512
- }
1513
- else {
1514
- // it must be `state.type === State.MAP_VALUE` here
1515
- state.map[state.key] = object;
1516
- state.readCount++;
1517
- if (state.readCount === state.size) {
1518
- object = state.map;
1519
- stack.release(state);
1520
- }
1521
- else {
1522
- state.key = null;
1523
- state.type = STATE_MAP_KEY;
1524
- continue DECODE;
1525
- }
1526
- }
1527
- }
1528
- return object;
1529
- }
1530
- }
1531
- readHeadByte() {
1532
- if (this.headByte === HEAD_BYTE_REQUIRED) {
1533
- this.headByte = this.readU8();
1534
- // console.log("headByte", prettyByte(this.headByte));
1535
- }
1536
- return this.headByte;
1537
- }
1538
- complete() {
1539
- this.headByte = HEAD_BYTE_REQUIRED;
1540
- }
1541
- readArraySize() {
1542
- const headByte = this.readHeadByte();
1543
- switch (headByte) {
1544
- case 0xdc:
1545
- return this.readU16();
1546
- case 0xdd:
1547
- return this.readU32();
1548
- default: {
1549
- if (headByte < 0xa0) {
1550
- return headByte - 0x90;
1551
- }
1552
- else {
1553
- throw new DecodeError(`Unrecognized array type byte: ${prettyByte(headByte)}`);
1554
- }
1555
- }
1556
- }
1557
- }
1558
- pushMapState(size) {
1559
- if (size > this.maxMapLength) {
1560
- throw new DecodeError(`Max length exceeded: map length (${size}) > maxMapLengthLength (${this.maxMapLength})`);
1561
- }
1562
- this.stack.pushMapState(size);
1563
- }
1564
- pushArrayState(size) {
1565
- if (size > this.maxArrayLength) {
1566
- throw new DecodeError(`Max length exceeded: array length (${size}) > maxArrayLength (${this.maxArrayLength})`);
1567
- }
1568
- this.stack.pushArrayState(size);
1569
- }
1570
- decodeString(byteLength, headerOffset) {
1571
- if (!this.rawStrings || this.stateIsMapKey()) {
1572
- return this.decodeUtf8String(byteLength, headerOffset);
1573
- }
1574
- return this.decodeBinary(byteLength, headerOffset);
1575
- }
1576
- /**
1577
- * @throws {@link RangeError}
1578
- */
1579
- decodeUtf8String(byteLength, headerOffset) {
1580
- if (byteLength > this.maxStrLength) {
1581
- throw new DecodeError(`Max length exceeded: UTF-8 byte length (${byteLength}) > maxStrLength (${this.maxStrLength})`);
1582
- }
1583
- if (this.bytes.byteLength < this.pos + headerOffset + byteLength) {
1584
- throw MORE_DATA;
1585
- }
1586
- const offset = this.pos + headerOffset;
1587
- let object;
1588
- if (this.stateIsMapKey() && this.keyDecoder?.canBeCached(byteLength)) {
1589
- object = this.keyDecoder.decode(this.bytes, offset, byteLength);
1590
- }
1591
- else {
1592
- object = utf8Decode(this.bytes, offset, byteLength);
1593
- }
1594
- this.pos += headerOffset + byteLength;
1595
- return object;
1596
- }
1597
- stateIsMapKey() {
1598
- if (this.stack.length > 0) {
1599
- const state = this.stack.top();
1600
- return state.type === STATE_MAP_KEY;
1601
- }
1602
- return false;
1603
- }
1604
- /**
1605
- * @throws {@link RangeError}
1606
- */
1607
- decodeBinary(byteLength, headOffset) {
1608
- if (byteLength > this.maxBinLength) {
1609
- throw new DecodeError(`Max length exceeded: bin length (${byteLength}) > maxBinLength (${this.maxBinLength})`);
1610
- }
1611
- if (!this.hasRemaining(byteLength + headOffset)) {
1612
- throw MORE_DATA;
1613
- }
1614
- const offset = this.pos + headOffset;
1615
- const object = this.bytes.subarray(offset, offset + byteLength);
1616
- this.pos += headOffset + byteLength;
1617
- return object;
1618
- }
1619
- decodeExtension(size, headOffset) {
1620
- if (size > this.maxExtLength) {
1621
- throw new DecodeError(`Max length exceeded: ext length (${size}) > maxExtLength (${this.maxExtLength})`);
1622
- }
1623
- const extType = this.view.getInt8(this.pos + headOffset);
1624
- const data = this.decodeBinary(size, headOffset + 1 /* extType */);
1625
- return this.extensionCodec.decode(data, extType, this.context);
1626
- }
1627
- lookU8() {
1628
- return this.view.getUint8(this.pos);
1629
- }
1630
- lookU16() {
1631
- return this.view.getUint16(this.pos);
1632
- }
1633
- lookU32() {
1634
- return this.view.getUint32(this.pos);
1635
- }
1636
- readU8() {
1637
- const value = this.view.getUint8(this.pos);
1638
- this.pos++;
1639
- return value;
1640
- }
1641
- readI8() {
1642
- const value = this.view.getInt8(this.pos);
1643
- this.pos++;
1644
- return value;
1645
- }
1646
- readU16() {
1647
- const value = this.view.getUint16(this.pos);
1648
- this.pos += 2;
1649
- return value;
1650
- }
1651
- readI16() {
1652
- const value = this.view.getInt16(this.pos);
1653
- this.pos += 2;
1654
- return value;
1655
- }
1656
- readU32() {
1657
- const value = this.view.getUint32(this.pos);
1658
- this.pos += 4;
1659
- return value;
1660
- }
1661
- readI32() {
1662
- const value = this.view.getInt32(this.pos);
1663
- this.pos += 4;
1664
- return value;
1665
- }
1666
- readU64() {
1667
- const value = getUint64(this.view, this.pos);
1668
- this.pos += 8;
1669
- return value;
1670
- }
1671
- readI64() {
1672
- const value = getInt64(this.view, this.pos);
1673
- this.pos += 8;
1674
- return value;
1675
- }
1676
- readU64AsBigInt() {
1677
- const value = this.view.getBigUint64(this.pos);
1678
- this.pos += 8;
1679
- return value;
1680
- }
1681
- readI64AsBigInt() {
1682
- const value = this.view.getBigInt64(this.pos);
1683
- this.pos += 8;
1684
- return value;
1685
- }
1686
- readF32() {
1687
- const value = this.view.getFloat32(this.pos);
1688
- this.pos += 4;
1689
- return value;
1690
- }
1691
- readF64() {
1692
- const value = this.view.getFloat64(this.pos);
1693
- this.pos += 8;
1694
- return value;
1695
- }
1696
- }
1697
-
1698
- /**
1699
- * It decodes a single MessagePack object in a buffer.
1700
- *
1701
- * This is a synchronous decoding function.
1702
- * See other variants for asynchronous decoding: {@link decodeAsync}, {@link decodeMultiStream}, or {@link decodeArrayStream}.
1703
- *
1704
- * @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty.
1705
- * @throws {@link DecodeError} if the buffer contains invalid data.
1706
- */
1707
- function decode(buffer, options) {
1708
- const decoder = new Decoder(options);
1709
- return decoder.decode(buffer);
1710
- }
1711
-
1712
- /**
1713
- * Heartbeat ping interval in milliseconds
1714
- * Also used as the minimum age for CRDT garbage collection - operations are
1715
- * retained for at least one heartbeat cycle to ensure even slow clients can never
1716
- * be desynced as a result of gc
1717
- */
1718
- const HEARTBEAT_INTERVAL = 5000;
1719
-
1720
- const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
1721
-
1722
- /**
1723
- * CRDT Manager
1724
- * Custom conflict-free replicated data type system with vector clocks
1725
- */
1726
- class CRDTManager {
1727
- // Storage
1728
- #replicaId;
1729
- #keyOperations = new Map();
1730
- #vectorClock = new Map();
1731
-
1732
- #propertyStore = {}; // Current local values as key/value store object
1733
- #lastPropertyStore = {}; // Last property store to compare against
1734
-
1735
- // Local Garbage Collection
1736
- #lastGCCheck = 0;
1737
- #opUuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
1738
-
1739
- // Debug
1740
- #debug = false;
1741
-
1742
- /**
1743
- * Create a new instance
1744
- * @param {boolean} [debug=false] - Enable debug logging
1745
- */
1746
- constructor(debug) {
1747
- if (debug) this.#debug = true;
1748
- this.#replicaId = crypto.randomUUID();
1749
- this.#vectorClock.set(this.#replicaId, 0);
1750
- }
1751
-
1752
- /**
1753
- * Import the entire state of the CRDT manager (this overwrites the old state)
1754
- * @param {object} state - Full state of the CRDT manager
1755
- */
1756
- importState(state) {
1757
- try {
1758
- const { keyOperations, vectorClock } = state;
1759
- if (this.#debug) console.log(CONSOLE_PREFIX + "Importing state:", state);
1760
-
1761
- // Resets
1762
- this.#opUuidTimestamp.clear();
1763
- this.#propertyStore = {};
1764
- this.#lastPropertyStore = {};
1765
-
1766
- this.#keyOperations = new Map(keyOperations); // Rebuild the operations map
1767
- this.#vectorClock = new Map(vectorClock); // Rebuild the vector clock map
1768
- if (!this.#vectorClock.has(this.#replicaId)) this.#vectorClock.set(this.#replicaId, 0); // Add own vector clock if it wasn't present in the imported state
1769
-
1770
- // Map all operation uuids to current timestamp
1771
- this.#keyOperations.forEach((value, _key) => {
1772
- value?.forEach((operation) => {
1773
- this.#opUuidTimestamp.set(operation?.uuid, Date.now());
1774
- });
1775
- });
1776
-
1777
- // Process each key to update local values
1778
- for (const key of this.#keyOperations.keys()) this.#processLocalProperty(key);
1779
-
1780
- } catch (error) {
1781
- console.error(CONSOLE_PREFIX + "Failed to import state:", error);
1782
- }
1783
- }
1784
-
1785
- /**
1786
- * Import property update
1787
- * @param {object} data - Property update data
1788
- */
1789
- importPropertyUpdate(data) {
1790
- try {
1791
- const { key, operation: rawOperation, vectorClock } = data;
1792
- const operation = this.#sanitizeValue(rawOperation);
1793
- if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
1794
-
1795
- // Check key limit to afeguard against too many keys
1796
- if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error('Key limit exceeded!');
1797
-
1798
- // Get COPY of current ops (or empty array if none yet)
1799
- const currentOps = [...(this.#keyOperations.get(key) || [])];
1800
- // Merge vector clocks (always take max value)
1801
- for (const [id, counter] of vectorClock) {
1802
- if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
1803
- this.#vectorClock.set(id, counter);
1804
- }
1805
- }
1806
-
1807
- // Safeguard against massive vector clocks
1808
- if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
1809
-
1810
- // Add new operation if it's not already added
1811
- const existingUuids = new Set(currentOps.map(op => op.uuid));
1812
- if (operation.uuid && !existingUuids.has(operation.uuid)) {
1813
- currentOps.push({ ...operation }); // Add operation
1814
- this.#opUuidTimestamp.set(operation.uuid, Date.now()); // Add timestamp for gc
1815
- }
1816
-
1817
- // Sort, update operations & local value
1818
- this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
1819
- this.#processLocalProperty(key);
1820
- this.#checkGarbageCollection();
1821
-
1822
- } catch (error) {
1823
- console.error(CONSOLE_PREFIX + "Failed to import property update:", error);
1824
- }
1825
- }
1826
-
1827
- /**
1828
- * Update a property
1829
- * @param {string} key - Storage key
1830
- * @param {string} type - Operation type
1831
- * @param {*} value - New value, or value to match
1832
- * @param {*} updateValue - New value for 'matching' operations
1833
- * @returns {object | undefined} - Returns the property update
1834
- */
1835
- updateProperty(key, type, value, updateValue) {
1836
- try {
1837
- // Sanitize inputs
1838
- value = this.#sanitizeValue(value);
1839
- updateValue = this.#sanitizeValue(updateValue);
1840
-
1841
- // Debug log
1842
- if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
1843
-
1844
- // Increment vector clock
1845
- const counter = this.#vectorClock.get(this.#replicaId) || 0;
1846
- this.#vectorClock.set(this.#replicaId, counter + 1);
1847
-
1848
- // Assign shallow COPY of currennt ops or fall back to empty array
1849
- const currentOps = [...(this.#keyOperations.get(key) || [])];
1850
-
1851
- // Add operation
1852
- const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
1853
- currentOps.push(newOp);
1854
- this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
1855
- this.#processLocalProperty(key); // Process local value
1856
- this.#checkGarbageCollection();
1857
-
1858
- // Return the property update with the new operation (this can be imported using importPropertyUpdate)
1859
- return {
1860
- key,
1861
- operation: { ...newOp },
1862
- vectorClock: Array.from(this.#vectorClock.entries())
1863
- };
1864
-
1865
- } catch (error) {
1866
- console.error(CONSOLE_PREFIX + `Failed to add operation for key "${key}":`, error);
1867
- }
1868
- }
1869
-
1870
- /**
1871
- * Check if garbage collected can be performed & run it
1872
- */
1873
- #checkGarbageCollection() {
1874
- const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
1875
-
1876
- try {
1877
- if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1878
- this.#lastGCCheck = Date.now();
1879
-
1880
- for (const [key, operations] of this.#keyOperations.entries()) {
1881
- if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1882
-
1883
- let retainCount = operations.length;
1884
- operations.forEach((op, index) => {
1885
- // Count how many ops, from last to latest, are older than the min-age in a row
1886
- if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > HEARTBEAT_INTERVAL) {
1887
- const removeCount = index + 1;
1888
- retainCount = operations.length - removeCount;
1889
- this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1890
- }
1891
- });
1892
-
1893
- if (retainCount < operations.length) {
1894
- if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1895
- const retainOps = operations.slice(-retainCount); // Newest ops
1896
- const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1897
- const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1898
-
1899
- // Calculate the value at the point where retained operations start
1900
- let baselineValue = null;
1901
- for (const op of removeOps) {
1902
- if (!op.data) continue;
1903
- baselineValue = this.#handleOperation(
1904
- baselineValue,
1905
- op.data.type,
1906
- op.data.value,
1907
- op.data.updateValue
1908
- );
1909
- }
1910
-
1911
- // Create a compact operation with baseline value and appropriate vector clock
1912
- const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1913
-
1914
- // Debug log
1915
- if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
1916
-
1917
- // Combine compact op with retained operations
1918
- this.#keyOperations.set(key, [compactOp, ...retainOps]);
1919
- }
1920
- }
1921
- } catch (error) {
1922
- console.error(CONSOLE_PREFIX + "Error running garbage collection:", error);
1923
- }
1924
- }
1925
-
1926
- /**
1927
- * Create an operation object
1928
- * @param {object} data - Operation data, consists of type, value and updateValue
1929
- * @param {Array} vectorClock - Vector clock of the replica/client
1930
- * @returns {object} - Operation object
1931
- */
1932
- #createOperation(data, vectorClock) {
1933
- const newOperation = {
1934
- data,
1935
- vectorClock,
1936
- source: this.#replicaId,
1937
- uuid: crypto.randomUUID()
1938
- };
1939
- this.#opUuidTimestamp.set(newOperation.uuid, Date.now());
1940
- return newOperation;
1941
- }
1942
-
1943
- /**
1944
- * Process a property's value by applying all operations and set it in the property store
1945
- * @param {string} key - Storage key
1946
- */
1947
- #processLocalProperty(key) {
1948
- try {
1949
- const ops = this.#keyOperations.get(key);
1950
- if (!ops?.length) return;
1951
-
1952
- // Apply all operations in order
1953
- let value = null;
1954
-
1955
- for (const op of ops) {
1956
- if (!op.data) continue;
1957
- value = this.#handleOperation(
1958
- value,
1959
- op.data.type,
1960
- op.data.value,
1961
- op.data.updateValue
1962
- );
1963
- }
1964
-
1965
- this.#propertyStore[key] = value; // Save locally
1966
- } catch (error) {
1967
- console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
1968
- }
1969
- }
1970
-
1971
- /**
1972
- * Sort operations by vector clock (causal order)
1973
- * @param {Array} operations - Full operations that include vector clocks, data and a source
1974
- * @returns {Array} - Sorted operations
1975
- */
1976
- #sortByVectorClock(operations) {
1977
- return [...operations].sort((a, b) => {
1978
- const clockA = new Map(a.vectorClock || []);
1979
- const clockB = new Map(b.vectorClock || []);
1980
-
1981
- // First sort by causal relationship
1982
- let aGreater = false, bGreater = false;
1983
-
1984
- const allIds = new Set([...clockA.keys(), ...clockB.keys()]);
1985
- for (const id of allIds) {
1986
- const countA = clockA.get(id) || 0;
1987
- const countB = clockB.get(id) || 0;
1988
-
1989
- if (countA > countB) aGreater = true;
1990
- if (countA < countB) bGreater = true;
1991
- }
1992
-
1993
- // Determine causal relationship (if one is greater, and the other is smaller, it's clear)
1994
- if (aGreater && !bGreater) return 1; // a happens after b
1995
- if (!aGreater && bGreater) return -1; // a happens before b
1996
-
1997
- // Concurrent (unclear): Compare highest counter in each (sort by which one is ahead)
1998
- const maxA = Math.max(...Array.from(clockA.values()), 0);
1999
- const maxB = Math.max(...Array.from(clockB.values()), 0);
2000
- if (maxA !== maxB) return maxA - maxB;
2001
-
2002
- // Final tiebreak: Sort by ID of Replica that actually generated the operation
2003
- return a.source.localeCompare(b.source);
2004
- });
2005
- }
2006
-
2007
- /**
2008
- * Handle an operation
2009
- * @param {*} curValue - Current value of the storage key
2010
- * @param {string} type - Operation type
2011
- * @param {*} value - New value, or value to match
2012
- * @param {*} [updateValue] - New value for 'matching' operations
2013
- * @returns {*} - Value after the operation
2014
- */
2015
- #handleOperation(curValue, type, value, updateValue) {
2016
- try {
2017
- // Deep copy to avoid reference issues in case value is or contains object(s)
2018
- curValue = structuredClone(curValue);
2019
-
2020
- // Set operation
2021
- if (type === "set") return value;
2022
-
2023
- // Array operations
2024
- if (type.startsWith('array')) {
2025
- if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
2026
-
2027
- // Comparison function
2028
- const isObject = typeof value === 'object' && value !== null;
2029
- const compare = (item) => {
2030
- if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
2031
- return item === value; // Value comparison
2032
- };
2033
-
2034
- switch (type) {
2035
- case 'array-add':
2036
- curValue.push(value);
2037
- break;
2038
- case 'array-add-unique':
2039
- if (!curValue.some(compare)) curValue.push(value);
2040
- break;
2041
- case 'array-remove-matching':
2042
- curValue = curValue.filter(item => !compare(item));
2043
- break;
2044
- case 'array-update-matching': {
2045
- const index = curValue.findIndex(compare);
2046
- if (index !== -1) curValue[index] = updateValue;
2047
- break;
2048
- }
2049
- }
2050
- }
2051
-
2052
- return curValue;
2053
-
2054
- } catch (error) {
2055
- console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
2056
- return curValue;
2057
- }
2058
- }
2059
-
2060
- /**
2061
- * Remove HTML to prevent XSS and enforce size limits
2062
- * @param {*} value - Value to sanitize
2063
- * @returns {*} - Sanitized value
2064
- */
2065
- #sanitizeValue(value) {
2066
- // Check total serialized size
2067
- const jsonString = JSON.stringify(value);
2068
- if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
2069
-
2070
- if (typeof value === 'string') return (value.includes('<') || value.includes('>')) ? value.replace(/[<>]/g, '') : value;
2071
- if (Array.isArray(value)) return value.map(item => this.#sanitizeValue(item));
2072
- if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.#sanitizeValue(v)]));
2073
- return value;
2074
- }
2075
-
2076
- /**
2077
- * Get property store
2078
- * @returns {Record<string, any>} - Object with key value pairs
2079
- */
2080
- get propertyStore() {
2081
- try {
2082
- return structuredClone(this.#propertyStore); // Return deep clone
2083
- } catch {
2084
- console.error(CONSOLE_PREFIX + "Failed to parse property store");
2085
- return {}
2086
- }
2087
- }
2088
-
2089
- /**
2090
- * Check for changes in the local property store
2091
- * @returns {boolean} - Check if any values changed going from the last to the current property update
2092
- */
2093
- get didPropertiesChange() {
2094
- try {
2095
- if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
2096
- this.#lastPropertyStore = structuredClone(this.#propertyStore);
2097
- return true;
2098
- }
2099
- } catch {
2100
- console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores");
2101
- }
2102
- return false;
2103
- }
2104
-
2105
- /**
2106
- * Export state (can be imported using importState, converts the maps to arrays for serialization)
2107
- * @returns {object} - State
2108
- */
2109
- get state() {
2110
- return {
2111
- keyOperations: [...this.#keyOperations.entries()],
2112
- vectorClock: [...this.#vectorClock.entries()]
2113
- };
2114
- }
2115
- }
2116
-
2117
- const ERROR_PREFIX = "PlaySocket error: ";
2118
- const WARNING_PREFIX = "PlaySocket warning: ";
2119
- const LOG_PREFIX = "PlaySocket log: ";
2120
- const TIMEOUT_MS = 3000; // 3 second timeout for WS messages
2121
-
2122
- /**
2123
- * PlaySocket Client
2124
- */
2125
- class PlaySocket {
2126
- // Core properties
2127
- #id; // Unique client ID
2128
- #sessionToken; // Unique session token
2129
- #endpoint;
2130
- #socket; // WebSocket connection
2131
- #initialized = false; // Initialization status
2132
- #customData;
2133
-
2134
- // Room properties
2135
- #roomHost;
2136
- #inRoom; // If the client is currently in a room or not
2137
- #participantCount = 0;
2138
- #crdtManager;
2139
- #roomVersion = 0; // Update version (used to compare local vs. remote state to detect package loss)
2140
-
2141
- // Event handling
2142
- #callbacks = new Map(); // Event callbacks
2143
-
2144
- // Async server operations
2145
- #pendingJoin;
2146
- #pendingCreate;
2147
- #pendingRegistration;
2148
- #pendingConnect;
2149
- #pendingReconnect;
2150
-
2151
- // Timeouts or intervals
2152
- #reconnectTimeout;
2153
- #reconnectCount = 0;
2154
- #isReconnecting = false;
2155
-
2156
- // Debug
2157
- #debug = false;
2158
-
2159
- /**
2160
- * Create a new PlaySocket instance
2161
- * @param {string} id - Unique identifier for this client
2162
- * @param {object} options - Connection options
2163
- * @param {string} options.endpoint - WebSocket endpoint path
2164
- * @param {object} [options.customData] - Custom registration data
2165
- * @param {boolean} [options.debug=false] - Enable debug logging
2166
- */
2167
- constructor(id, options = {}) {
2168
- this.#id = id;
2169
- if (options.endpoint) this.#endpoint = options.endpoint;
2170
- if (options.customData) this.#customData = { ...options.customData };
2171
- if (options.debug) this.#debug = true; // Enabling extra logging
2172
- this.#crdtManager = new CRDTManager(this.#debug);
2173
- }
2174
-
2175
- /**
2176
- * Helper to create timeout promises for create room, join room etc.
2177
- * @param {string} name - Name of the timeout
2178
- * @returns {Promise} - Promise that rejects after the timeout
2179
- * @private
2180
- */
2181
- #createTimeout(name) {
2182
- return new Promise((_, reject) =>
2183
- setTimeout(() => reject(new Error(`${name} timed out.`)), TIMEOUT_MS)
2184
- );
2185
- }
2186
-
2187
- /**
2188
- * Register an event callback
2189
- * @param {string} event - Event name
2190
- * @param {Function} callback - Callback function
2191
- */
2192
- onEvent(event, callback) {
2193
- const validEvents = ["status", "error", "moved", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientJoined", "clientLeft"];
2194
- if (!validEvents.includes(event)) {
2195
- console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
2196
- return;
2197
- } if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2198
- this.#callbacks.get(event).push(callback);
2199
- }
2200
-
2201
- /**
2202
- * Trigger an event to registered callbacks
2203
- * @param {string} event - Event name
2204
- * @param {...*} args - Arguments
2205
- * @private
2206
- */
2207
- #triggerEvent(event, ...args) {
2208
- const callbacks = this.#callbacks.get(event);
2209
- callbacks?.forEach(callback => {
2210
- try {
2211
- callback(...args);
2212
- } catch (error) {
2213
- console.error(ERROR_PREFIX + `${event} callback error:`, error);
2214
- }
2215
- });
2216
- }
2217
-
2218
- /**
2219
- * Initialize the PlaySocket instance by connecting to the WS server
2220
- * @returns {Promise} Resolves when connection is established
2221
- */
2222
- async init() {
2223
- if (this.#initialized) return Promise.reject(new Error("Already initialized!"));
2224
- if (!this.#endpoint) return Promise.reject(new Error("No websocket endpoint provided!"));
2225
- this.#triggerEvent("status", "Initializing...");
2226
-
2227
- // Connect to WS server
2228
- await this.#connect();
2229
-
2230
- // Register with server
2231
- const id = await Promise.race([
2232
- new Promise((resolve, reject) => {
2233
- this.#pendingRegistration = { resolve, reject }; // Resolve or reject depending on answer to registration msg
2234
-
2235
- // Register with server
2236
- this.#sendToServer({
2237
- type: 'register',
2238
- id: this.#id,
2239
- customData: this.#customData || undefined
2240
- });
2241
- }),
2242
- this.#createTimeout("Registration")
2243
- ]).finally(() => {
2244
- this.#pendingRegistration = null;
2245
- });
2246
-
2247
- // If the server sent out an id, store and return that
2248
- if (id) this.#id = id;
2249
- return this.#id;
2250
- }
2251
-
2252
- /**
2253
- * Connect to the WS server and
2254
- * @returns {Promise} Resolves when the connection was established
2255
- * @private
2256
- */
2257
- async #connect() {
2258
- return Promise.race([
2259
- new Promise((resolve, reject) => {
2260
- this.#pendingConnect = { reject };
2261
- this.#socket = new WebSocket(this.#endpoint);
2262
- this.#socket.binaryType = 'arraybuffer'; // Important for MessagePack binary data
2263
- this.#setupSocketHandlers(); // Message & close events
2264
- this.#socket.onopen = resolve;
2265
- }),
2266
- this.#createTimeout("Connection attempt")
2267
- ]).finally(() => {
2268
- this.#pendingConnect = null;
2269
- });
2270
- }
2271
-
2272
- /**
2273
- * Set up WebSocket message & close handlers
2274
- * @private
2275
- */
2276
- #setupSocketHandlers() {
2277
- this.#socket.onmessage = (event) => {
2278
- try {
2279
- const message = decode(new Uint8Array(event.data));
2280
- if (!message.type) return;
2281
-
2282
- switch (message.type) {
2283
- case 'registration_failed':
2284
- this.#triggerEvent("error", "Failed to register: " + message.reason);
2285
- if (this.#pendingRegistration) this.#pendingRegistration.reject(new Error("Failed to register: " + message.reason));
2286
- break;
2287
-
2288
- case 'registered':
2289
- this.#sessionToken = message.sessionToken;
2290
- this.#initialized = true;
2291
- if (this.#pendingRegistration) this.#pendingRegistration.resolve(message.id);
2292
- this.#triggerEvent("status", "Connected to server.");
2293
- break;
2294
-
2295
- case 'join_accepted':
2296
- // Connected to room
2297
- if (this.#debug) console.log(LOG_PREFIX + "State received for join:", message.state);
2298
- this.#inRoom = true;
2299
- this.#crdtManager.importState(message.state);
2300
- this.#participantCount = message.participantCount;
2301
- this.#roomHost = message.host;
2302
- this.#roomVersion = message.version;
2303
- this.#triggerEvent("storageUpdated", this.storage);
2304
- if (this.#pendingJoin) {
2305
- this.#triggerEvent("status", "Connected to room.");
2306
- this.#pendingJoin.resolve();
2307
- } else {
2308
- this.#triggerEvent("status", "Moved to room.");
2309
- this.#triggerEvent("moved", message.roomId);
2310
- }
2311
- break;
2312
-
2313
- case 'join_rejected':
2314
- // Connection to room failed
2315
- this.#triggerEvent("status", "Failed to join room: " + message.reason);
2316
- if (this.#pendingJoin) this.#pendingJoin.reject(new Error("Failed to join room: " + message.reason));
2317
- break;
2318
-
2319
- case 'reconnected':
2320
- // Successfully reconnected
2321
- this.#isReconnecting = false;
2322
- this.#reconnectCount = 0;
2323
- if (message.roomData) {
2324
- if (this.#debug) console.log(LOG_PREFIX + "State received for reconnect:", message.roomData.state);
2325
- this.#crdtManager.importState(message.roomData.state);
2326
- this.#roomVersion = message.roomData.version;
2327
- this.#participantCount = message.roomData.participantCount;
2328
- this.#setHost(message.roomData.host); // Set host before in case there are .isHost checks in the storageUpdate fallback
2329
- this.#triggerEvent("storageUpdated", this.storage);
2330
- } else if (this.#inRoom) {
2331
- // If no room data received, but client thinks they were in a room...
2332
- this.#triggerEvent("error", "Reconnected, but room no longer exists.");
2333
- return this.destroy();
2334
- }
2335
- this.#triggerEvent("status", "Reconnected.");
2336
- if (this.#pendingReconnect) this.#pendingReconnect.resolve();
2337
- break;
2338
-
2339
- case 'reconnection_failed':
2340
- if (this.#pendingReconnect) this.#pendingReconnect.reject(new Error("Server rejected reconnection: " + message.reason));
2341
- break;
2342
-
2343
- case 'room_created':
2344
- this.#inRoom = true;
2345
- this.#participantCount = message.participantCount;
2346
- this.#triggerEvent("status", `Room created.`);
2347
- this.#crdtManager.importState(message.state);
2348
- this.#triggerEvent("storageUpdated", this.storage);
2349
- if (this.#pendingCreate) this.#pendingCreate.resolve(message.roomId);
2350
- break;
2351
-
2352
- case 'room_creation_failed':
2353
- this.#triggerEvent("error", "Failed to create room: " + message.reason);
2354
- if (this.#pendingCreate) this.#pendingCreate.reject(new Error("Failed to create room: " + message.reason));
2355
- break;
2356
-
2357
- case 'property_updated':
2358
- this.#roomVersion++; // Increment room version
2359
- if (this.#debug) console.log(LOG_PREFIX + "Property update received:", message.update);
2360
- this.#crdtManager.importPropertyUpdate(message.update);
2361
- if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.storage);
2362
- if (this.#roomVersion != message.version && this.#initialized && this.#socket?.readyState === WebSocket.OPEN) {
2363
- this.#triggerEvent("error", "Detected skipped update – forcing reconnect.");
2364
- this.#socket?.close();
2365
- }
2366
- break;
2367
-
2368
- case 'property_update_rejected':
2369
- this.#triggerEvent("error", "Property update rejected. Re-syncing state.");
2370
- this.#crdtManager.importState(message.state);
2371
- this.#triggerEvent("storageUpdated", this.storage);
2372
- break;
2373
-
2374
- case 'kicked':
2375
- this.#triggerEvent("error", `Kicked out of room: ${message.reason || "No reason provided."}`);
2376
- this.destroy();
2377
- break;
2378
-
2379
- case 'host_migrated':
2380
- this.#setHost(message.newHost);
2381
- break;
2382
-
2383
- case 'client_left':
2384
- this.#participantCount = message.participantCount;
2385
- this.#triggerEvent("clientLeft", message.client);
2386
- this.#triggerEvent("status", `Client ${message.client} disconnected.`);
2387
- break;
2388
-
2389
- case 'client_joined':
2390
- this.#participantCount = message.participantCount;
2391
- this.#triggerEvent("clientJoined", message.client);
2392
- this.#triggerEvent("status", `Client ${message.client} connected.`);
2393
- break;
2394
- }
2395
- } catch (error) {
2396
- console.error(ERROR_PREFIX + "Error handling message:", error);
2397
- }
2398
- };
2399
-
2400
- // Handle socket errors
2401
- this.#socket.onerror = () => {
2402
- this.#triggerEvent("error", "WebSocket error.");
2403
- if (this.#pendingConnect) this.#pendingConnect.reject(new Error("WebSocket error!"));
2404
- };
2405
-
2406
- // Handle socket close & attempt reconnect
2407
- this.#socket.onclose = () => {
2408
- if (!this.#initialized || this.#isReconnecting) return;
2409
- this.#triggerEvent("status", "Disconnected.");
2410
- this.#reconnectCount = 0;
2411
- this.#attemptReconnect();
2412
- };
2413
- };
2414
-
2415
- /**
2416
- * Attempt to reconnect with a fixed number of retries
2417
- * @private
2418
- */
2419
- async #attemptReconnect() {
2420
- this.#reconnectCount++;
2421
- if (this.#reconnectCount > 9) {
2422
- this.#triggerEvent("error", "Disconnected from server.");
2423
- this.destroy();
2424
- return;
2425
- }
2426
-
2427
- this.#triggerEvent("status", `Attempting to reconnect... (${this.#reconnectCount})`);
2428
- this.#isReconnecting = true;
2429
- try {
2430
- await this.#connect();
2431
- await Promise.race([
2432
- new Promise((resolve, reject) => {
2433
- this.#pendingReconnect = { resolve, reject };
2434
- this.#sendToServer({
2435
- type: 'reconnect',
2436
- id: this.#id,
2437
- sessionToken: this.#sessionToken
2438
- });
2439
- }),
2440
- this.#createTimeout("Reconnection request")
2441
- ]).finally(() => {
2442
- this.#pendingReconnect = null;
2443
- });
2444
- } catch (error) {
2445
- if (!this.#initialized) return;
2446
- this.#triggerEvent("status", "Reconnection failed: " + error.message);
2447
- this.#reconnectTimeout = setTimeout(() => this.#attemptReconnect(), 500);
2448
- }
2449
- }
2450
-
2451
- /**
2452
- * Update the host in case a new one was chosen
2453
- * @param {string} hostId - Client ID of new host
2454
- * @private
2455
- */
2456
- #setHost(hostId) {
2457
- if (this.#roomHost != hostId) {
2458
- this.#roomHost = hostId;
2459
- this.#triggerEvent("hostMigrated", hostId);
2460
- }
2461
- }
2462
-
2463
- /**
2464
- * Send a message to the server
2465
- * @param {object} data - The data to send to the server
2466
- * @private
2467
- */
2468
- #sendToServer(data) {
2469
- if (!this.#socket || this.#socket?.readyState !== WebSocket.OPEN) {
2470
- console.warn(WARNING_PREFIX + "Failed to send message - not connected.");
2471
- return;
2472
- }
2473
- try {
2474
- this.#socket.send(encode(data));
2475
- } catch (error) {
2476
- console.error(ERROR_PREFIX + "Error sending message:", error);
2477
- this.#triggerEvent("error", "Error sending message: " + error.message);
2478
- }
2479
- }
2480
-
2481
- /**
2482
- * Create a new room and become host
2483
- * @param {object} initialStorage - Initial state
2484
- * @param {number} size - Max number of participants
2485
- * @returns {Promise} Resolves with room ID
2486
- */
2487
- async createRoom(initialStorage = {}, size) {
2488
- if (!this.#initialized) {
2489
- this.#triggerEvent("error", "Failed to create room - not initialized");
2490
- return Promise.reject(new Error("Not initialized!"));
2491
- }
2492
-
2493
- return Promise.race([
2494
- new Promise((resolve, reject) => {
2495
- this.#roomHost = this.#id;
2496
- this.#pendingCreate = { resolve, reject };
2497
- this.#sendToServer({
2498
- type: 'create_room',
2499
- initialStorage,
2500
- size
2501
- });
2502
- }),
2503
- this.#createTimeout("Room creation")
2504
- ]).finally(() => {
2505
- this.#pendingCreate = null;
2506
- });
2507
- }
2508
-
2509
- /**
2510
- * Join an existing room
2511
- * @param {string} roomId - ID of the room
2512
- * @returns {Promise} - Resolves when connected
2513
- */
2514
- async joinRoom(roomId) {
2515
- if (!this.#initialized) {
2516
- this.#triggerEvent("error", "Failed to join room - not initialized");
2517
- return Promise.reject(new Error("Not initialized!"));
2518
- }
2519
-
2520
- return Promise.race([
2521
- new Promise((resolve, reject) => {
2522
- this.#pendingJoin = { resolve, reject };
2523
-
2524
- // Send connection request
2525
- this.#triggerEvent("status", `Connecting to room ${roomId}...`);
2526
- this.#sendToServer({
2527
- type: 'join_room',
2528
- roomId
2529
- });
2530
- }),
2531
- this.#createTimeout("Room join")
2532
- ]).finally(() => {
2533
- this.#pendingJoin = null;
2534
- });
2535
- }
2536
-
2537
- /**
2538
- * Update a value in the shared storage
2539
- * @param {string} key - Storage key
2540
- * @param {'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching'} type - Operation type
2541
- * @param {*} value - New value or value to operate on
2542
- * @param {*} updateValue - New value for update-matching
2543
- */
2544
- updateStorage(key, type, value, updateValue) {
2545
- if (!this.#inRoom) {
2546
- this.#triggerEvent("error", "Failed to update storage - not in a room.");
2547
- return;
2548
- } if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and updateValue ${updateValue}.`);
2549
- const propUpdate = this.#crdtManager.updateProperty(key, type, value, updateValue);
2550
- this.#sendToServer({
2551
- type: 'update_property',
2552
- update: propUpdate
2553
- });
2554
- if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.storage); // Always trigger callback AFTER send in case msgs are sent in the callback (which would break the order)
2555
- }
2556
-
2557
- /**
2558
- * Send a custom request to the server
2559
- * @param {string} name - Name of the request
2560
- * @param {*} [data] - Custom data
2561
- */
2562
- sendRequest(name, data) {
2563
- if (this.#debug) console.log(LOG_PREFIX + `Server request with name ${name} and data:`, data);
2564
- this.#sendToServer({
2565
- type: 'request',
2566
- request: { name, data }
2567
- });
2568
- }
2569
-
2570
- /**
2571
- * Destroy the PlaySocket instance and disconnect
2572
- */
2573
- destroy() {
2574
- this.#initialized = false; // Set this immediately to prevent automatic reconnection
2575
-
2576
- if (this.#socket) {
2577
- // Signal to the server that it can immediately remove this user
2578
- if (this.#socket.readyState === WebSocket.OPEN) this.#sendToServer({ type: 'disconnect' });
2579
- this.#socket.close();
2580
- this.#socket.onmessage = null;
2581
- this.#socket.onerror = null;
2582
- this.#socket.onclose = null;
2583
- this.#socket = null;
2584
-
2585
- // Trigger events
2586
- this.#triggerEvent("status", "Destroyed.");
2587
- this.#triggerEvent("instanceDestroyed");
2588
- }
2589
-
2590
- // Reset state
2591
- clearTimeout(this.#reconnectTimeout);
2592
- this.#roomHost = null;
2593
- this.#inRoom = false;
2594
- this.#participantCount = 0;
2595
- this.#isReconnecting = false;
2596
- this.#reconnectCount = 0;
2597
- this.#roomVersion = 0;
2598
-
2599
- // Reject timeouts if currently active
2600
- if (this.#pendingJoin) this.#pendingJoin.reject();
2601
- if (this.#pendingCreate) this.#pendingCreate.reject();
2602
- if (this.#pendingRegistration) this.#pendingRegistration.reject();
2603
- if (this.#pendingConnect) this.#pendingConnect.reject();
2604
- if (this.#pendingReconnect) this.#pendingReconnect.reject();
2605
- }
2606
-
2607
- // Public getters
2608
- get participantCount() { return this.#participantCount; }
2609
- get storage() { return this.#crdtManager.propertyStore; }
2610
- get isHost() { return this.#id == this.#roomHost; }
2611
- get id() { return this.#id; }
2612
- }
2613
-
2614
- export { PlaySocket as default };