playsocketjs 2.5.2 → 2.5.3

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