playsocketjs 1.0.14 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/playsocket-client.js +2104 -121
- package/dist/playsocket-server.js +2215 -127
- package/package.json +5 -2
|
@@ -1,7 +1,2065 @@
|
|
|
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
|
+
constructor(type, data) {
|
|
174
|
+
this.type = type;
|
|
175
|
+
this.data = data;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class DecodeError extends Error {
|
|
180
|
+
constructor(message) {
|
|
181
|
+
super(message);
|
|
182
|
+
// fix the prototype chain in a cross-platform way
|
|
183
|
+
const proto = Object.create(DecodeError.prototype);
|
|
184
|
+
Object.setPrototypeOf(this, proto);
|
|
185
|
+
Object.defineProperty(this, "name", {
|
|
186
|
+
configurable: true,
|
|
187
|
+
enumerable: false,
|
|
188
|
+
value: DecodeError.name,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Integer Utility
|
|
194
|
+
const UINT32_MAX = 4294967295;
|
|
195
|
+
// DataView extension to handle int64 / uint64,
|
|
196
|
+
// where the actual range is 53-bits integer (a.k.a. safe integer)
|
|
197
|
+
function setUint64(view, offset, value) {
|
|
198
|
+
const high = value / 4294967296;
|
|
199
|
+
const low = value; // high bits are truncated by DataView
|
|
200
|
+
view.setUint32(offset, high);
|
|
201
|
+
view.setUint32(offset + 4, low);
|
|
202
|
+
}
|
|
203
|
+
function setInt64(view, offset, value) {
|
|
204
|
+
const high = Math.floor(value / 4294967296);
|
|
205
|
+
const low = value; // high bits are truncated by DataView
|
|
206
|
+
view.setUint32(offset, high);
|
|
207
|
+
view.setUint32(offset + 4, low);
|
|
208
|
+
}
|
|
209
|
+
function getInt64(view, offset) {
|
|
210
|
+
const high = view.getInt32(offset);
|
|
211
|
+
const low = view.getUint32(offset + 4);
|
|
212
|
+
return high * 4294967296 + low;
|
|
213
|
+
}
|
|
214
|
+
function getUint64(view, offset) {
|
|
215
|
+
const high = view.getUint32(offset);
|
|
216
|
+
const low = view.getUint32(offset + 4);
|
|
217
|
+
return high * 4294967296 + low;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
|
|
221
|
+
const EXT_TIMESTAMP = -1;
|
|
222
|
+
const TIMESTAMP32_MAX_SEC = 0x100000000 - 1; // 32-bit unsigned int
|
|
223
|
+
const TIMESTAMP64_MAX_SEC = 0x400000000 - 1; // 34-bit unsigned int
|
|
224
|
+
function encodeTimeSpecToTimestamp({ sec, nsec }) {
|
|
225
|
+
if (sec >= 0 && nsec >= 0 && sec <= TIMESTAMP64_MAX_SEC) {
|
|
226
|
+
// Here sec >= 0 && nsec >= 0
|
|
227
|
+
if (nsec === 0 && sec <= TIMESTAMP32_MAX_SEC) {
|
|
228
|
+
// timestamp 32 = { sec32 (unsigned) }
|
|
229
|
+
const rv = new Uint8Array(4);
|
|
230
|
+
const view = new DataView(rv.buffer);
|
|
231
|
+
view.setUint32(0, sec);
|
|
232
|
+
return rv;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// timestamp 64 = { nsec30 (unsigned), sec34 (unsigned) }
|
|
236
|
+
const secHigh = sec / 0x100000000;
|
|
237
|
+
const secLow = sec & 0xffffffff;
|
|
238
|
+
const rv = new Uint8Array(8);
|
|
239
|
+
const view = new DataView(rv.buffer);
|
|
240
|
+
// nsec30 | secHigh2
|
|
241
|
+
view.setUint32(0, (nsec << 2) | (secHigh & 0x3));
|
|
242
|
+
// secLow32
|
|
243
|
+
view.setUint32(4, secLow);
|
|
244
|
+
return rv;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// timestamp 96 = { nsec32 (unsigned), sec64 (signed) }
|
|
249
|
+
const rv = new Uint8Array(12);
|
|
250
|
+
const view = new DataView(rv.buffer);
|
|
251
|
+
view.setUint32(0, nsec);
|
|
252
|
+
setInt64(view, 4, sec);
|
|
253
|
+
return rv;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function encodeDateToTimeSpec(date) {
|
|
257
|
+
const msec = date.getTime();
|
|
258
|
+
const sec = Math.floor(msec / 1e3);
|
|
259
|
+
const nsec = (msec - sec * 1e3) * 1e6;
|
|
260
|
+
// Normalizes { sec, nsec } to ensure nsec is unsigned.
|
|
261
|
+
const nsecInSec = Math.floor(nsec / 1e9);
|
|
262
|
+
return {
|
|
263
|
+
sec: sec + nsecInSec,
|
|
264
|
+
nsec: nsec - nsecInSec * 1e9,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function encodeTimestampExtension(object) {
|
|
268
|
+
if (object instanceof Date) {
|
|
269
|
+
const timeSpec = encodeDateToTimeSpec(object);
|
|
270
|
+
return encodeTimeSpecToTimestamp(timeSpec);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function decodeTimestampToTimeSpec(data) {
|
|
277
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
278
|
+
// data may be 32, 64, or 96 bits
|
|
279
|
+
switch (data.byteLength) {
|
|
280
|
+
case 4: {
|
|
281
|
+
// timestamp 32 = { sec32 }
|
|
282
|
+
const sec = view.getUint32(0);
|
|
283
|
+
const nsec = 0;
|
|
284
|
+
return { sec, nsec };
|
|
285
|
+
}
|
|
286
|
+
case 8: {
|
|
287
|
+
// timestamp 64 = { nsec30, sec34 }
|
|
288
|
+
const nsec30AndSecHigh2 = view.getUint32(0);
|
|
289
|
+
const secLow32 = view.getUint32(4);
|
|
290
|
+
const sec = (nsec30AndSecHigh2 & 0x3) * 0x100000000 + secLow32;
|
|
291
|
+
const nsec = nsec30AndSecHigh2 >>> 2;
|
|
292
|
+
return { sec, nsec };
|
|
293
|
+
}
|
|
294
|
+
case 12: {
|
|
295
|
+
// timestamp 96 = { nsec32 (unsigned), sec64 (signed) }
|
|
296
|
+
const sec = getInt64(view, 4);
|
|
297
|
+
const nsec = view.getUint32(0);
|
|
298
|
+
return { sec, nsec };
|
|
299
|
+
}
|
|
300
|
+
default:
|
|
301
|
+
throw new DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${data.length}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function decodeTimestampExtension(data) {
|
|
305
|
+
const timeSpec = decodeTimestampToTimeSpec(data);
|
|
306
|
+
return new Date(timeSpec.sec * 1e3 + timeSpec.nsec / 1e6);
|
|
307
|
+
}
|
|
308
|
+
const timestampExtension = {
|
|
309
|
+
type: EXT_TIMESTAMP,
|
|
310
|
+
encode: encodeTimestampExtension,
|
|
311
|
+
decode: decodeTimestampExtension,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// ExtensionCodec to handle MessagePack extensions
|
|
315
|
+
class ExtensionCodec {
|
|
316
|
+
constructor() {
|
|
317
|
+
// built-in extensions
|
|
318
|
+
this.builtInEncoders = [];
|
|
319
|
+
this.builtInDecoders = [];
|
|
320
|
+
// custom extensions
|
|
321
|
+
this.encoders = [];
|
|
322
|
+
this.decoders = [];
|
|
323
|
+
this.register(timestampExtension);
|
|
324
|
+
}
|
|
325
|
+
register({ type, encode, decode, }) {
|
|
326
|
+
if (type >= 0) {
|
|
327
|
+
// custom extensions
|
|
328
|
+
this.encoders[type] = encode;
|
|
329
|
+
this.decoders[type] = decode;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// built-in extensions
|
|
333
|
+
const index = -1 - type;
|
|
334
|
+
this.builtInEncoders[index] = encode;
|
|
335
|
+
this.builtInDecoders[index] = decode;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
tryToEncode(object, context) {
|
|
339
|
+
// built-in extensions
|
|
340
|
+
for (let i = 0; i < this.builtInEncoders.length; i++) {
|
|
341
|
+
const encodeExt = this.builtInEncoders[i];
|
|
342
|
+
if (encodeExt != null) {
|
|
343
|
+
const data = encodeExt(object, context);
|
|
344
|
+
if (data != null) {
|
|
345
|
+
const type = -1 - i;
|
|
346
|
+
return new ExtData(type, data);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// custom extensions
|
|
351
|
+
for (let i = 0; i < this.encoders.length; i++) {
|
|
352
|
+
const encodeExt = this.encoders[i];
|
|
353
|
+
if (encodeExt != null) {
|
|
354
|
+
const data = encodeExt(object, context);
|
|
355
|
+
if (data != null) {
|
|
356
|
+
const type = i;
|
|
357
|
+
return new ExtData(type, data);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (object instanceof ExtData) {
|
|
362
|
+
// to keep ExtData as is
|
|
363
|
+
return object;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
decode(data, type, context) {
|
|
368
|
+
const decodeExt = type < 0 ? this.builtInDecoders[-1 - type] : this.decoders[type];
|
|
369
|
+
if (decodeExt) {
|
|
370
|
+
return decodeExt(data, type, context);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// decode() does not fail, returns ExtData instead.
|
|
374
|
+
return new ExtData(type, data);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
ExtensionCodec.defaultCodec = new ExtensionCodec();
|
|
379
|
+
|
|
380
|
+
function isArrayBufferLike(buffer) {
|
|
381
|
+
return (buffer instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer));
|
|
382
|
+
}
|
|
383
|
+
function ensureUint8Array(buffer) {
|
|
384
|
+
if (buffer instanceof Uint8Array) {
|
|
385
|
+
return buffer;
|
|
386
|
+
}
|
|
387
|
+
else if (ArrayBuffer.isView(buffer)) {
|
|
388
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
389
|
+
}
|
|
390
|
+
else if (isArrayBufferLike(buffer)) {
|
|
391
|
+
return new Uint8Array(buffer);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
// ArrayLike<number>
|
|
395
|
+
return Uint8Array.from(buffer);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const DEFAULT_MAX_DEPTH = 100;
|
|
400
|
+
const DEFAULT_INITIAL_BUFFER_SIZE = 2048;
|
|
401
|
+
class Encoder {
|
|
402
|
+
constructor(options) {
|
|
403
|
+
this.entered = false;
|
|
404
|
+
this.extensionCodec = options?.extensionCodec ?? ExtensionCodec.defaultCodec;
|
|
405
|
+
this.context = options?.context; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
|
|
406
|
+
this.useBigInt64 = options?.useBigInt64 ?? false;
|
|
407
|
+
this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
408
|
+
this.initialBufferSize = options?.initialBufferSize ?? DEFAULT_INITIAL_BUFFER_SIZE;
|
|
409
|
+
this.sortKeys = options?.sortKeys ?? false;
|
|
410
|
+
this.forceFloat32 = options?.forceFloat32 ?? false;
|
|
411
|
+
this.ignoreUndefined = options?.ignoreUndefined ?? false;
|
|
412
|
+
this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false;
|
|
413
|
+
this.pos = 0;
|
|
414
|
+
this.view = new DataView(new ArrayBuffer(this.initialBufferSize));
|
|
415
|
+
this.bytes = new Uint8Array(this.view.buffer);
|
|
416
|
+
}
|
|
417
|
+
clone() {
|
|
418
|
+
// Because of slightly special argument `context`,
|
|
419
|
+
// type assertion is needed.
|
|
420
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
421
|
+
return new Encoder({
|
|
422
|
+
extensionCodec: this.extensionCodec,
|
|
423
|
+
context: this.context,
|
|
424
|
+
useBigInt64: this.useBigInt64,
|
|
425
|
+
maxDepth: this.maxDepth,
|
|
426
|
+
initialBufferSize: this.initialBufferSize,
|
|
427
|
+
sortKeys: this.sortKeys,
|
|
428
|
+
forceFloat32: this.forceFloat32,
|
|
429
|
+
ignoreUndefined: this.ignoreUndefined,
|
|
430
|
+
forceIntegerToFloat: this.forceIntegerToFloat,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
reinitializeState() {
|
|
434
|
+
this.pos = 0;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 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}.
|
|
438
|
+
*
|
|
439
|
+
* @returns Encodes the object and returns a shared reference the encoder's internal buffer.
|
|
440
|
+
*/
|
|
441
|
+
encodeSharedRef(object) {
|
|
442
|
+
if (this.entered) {
|
|
443
|
+
const instance = this.clone();
|
|
444
|
+
return instance.encodeSharedRef(object);
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
this.entered = true;
|
|
448
|
+
this.reinitializeState();
|
|
449
|
+
this.doEncode(object, 1);
|
|
450
|
+
return this.bytes.subarray(0, this.pos);
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
this.entered = false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* @returns Encodes the object and returns a copy of the encoder's internal buffer.
|
|
458
|
+
*/
|
|
459
|
+
encode(object) {
|
|
460
|
+
if (this.entered) {
|
|
461
|
+
const instance = this.clone();
|
|
462
|
+
return instance.encode(object);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
this.entered = true;
|
|
466
|
+
this.reinitializeState();
|
|
467
|
+
this.doEncode(object, 1);
|
|
468
|
+
return this.bytes.slice(0, this.pos);
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
this.entered = false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
doEncode(object, depth) {
|
|
475
|
+
if (depth > this.maxDepth) {
|
|
476
|
+
throw new Error(`Too deep objects in depth ${depth}`);
|
|
477
|
+
}
|
|
478
|
+
if (object == null) {
|
|
479
|
+
this.encodeNil();
|
|
480
|
+
}
|
|
481
|
+
else if (typeof object === "boolean") {
|
|
482
|
+
this.encodeBoolean(object);
|
|
483
|
+
}
|
|
484
|
+
else if (typeof object === "number") {
|
|
485
|
+
if (!this.forceIntegerToFloat) {
|
|
486
|
+
this.encodeNumber(object);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
this.encodeNumberAsFloat(object);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else if (typeof object === "string") {
|
|
493
|
+
this.encodeString(object);
|
|
494
|
+
}
|
|
495
|
+
else if (this.useBigInt64 && typeof object === "bigint") {
|
|
496
|
+
this.encodeBigInt64(object);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
this.encodeObject(object, depth);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
ensureBufferSizeToWrite(sizeToWrite) {
|
|
503
|
+
const requiredSize = this.pos + sizeToWrite;
|
|
504
|
+
if (this.view.byteLength < requiredSize) {
|
|
505
|
+
this.resizeBuffer(requiredSize * 2);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
resizeBuffer(newSize) {
|
|
509
|
+
const newBuffer = new ArrayBuffer(newSize);
|
|
510
|
+
const newBytes = new Uint8Array(newBuffer);
|
|
511
|
+
const newView = new DataView(newBuffer);
|
|
512
|
+
newBytes.set(this.bytes);
|
|
513
|
+
this.view = newView;
|
|
514
|
+
this.bytes = newBytes;
|
|
515
|
+
}
|
|
516
|
+
encodeNil() {
|
|
517
|
+
this.writeU8(0xc0);
|
|
518
|
+
}
|
|
519
|
+
encodeBoolean(object) {
|
|
520
|
+
if (object === false) {
|
|
521
|
+
this.writeU8(0xc2);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
this.writeU8(0xc3);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
encodeNumber(object) {
|
|
528
|
+
if (!this.forceIntegerToFloat && Number.isSafeInteger(object)) {
|
|
529
|
+
if (object >= 0) {
|
|
530
|
+
if (object < 0x80) {
|
|
531
|
+
// positive fixint
|
|
532
|
+
this.writeU8(object);
|
|
533
|
+
}
|
|
534
|
+
else if (object < 0x100) {
|
|
535
|
+
// uint 8
|
|
536
|
+
this.writeU8(0xcc);
|
|
537
|
+
this.writeU8(object);
|
|
538
|
+
}
|
|
539
|
+
else if (object < 0x10000) {
|
|
540
|
+
// uint 16
|
|
541
|
+
this.writeU8(0xcd);
|
|
542
|
+
this.writeU16(object);
|
|
543
|
+
}
|
|
544
|
+
else if (object < 0x100000000) {
|
|
545
|
+
// uint 32
|
|
546
|
+
this.writeU8(0xce);
|
|
547
|
+
this.writeU32(object);
|
|
548
|
+
}
|
|
549
|
+
else if (!this.useBigInt64) {
|
|
550
|
+
// uint 64
|
|
551
|
+
this.writeU8(0xcf);
|
|
552
|
+
this.writeU64(object);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
this.encodeNumberAsFloat(object);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
if (object >= -32) {
|
|
560
|
+
// negative fixint
|
|
561
|
+
this.writeU8(0xe0 | (object + 0x20));
|
|
562
|
+
}
|
|
563
|
+
else if (object >= -128) {
|
|
564
|
+
// int 8
|
|
565
|
+
this.writeU8(0xd0);
|
|
566
|
+
this.writeI8(object);
|
|
567
|
+
}
|
|
568
|
+
else if (object >= -32768) {
|
|
569
|
+
// int 16
|
|
570
|
+
this.writeU8(0xd1);
|
|
571
|
+
this.writeI16(object);
|
|
572
|
+
}
|
|
573
|
+
else if (object >= -2147483648) {
|
|
574
|
+
// int 32
|
|
575
|
+
this.writeU8(0xd2);
|
|
576
|
+
this.writeI32(object);
|
|
577
|
+
}
|
|
578
|
+
else if (!this.useBigInt64) {
|
|
579
|
+
// int 64
|
|
580
|
+
this.writeU8(0xd3);
|
|
581
|
+
this.writeI64(object);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
this.encodeNumberAsFloat(object);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
this.encodeNumberAsFloat(object);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
encodeNumberAsFloat(object) {
|
|
593
|
+
if (this.forceFloat32) {
|
|
594
|
+
// float 32
|
|
595
|
+
this.writeU8(0xca);
|
|
596
|
+
this.writeF32(object);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// float 64
|
|
600
|
+
this.writeU8(0xcb);
|
|
601
|
+
this.writeF64(object);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
encodeBigInt64(object) {
|
|
605
|
+
if (object >= BigInt(0)) {
|
|
606
|
+
// uint 64
|
|
607
|
+
this.writeU8(0xcf);
|
|
608
|
+
this.writeBigUint64(object);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// int 64
|
|
612
|
+
this.writeU8(0xd3);
|
|
613
|
+
this.writeBigInt64(object);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
writeStringHeader(byteLength) {
|
|
617
|
+
if (byteLength < 32) {
|
|
618
|
+
// fixstr
|
|
619
|
+
this.writeU8(0xa0 + byteLength);
|
|
620
|
+
}
|
|
621
|
+
else if (byteLength < 0x100) {
|
|
622
|
+
// str 8
|
|
623
|
+
this.writeU8(0xd9);
|
|
624
|
+
this.writeU8(byteLength);
|
|
625
|
+
}
|
|
626
|
+
else if (byteLength < 0x10000) {
|
|
627
|
+
// str 16
|
|
628
|
+
this.writeU8(0xda);
|
|
629
|
+
this.writeU16(byteLength);
|
|
630
|
+
}
|
|
631
|
+
else if (byteLength < 0x100000000) {
|
|
632
|
+
// str 32
|
|
633
|
+
this.writeU8(0xdb);
|
|
634
|
+
this.writeU32(byteLength);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
throw new Error(`Too long string: ${byteLength} bytes in UTF-8`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
encodeString(object) {
|
|
641
|
+
const maxHeaderSize = 1 + 4;
|
|
642
|
+
const byteLength = utf8Count(object);
|
|
643
|
+
this.ensureBufferSizeToWrite(maxHeaderSize + byteLength);
|
|
644
|
+
this.writeStringHeader(byteLength);
|
|
645
|
+
utf8Encode(object, this.bytes, this.pos);
|
|
646
|
+
this.pos += byteLength;
|
|
647
|
+
}
|
|
648
|
+
encodeObject(object, depth) {
|
|
649
|
+
// try to encode objects with custom codec first of non-primitives
|
|
650
|
+
const ext = this.extensionCodec.tryToEncode(object, this.context);
|
|
651
|
+
if (ext != null) {
|
|
652
|
+
this.encodeExtension(ext);
|
|
653
|
+
}
|
|
654
|
+
else if (Array.isArray(object)) {
|
|
655
|
+
this.encodeArray(object, depth);
|
|
656
|
+
}
|
|
657
|
+
else if (ArrayBuffer.isView(object)) {
|
|
658
|
+
this.encodeBinary(object);
|
|
659
|
+
}
|
|
660
|
+
else if (typeof object === "object") {
|
|
661
|
+
this.encodeMap(object, depth);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
// symbol, function and other special object come here unless extensionCodec handles them.
|
|
665
|
+
throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(object)}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
encodeBinary(object) {
|
|
669
|
+
const size = object.byteLength;
|
|
670
|
+
if (size < 0x100) {
|
|
671
|
+
// bin 8
|
|
672
|
+
this.writeU8(0xc4);
|
|
673
|
+
this.writeU8(size);
|
|
674
|
+
}
|
|
675
|
+
else if (size < 0x10000) {
|
|
676
|
+
// bin 16
|
|
677
|
+
this.writeU8(0xc5);
|
|
678
|
+
this.writeU16(size);
|
|
679
|
+
}
|
|
680
|
+
else if (size < 0x100000000) {
|
|
681
|
+
// bin 32
|
|
682
|
+
this.writeU8(0xc6);
|
|
683
|
+
this.writeU32(size);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
throw new Error(`Too large binary: ${size}`);
|
|
687
|
+
}
|
|
688
|
+
const bytes = ensureUint8Array(object);
|
|
689
|
+
this.writeU8a(bytes);
|
|
690
|
+
}
|
|
691
|
+
encodeArray(object, depth) {
|
|
692
|
+
const size = object.length;
|
|
693
|
+
if (size < 16) {
|
|
694
|
+
// fixarray
|
|
695
|
+
this.writeU8(0x90 + size);
|
|
696
|
+
}
|
|
697
|
+
else if (size < 0x10000) {
|
|
698
|
+
// array 16
|
|
699
|
+
this.writeU8(0xdc);
|
|
700
|
+
this.writeU16(size);
|
|
701
|
+
}
|
|
702
|
+
else if (size < 0x100000000) {
|
|
703
|
+
// array 32
|
|
704
|
+
this.writeU8(0xdd);
|
|
705
|
+
this.writeU32(size);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
throw new Error(`Too large array: ${size}`);
|
|
709
|
+
}
|
|
710
|
+
for (const item of object) {
|
|
711
|
+
this.doEncode(item, depth + 1);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
countWithoutUndefined(object, keys) {
|
|
715
|
+
let count = 0;
|
|
716
|
+
for (const key of keys) {
|
|
717
|
+
if (object[key] !== undefined) {
|
|
718
|
+
count++;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return count;
|
|
722
|
+
}
|
|
723
|
+
encodeMap(object, depth) {
|
|
724
|
+
const keys = Object.keys(object);
|
|
725
|
+
if (this.sortKeys) {
|
|
726
|
+
keys.sort();
|
|
727
|
+
}
|
|
728
|
+
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length;
|
|
729
|
+
if (size < 16) {
|
|
730
|
+
// fixmap
|
|
731
|
+
this.writeU8(0x80 + size);
|
|
732
|
+
}
|
|
733
|
+
else if (size < 0x10000) {
|
|
734
|
+
// map 16
|
|
735
|
+
this.writeU8(0xde);
|
|
736
|
+
this.writeU16(size);
|
|
737
|
+
}
|
|
738
|
+
else if (size < 0x100000000) {
|
|
739
|
+
// map 32
|
|
740
|
+
this.writeU8(0xdf);
|
|
741
|
+
this.writeU32(size);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
throw new Error(`Too large map object: ${size}`);
|
|
745
|
+
}
|
|
746
|
+
for (const key of keys) {
|
|
747
|
+
const value = object[key];
|
|
748
|
+
if (!(this.ignoreUndefined && value === undefined)) {
|
|
749
|
+
this.encodeString(key);
|
|
750
|
+
this.doEncode(value, depth + 1);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
encodeExtension(ext) {
|
|
755
|
+
if (typeof ext.data === "function") {
|
|
756
|
+
const data = ext.data(this.pos + 6);
|
|
757
|
+
const size = data.length;
|
|
758
|
+
if (size >= 0x100000000) {
|
|
759
|
+
throw new Error(`Too large extension object: ${size}`);
|
|
760
|
+
}
|
|
761
|
+
this.writeU8(0xc9);
|
|
762
|
+
this.writeU32(size);
|
|
763
|
+
this.writeI8(ext.type);
|
|
764
|
+
this.writeU8a(data);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const size = ext.data.length;
|
|
768
|
+
if (size === 1) {
|
|
769
|
+
// fixext 1
|
|
770
|
+
this.writeU8(0xd4);
|
|
771
|
+
}
|
|
772
|
+
else if (size === 2) {
|
|
773
|
+
// fixext 2
|
|
774
|
+
this.writeU8(0xd5);
|
|
775
|
+
}
|
|
776
|
+
else if (size === 4) {
|
|
777
|
+
// fixext 4
|
|
778
|
+
this.writeU8(0xd6);
|
|
779
|
+
}
|
|
780
|
+
else if (size === 8) {
|
|
781
|
+
// fixext 8
|
|
782
|
+
this.writeU8(0xd7);
|
|
783
|
+
}
|
|
784
|
+
else if (size === 16) {
|
|
785
|
+
// fixext 16
|
|
786
|
+
this.writeU8(0xd8);
|
|
787
|
+
}
|
|
788
|
+
else if (size < 0x100) {
|
|
789
|
+
// ext 8
|
|
790
|
+
this.writeU8(0xc7);
|
|
791
|
+
this.writeU8(size);
|
|
792
|
+
}
|
|
793
|
+
else if (size < 0x10000) {
|
|
794
|
+
// ext 16
|
|
795
|
+
this.writeU8(0xc8);
|
|
796
|
+
this.writeU16(size);
|
|
797
|
+
}
|
|
798
|
+
else if (size < 0x100000000) {
|
|
799
|
+
// ext 32
|
|
800
|
+
this.writeU8(0xc9);
|
|
801
|
+
this.writeU32(size);
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
throw new Error(`Too large extension object: ${size}`);
|
|
805
|
+
}
|
|
806
|
+
this.writeI8(ext.type);
|
|
807
|
+
this.writeU8a(ext.data);
|
|
808
|
+
}
|
|
809
|
+
writeU8(value) {
|
|
810
|
+
this.ensureBufferSizeToWrite(1);
|
|
811
|
+
this.view.setUint8(this.pos, value);
|
|
812
|
+
this.pos++;
|
|
813
|
+
}
|
|
814
|
+
writeU8a(values) {
|
|
815
|
+
const size = values.length;
|
|
816
|
+
this.ensureBufferSizeToWrite(size);
|
|
817
|
+
this.bytes.set(values, this.pos);
|
|
818
|
+
this.pos += size;
|
|
819
|
+
}
|
|
820
|
+
writeI8(value) {
|
|
821
|
+
this.ensureBufferSizeToWrite(1);
|
|
822
|
+
this.view.setInt8(this.pos, value);
|
|
823
|
+
this.pos++;
|
|
824
|
+
}
|
|
825
|
+
writeU16(value) {
|
|
826
|
+
this.ensureBufferSizeToWrite(2);
|
|
827
|
+
this.view.setUint16(this.pos, value);
|
|
828
|
+
this.pos += 2;
|
|
829
|
+
}
|
|
830
|
+
writeI16(value) {
|
|
831
|
+
this.ensureBufferSizeToWrite(2);
|
|
832
|
+
this.view.setInt16(this.pos, value);
|
|
833
|
+
this.pos += 2;
|
|
834
|
+
}
|
|
835
|
+
writeU32(value) {
|
|
836
|
+
this.ensureBufferSizeToWrite(4);
|
|
837
|
+
this.view.setUint32(this.pos, value);
|
|
838
|
+
this.pos += 4;
|
|
839
|
+
}
|
|
840
|
+
writeI32(value) {
|
|
841
|
+
this.ensureBufferSizeToWrite(4);
|
|
842
|
+
this.view.setInt32(this.pos, value);
|
|
843
|
+
this.pos += 4;
|
|
844
|
+
}
|
|
845
|
+
writeF32(value) {
|
|
846
|
+
this.ensureBufferSizeToWrite(4);
|
|
847
|
+
this.view.setFloat32(this.pos, value);
|
|
848
|
+
this.pos += 4;
|
|
849
|
+
}
|
|
850
|
+
writeF64(value) {
|
|
851
|
+
this.ensureBufferSizeToWrite(8);
|
|
852
|
+
this.view.setFloat64(this.pos, value);
|
|
853
|
+
this.pos += 8;
|
|
854
|
+
}
|
|
855
|
+
writeU64(value) {
|
|
856
|
+
this.ensureBufferSizeToWrite(8);
|
|
857
|
+
setUint64(this.view, this.pos, value);
|
|
858
|
+
this.pos += 8;
|
|
859
|
+
}
|
|
860
|
+
writeI64(value) {
|
|
861
|
+
this.ensureBufferSizeToWrite(8);
|
|
862
|
+
setInt64(this.view, this.pos, value);
|
|
863
|
+
this.pos += 8;
|
|
864
|
+
}
|
|
865
|
+
writeBigUint64(value) {
|
|
866
|
+
this.ensureBufferSizeToWrite(8);
|
|
867
|
+
this.view.setBigUint64(this.pos, value);
|
|
868
|
+
this.pos += 8;
|
|
869
|
+
}
|
|
870
|
+
writeBigInt64(value) {
|
|
871
|
+
this.ensureBufferSizeToWrite(8);
|
|
872
|
+
this.view.setBigInt64(this.pos, value);
|
|
873
|
+
this.pos += 8;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* It encodes `value` in the MessagePack format and
|
|
879
|
+
* returns a byte buffer.
|
|
880
|
+
*
|
|
881
|
+
* 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`.
|
|
882
|
+
*/
|
|
883
|
+
function encode(value, options) {
|
|
884
|
+
const encoder = new Encoder(options);
|
|
885
|
+
return encoder.encodeSharedRef(value);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function prettyByte(byte) {
|
|
889
|
+
return `${byte < 0 ? "-" : ""}0x${Math.abs(byte).toString(16).padStart(2, "0")}`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const DEFAULT_MAX_KEY_LENGTH = 16;
|
|
893
|
+
const DEFAULT_MAX_LENGTH_PER_KEY = 16;
|
|
894
|
+
class CachedKeyDecoder {
|
|
895
|
+
constructor(maxKeyLength = DEFAULT_MAX_KEY_LENGTH, maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY) {
|
|
896
|
+
this.hit = 0;
|
|
897
|
+
this.miss = 0;
|
|
898
|
+
this.maxKeyLength = maxKeyLength;
|
|
899
|
+
this.maxLengthPerKey = maxLengthPerKey;
|
|
900
|
+
// avoid `new Array(N)`, which makes a sparse array,
|
|
901
|
+
// because a sparse array is typically slower than a non-sparse array.
|
|
902
|
+
this.caches = [];
|
|
903
|
+
for (let i = 0; i < this.maxKeyLength; i++) {
|
|
904
|
+
this.caches.push([]);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
canBeCached(byteLength) {
|
|
908
|
+
return byteLength > 0 && byteLength <= this.maxKeyLength;
|
|
909
|
+
}
|
|
910
|
+
find(bytes, inputOffset, byteLength) {
|
|
911
|
+
const records = this.caches[byteLength - 1];
|
|
912
|
+
FIND_CHUNK: for (const record of records) {
|
|
913
|
+
const recordBytes = record.bytes;
|
|
914
|
+
for (let j = 0; j < byteLength; j++) {
|
|
915
|
+
if (recordBytes[j] !== bytes[inputOffset + j]) {
|
|
916
|
+
continue FIND_CHUNK;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return record.str;
|
|
920
|
+
}
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
store(bytes, value) {
|
|
924
|
+
const records = this.caches[bytes.length - 1];
|
|
925
|
+
const record = { bytes, str: value };
|
|
926
|
+
if (records.length >= this.maxLengthPerKey) {
|
|
927
|
+
// `records` are full!
|
|
928
|
+
// Set `record` to an arbitrary position.
|
|
929
|
+
records[(Math.random() * records.length) | 0] = record;
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
records.push(record);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
decode(bytes, inputOffset, byteLength) {
|
|
936
|
+
const cachedValue = this.find(bytes, inputOffset, byteLength);
|
|
937
|
+
if (cachedValue != null) {
|
|
938
|
+
this.hit++;
|
|
939
|
+
return cachedValue;
|
|
940
|
+
}
|
|
941
|
+
this.miss++;
|
|
942
|
+
const str = utf8DecodeJs(bytes, inputOffset, byteLength);
|
|
943
|
+
// 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.
|
|
944
|
+
const slicedCopyOfBytes = Uint8Array.prototype.slice.call(bytes, inputOffset, inputOffset + byteLength);
|
|
945
|
+
this.store(slicedCopyOfBytes, str);
|
|
946
|
+
return str;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const STATE_ARRAY = "array";
|
|
951
|
+
const STATE_MAP_KEY = "map_key";
|
|
952
|
+
const STATE_MAP_VALUE = "map_value";
|
|
953
|
+
const mapKeyConverter = (key) => {
|
|
954
|
+
if (typeof key === "string" || typeof key === "number") {
|
|
955
|
+
return key;
|
|
956
|
+
}
|
|
957
|
+
throw new DecodeError("The type of key must be string or number but " + typeof key);
|
|
958
|
+
};
|
|
959
|
+
class StackPool {
|
|
960
|
+
constructor() {
|
|
961
|
+
this.stack = [];
|
|
962
|
+
this.stackHeadPosition = -1;
|
|
963
|
+
}
|
|
964
|
+
get length() {
|
|
965
|
+
return this.stackHeadPosition + 1;
|
|
966
|
+
}
|
|
967
|
+
top() {
|
|
968
|
+
return this.stack[this.stackHeadPosition];
|
|
969
|
+
}
|
|
970
|
+
pushArrayState(size) {
|
|
971
|
+
const state = this.getUninitializedStateFromPool();
|
|
972
|
+
state.type = STATE_ARRAY;
|
|
973
|
+
state.position = 0;
|
|
974
|
+
state.size = size;
|
|
975
|
+
state.array = new Array(size);
|
|
976
|
+
}
|
|
977
|
+
pushMapState(size) {
|
|
978
|
+
const state = this.getUninitializedStateFromPool();
|
|
979
|
+
state.type = STATE_MAP_KEY;
|
|
980
|
+
state.readCount = 0;
|
|
981
|
+
state.size = size;
|
|
982
|
+
state.map = {};
|
|
983
|
+
}
|
|
984
|
+
getUninitializedStateFromPool() {
|
|
985
|
+
this.stackHeadPosition++;
|
|
986
|
+
if (this.stackHeadPosition === this.stack.length) {
|
|
987
|
+
const partialState = {
|
|
988
|
+
type: undefined,
|
|
989
|
+
size: 0,
|
|
990
|
+
array: undefined,
|
|
991
|
+
position: 0,
|
|
992
|
+
readCount: 0,
|
|
993
|
+
map: undefined,
|
|
994
|
+
key: null,
|
|
995
|
+
};
|
|
996
|
+
this.stack.push(partialState);
|
|
997
|
+
}
|
|
998
|
+
return this.stack[this.stackHeadPosition];
|
|
999
|
+
}
|
|
1000
|
+
release(state) {
|
|
1001
|
+
const topStackState = this.stack[this.stackHeadPosition];
|
|
1002
|
+
if (topStackState !== state) {
|
|
1003
|
+
throw new Error("Invalid stack state. Released state is not on top of the stack.");
|
|
1004
|
+
}
|
|
1005
|
+
if (state.type === STATE_ARRAY) {
|
|
1006
|
+
const partialState = state;
|
|
1007
|
+
partialState.size = 0;
|
|
1008
|
+
partialState.array = undefined;
|
|
1009
|
+
partialState.position = 0;
|
|
1010
|
+
partialState.type = undefined;
|
|
1011
|
+
}
|
|
1012
|
+
if (state.type === STATE_MAP_KEY || state.type === STATE_MAP_VALUE) {
|
|
1013
|
+
const partialState = state;
|
|
1014
|
+
partialState.size = 0;
|
|
1015
|
+
partialState.map = undefined;
|
|
1016
|
+
partialState.readCount = 0;
|
|
1017
|
+
partialState.type = undefined;
|
|
1018
|
+
}
|
|
1019
|
+
this.stackHeadPosition--;
|
|
1020
|
+
}
|
|
1021
|
+
reset() {
|
|
1022
|
+
this.stack.length = 0;
|
|
1023
|
+
this.stackHeadPosition = -1;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const HEAD_BYTE_REQUIRED = -1;
|
|
1027
|
+
const EMPTY_VIEW = new DataView(new ArrayBuffer(0));
|
|
1028
|
+
const EMPTY_BYTES = new Uint8Array(EMPTY_VIEW.buffer);
|
|
1029
|
+
try {
|
|
1030
|
+
// IE11: The spec says it should throw RangeError,
|
|
1031
|
+
// IE11: but in IE11 it throws TypeError.
|
|
1032
|
+
EMPTY_VIEW.getInt8(0);
|
|
1033
|
+
}
|
|
1034
|
+
catch (e) {
|
|
1035
|
+
if (!(e instanceof RangeError)) {
|
|
1036
|
+
throw new Error("This module is not supported in the current JavaScript engine because DataView does not throw RangeError on out-of-bounds access");
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const MORE_DATA = new RangeError("Insufficient data");
|
|
1040
|
+
const sharedCachedKeyDecoder = new CachedKeyDecoder();
|
|
1041
|
+
class Decoder {
|
|
1042
|
+
constructor(options) {
|
|
1043
|
+
this.totalPos = 0;
|
|
1044
|
+
this.pos = 0;
|
|
1045
|
+
this.view = EMPTY_VIEW;
|
|
1046
|
+
this.bytes = EMPTY_BYTES;
|
|
1047
|
+
this.headByte = HEAD_BYTE_REQUIRED;
|
|
1048
|
+
this.stack = new StackPool();
|
|
1049
|
+
this.entered = false;
|
|
1050
|
+
this.extensionCodec = options?.extensionCodec ?? ExtensionCodec.defaultCodec;
|
|
1051
|
+
this.context = options?.context; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
|
|
1052
|
+
this.useBigInt64 = options?.useBigInt64 ?? false;
|
|
1053
|
+
this.rawStrings = options?.rawStrings ?? false;
|
|
1054
|
+
this.maxStrLength = options?.maxStrLength ?? UINT32_MAX;
|
|
1055
|
+
this.maxBinLength = options?.maxBinLength ?? UINT32_MAX;
|
|
1056
|
+
this.maxArrayLength = options?.maxArrayLength ?? UINT32_MAX;
|
|
1057
|
+
this.maxMapLength = options?.maxMapLength ?? UINT32_MAX;
|
|
1058
|
+
this.maxExtLength = options?.maxExtLength ?? UINT32_MAX;
|
|
1059
|
+
this.keyDecoder = options?.keyDecoder !== undefined ? options.keyDecoder : sharedCachedKeyDecoder;
|
|
1060
|
+
this.mapKeyConverter = options?.mapKeyConverter ?? mapKeyConverter;
|
|
1061
|
+
}
|
|
1062
|
+
clone() {
|
|
1063
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
1064
|
+
return new Decoder({
|
|
1065
|
+
extensionCodec: this.extensionCodec,
|
|
1066
|
+
context: this.context,
|
|
1067
|
+
useBigInt64: this.useBigInt64,
|
|
1068
|
+
rawStrings: this.rawStrings,
|
|
1069
|
+
maxStrLength: this.maxStrLength,
|
|
1070
|
+
maxBinLength: this.maxBinLength,
|
|
1071
|
+
maxArrayLength: this.maxArrayLength,
|
|
1072
|
+
maxMapLength: this.maxMapLength,
|
|
1073
|
+
maxExtLength: this.maxExtLength,
|
|
1074
|
+
keyDecoder: this.keyDecoder,
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
reinitializeState() {
|
|
1078
|
+
this.totalPos = 0;
|
|
1079
|
+
this.headByte = HEAD_BYTE_REQUIRED;
|
|
1080
|
+
this.stack.reset();
|
|
1081
|
+
// view, bytes, and pos will be re-initialized in setBuffer()
|
|
1082
|
+
}
|
|
1083
|
+
setBuffer(buffer) {
|
|
1084
|
+
const bytes = ensureUint8Array(buffer);
|
|
1085
|
+
this.bytes = bytes;
|
|
1086
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1087
|
+
this.pos = 0;
|
|
1088
|
+
}
|
|
1089
|
+
appendBuffer(buffer) {
|
|
1090
|
+
if (this.headByte === HEAD_BYTE_REQUIRED && !this.hasRemaining(1)) {
|
|
1091
|
+
this.setBuffer(buffer);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
const remainingData = this.bytes.subarray(this.pos);
|
|
1095
|
+
const newData = ensureUint8Array(buffer);
|
|
1096
|
+
// concat remainingData + newData
|
|
1097
|
+
const newBuffer = new Uint8Array(remainingData.length + newData.length);
|
|
1098
|
+
newBuffer.set(remainingData);
|
|
1099
|
+
newBuffer.set(newData, remainingData.length);
|
|
1100
|
+
this.setBuffer(newBuffer);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
hasRemaining(size) {
|
|
1104
|
+
return this.view.byteLength - this.pos >= size;
|
|
1105
|
+
}
|
|
1106
|
+
createExtraByteError(posToShow) {
|
|
1107
|
+
const { view, pos } = this;
|
|
1108
|
+
return new RangeError(`Extra ${view.byteLength - pos} of ${view.byteLength} byte(s) found at buffer[${posToShow}]`);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* @throws {@link DecodeError}
|
|
1112
|
+
* @throws {@link RangeError}
|
|
1113
|
+
*/
|
|
1114
|
+
decode(buffer) {
|
|
1115
|
+
if (this.entered) {
|
|
1116
|
+
const instance = this.clone();
|
|
1117
|
+
return instance.decode(buffer);
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
this.entered = true;
|
|
1121
|
+
this.reinitializeState();
|
|
1122
|
+
this.setBuffer(buffer);
|
|
1123
|
+
const object = this.doDecodeSync();
|
|
1124
|
+
if (this.hasRemaining(1)) {
|
|
1125
|
+
throw this.createExtraByteError(this.pos);
|
|
1126
|
+
}
|
|
1127
|
+
return object;
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
this.entered = false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
*decodeMulti(buffer) {
|
|
1134
|
+
if (this.entered) {
|
|
1135
|
+
const instance = this.clone();
|
|
1136
|
+
yield* instance.decodeMulti(buffer);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
this.entered = true;
|
|
1141
|
+
this.reinitializeState();
|
|
1142
|
+
this.setBuffer(buffer);
|
|
1143
|
+
while (this.hasRemaining(1)) {
|
|
1144
|
+
yield this.doDecodeSync();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
finally {
|
|
1148
|
+
this.entered = false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async decodeAsync(stream) {
|
|
1152
|
+
if (this.entered) {
|
|
1153
|
+
const instance = this.clone();
|
|
1154
|
+
return instance.decodeAsync(stream);
|
|
1155
|
+
}
|
|
1156
|
+
try {
|
|
1157
|
+
this.entered = true;
|
|
1158
|
+
let decoded = false;
|
|
1159
|
+
let object;
|
|
1160
|
+
for await (const buffer of stream) {
|
|
1161
|
+
if (decoded) {
|
|
1162
|
+
this.entered = false;
|
|
1163
|
+
throw this.createExtraByteError(this.totalPos);
|
|
1164
|
+
}
|
|
1165
|
+
this.appendBuffer(buffer);
|
|
1166
|
+
try {
|
|
1167
|
+
object = this.doDecodeSync();
|
|
1168
|
+
decoded = true;
|
|
1169
|
+
}
|
|
1170
|
+
catch (e) {
|
|
1171
|
+
if (!(e instanceof RangeError)) {
|
|
1172
|
+
throw e; // rethrow
|
|
1173
|
+
}
|
|
1174
|
+
// fallthrough
|
|
1175
|
+
}
|
|
1176
|
+
this.totalPos += this.pos;
|
|
1177
|
+
}
|
|
1178
|
+
if (decoded) {
|
|
1179
|
+
if (this.hasRemaining(1)) {
|
|
1180
|
+
throw this.createExtraByteError(this.totalPos);
|
|
1181
|
+
}
|
|
1182
|
+
return object;
|
|
1183
|
+
}
|
|
1184
|
+
const { headByte, pos, totalPos } = this;
|
|
1185
|
+
throw new RangeError(`Insufficient data in parsing ${prettyByte(headByte)} at ${totalPos} (${pos} in the current buffer)`);
|
|
1186
|
+
}
|
|
1187
|
+
finally {
|
|
1188
|
+
this.entered = false;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
decodeArrayStream(stream) {
|
|
1192
|
+
return this.decodeMultiAsync(stream, true);
|
|
1193
|
+
}
|
|
1194
|
+
decodeStream(stream) {
|
|
1195
|
+
return this.decodeMultiAsync(stream, false);
|
|
1196
|
+
}
|
|
1197
|
+
async *decodeMultiAsync(stream, isArray) {
|
|
1198
|
+
if (this.entered) {
|
|
1199
|
+
const instance = this.clone();
|
|
1200
|
+
yield* instance.decodeMultiAsync(stream, isArray);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
this.entered = true;
|
|
1205
|
+
let isArrayHeaderRequired = isArray;
|
|
1206
|
+
let arrayItemsLeft = -1;
|
|
1207
|
+
for await (const buffer of stream) {
|
|
1208
|
+
if (isArray && arrayItemsLeft === 0) {
|
|
1209
|
+
throw this.createExtraByteError(this.totalPos);
|
|
1210
|
+
}
|
|
1211
|
+
this.appendBuffer(buffer);
|
|
1212
|
+
if (isArrayHeaderRequired) {
|
|
1213
|
+
arrayItemsLeft = this.readArraySize();
|
|
1214
|
+
isArrayHeaderRequired = false;
|
|
1215
|
+
this.complete();
|
|
1216
|
+
}
|
|
1217
|
+
try {
|
|
1218
|
+
while (true) {
|
|
1219
|
+
yield this.doDecodeSync();
|
|
1220
|
+
if (--arrayItemsLeft === 0) {
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
catch (e) {
|
|
1226
|
+
if (!(e instanceof RangeError)) {
|
|
1227
|
+
throw e; // rethrow
|
|
1228
|
+
}
|
|
1229
|
+
// fallthrough
|
|
1230
|
+
}
|
|
1231
|
+
this.totalPos += this.pos;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
finally {
|
|
1235
|
+
this.entered = false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
doDecodeSync() {
|
|
1239
|
+
DECODE: while (true) {
|
|
1240
|
+
const headByte = this.readHeadByte();
|
|
1241
|
+
let object;
|
|
1242
|
+
if (headByte >= 0xe0) {
|
|
1243
|
+
// negative fixint (111x xxxx) 0xe0 - 0xff
|
|
1244
|
+
object = headByte - 0x100;
|
|
1245
|
+
}
|
|
1246
|
+
else if (headByte < 0xc0) {
|
|
1247
|
+
if (headByte < 0x80) {
|
|
1248
|
+
// positive fixint (0xxx xxxx) 0x00 - 0x7f
|
|
1249
|
+
object = headByte;
|
|
1250
|
+
}
|
|
1251
|
+
else if (headByte < 0x90) {
|
|
1252
|
+
// fixmap (1000 xxxx) 0x80 - 0x8f
|
|
1253
|
+
const size = headByte - 0x80;
|
|
1254
|
+
if (size !== 0) {
|
|
1255
|
+
this.pushMapState(size);
|
|
1256
|
+
this.complete();
|
|
1257
|
+
continue DECODE;
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
object = {};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
else if (headByte < 0xa0) {
|
|
1264
|
+
// fixarray (1001 xxxx) 0x90 - 0x9f
|
|
1265
|
+
const size = headByte - 0x90;
|
|
1266
|
+
if (size !== 0) {
|
|
1267
|
+
this.pushArrayState(size);
|
|
1268
|
+
this.complete();
|
|
1269
|
+
continue DECODE;
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
object = [];
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
// fixstr (101x xxxx) 0xa0 - 0xbf
|
|
1277
|
+
const byteLength = headByte - 0xa0;
|
|
1278
|
+
object = this.decodeString(byteLength, 0);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
else if (headByte === 0xc0) {
|
|
1282
|
+
// nil
|
|
1283
|
+
object = null;
|
|
1284
|
+
}
|
|
1285
|
+
else if (headByte === 0xc2) {
|
|
1286
|
+
// false
|
|
1287
|
+
object = false;
|
|
1288
|
+
}
|
|
1289
|
+
else if (headByte === 0xc3) {
|
|
1290
|
+
// true
|
|
1291
|
+
object = true;
|
|
1292
|
+
}
|
|
1293
|
+
else if (headByte === 0xca) {
|
|
1294
|
+
// float 32
|
|
1295
|
+
object = this.readF32();
|
|
1296
|
+
}
|
|
1297
|
+
else if (headByte === 0xcb) {
|
|
1298
|
+
// float 64
|
|
1299
|
+
object = this.readF64();
|
|
1300
|
+
}
|
|
1301
|
+
else if (headByte === 0xcc) {
|
|
1302
|
+
// uint 8
|
|
1303
|
+
object = this.readU8();
|
|
1304
|
+
}
|
|
1305
|
+
else if (headByte === 0xcd) {
|
|
1306
|
+
// uint 16
|
|
1307
|
+
object = this.readU16();
|
|
1308
|
+
}
|
|
1309
|
+
else if (headByte === 0xce) {
|
|
1310
|
+
// uint 32
|
|
1311
|
+
object = this.readU32();
|
|
1312
|
+
}
|
|
1313
|
+
else if (headByte === 0xcf) {
|
|
1314
|
+
// uint 64
|
|
1315
|
+
if (this.useBigInt64) {
|
|
1316
|
+
object = this.readU64AsBigInt();
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
object = this.readU64();
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
else if (headByte === 0xd0) {
|
|
1323
|
+
// int 8
|
|
1324
|
+
object = this.readI8();
|
|
1325
|
+
}
|
|
1326
|
+
else if (headByte === 0xd1) {
|
|
1327
|
+
// int 16
|
|
1328
|
+
object = this.readI16();
|
|
1329
|
+
}
|
|
1330
|
+
else if (headByte === 0xd2) {
|
|
1331
|
+
// int 32
|
|
1332
|
+
object = this.readI32();
|
|
1333
|
+
}
|
|
1334
|
+
else if (headByte === 0xd3) {
|
|
1335
|
+
// int 64
|
|
1336
|
+
if (this.useBigInt64) {
|
|
1337
|
+
object = this.readI64AsBigInt();
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
object = this.readI64();
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
else if (headByte === 0xd9) {
|
|
1344
|
+
// str 8
|
|
1345
|
+
const byteLength = this.lookU8();
|
|
1346
|
+
object = this.decodeString(byteLength, 1);
|
|
1347
|
+
}
|
|
1348
|
+
else if (headByte === 0xda) {
|
|
1349
|
+
// str 16
|
|
1350
|
+
const byteLength = this.lookU16();
|
|
1351
|
+
object = this.decodeString(byteLength, 2);
|
|
1352
|
+
}
|
|
1353
|
+
else if (headByte === 0xdb) {
|
|
1354
|
+
// str 32
|
|
1355
|
+
const byteLength = this.lookU32();
|
|
1356
|
+
object = this.decodeString(byteLength, 4);
|
|
1357
|
+
}
|
|
1358
|
+
else if (headByte === 0xdc) {
|
|
1359
|
+
// array 16
|
|
1360
|
+
const size = this.readU16();
|
|
1361
|
+
if (size !== 0) {
|
|
1362
|
+
this.pushArrayState(size);
|
|
1363
|
+
this.complete();
|
|
1364
|
+
continue DECODE;
|
|
1365
|
+
}
|
|
1366
|
+
else {
|
|
1367
|
+
object = [];
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
else if (headByte === 0xdd) {
|
|
1371
|
+
// array 32
|
|
1372
|
+
const size = this.readU32();
|
|
1373
|
+
if (size !== 0) {
|
|
1374
|
+
this.pushArrayState(size);
|
|
1375
|
+
this.complete();
|
|
1376
|
+
continue DECODE;
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
object = [];
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
else if (headByte === 0xde) {
|
|
1383
|
+
// map 16
|
|
1384
|
+
const size = this.readU16();
|
|
1385
|
+
if (size !== 0) {
|
|
1386
|
+
this.pushMapState(size);
|
|
1387
|
+
this.complete();
|
|
1388
|
+
continue DECODE;
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
object = {};
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
else if (headByte === 0xdf) {
|
|
1395
|
+
// map 32
|
|
1396
|
+
const size = this.readU32();
|
|
1397
|
+
if (size !== 0) {
|
|
1398
|
+
this.pushMapState(size);
|
|
1399
|
+
this.complete();
|
|
1400
|
+
continue DECODE;
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
object = {};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
else if (headByte === 0xc4) {
|
|
1407
|
+
// bin 8
|
|
1408
|
+
const size = this.lookU8();
|
|
1409
|
+
object = this.decodeBinary(size, 1);
|
|
1410
|
+
}
|
|
1411
|
+
else if (headByte === 0xc5) {
|
|
1412
|
+
// bin 16
|
|
1413
|
+
const size = this.lookU16();
|
|
1414
|
+
object = this.decodeBinary(size, 2);
|
|
1415
|
+
}
|
|
1416
|
+
else if (headByte === 0xc6) {
|
|
1417
|
+
// bin 32
|
|
1418
|
+
const size = this.lookU32();
|
|
1419
|
+
object = this.decodeBinary(size, 4);
|
|
1420
|
+
}
|
|
1421
|
+
else if (headByte === 0xd4) {
|
|
1422
|
+
// fixext 1
|
|
1423
|
+
object = this.decodeExtension(1, 0);
|
|
1424
|
+
}
|
|
1425
|
+
else if (headByte === 0xd5) {
|
|
1426
|
+
// fixext 2
|
|
1427
|
+
object = this.decodeExtension(2, 0);
|
|
1428
|
+
}
|
|
1429
|
+
else if (headByte === 0xd6) {
|
|
1430
|
+
// fixext 4
|
|
1431
|
+
object = this.decodeExtension(4, 0);
|
|
1432
|
+
}
|
|
1433
|
+
else if (headByte === 0xd7) {
|
|
1434
|
+
// fixext 8
|
|
1435
|
+
object = this.decodeExtension(8, 0);
|
|
1436
|
+
}
|
|
1437
|
+
else if (headByte === 0xd8) {
|
|
1438
|
+
// fixext 16
|
|
1439
|
+
object = this.decodeExtension(16, 0);
|
|
1440
|
+
}
|
|
1441
|
+
else if (headByte === 0xc7) {
|
|
1442
|
+
// ext 8
|
|
1443
|
+
const size = this.lookU8();
|
|
1444
|
+
object = this.decodeExtension(size, 1);
|
|
1445
|
+
}
|
|
1446
|
+
else if (headByte === 0xc8) {
|
|
1447
|
+
// ext 16
|
|
1448
|
+
const size = this.lookU16();
|
|
1449
|
+
object = this.decodeExtension(size, 2);
|
|
1450
|
+
}
|
|
1451
|
+
else if (headByte === 0xc9) {
|
|
1452
|
+
// ext 32
|
|
1453
|
+
const size = this.lookU32();
|
|
1454
|
+
object = this.decodeExtension(size, 4);
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
throw new DecodeError(`Unrecognized type byte: ${prettyByte(headByte)}`);
|
|
1458
|
+
}
|
|
1459
|
+
this.complete();
|
|
1460
|
+
const stack = this.stack;
|
|
1461
|
+
while (stack.length > 0) {
|
|
1462
|
+
// arrays and maps
|
|
1463
|
+
const state = stack.top();
|
|
1464
|
+
if (state.type === STATE_ARRAY) {
|
|
1465
|
+
state.array[state.position] = object;
|
|
1466
|
+
state.position++;
|
|
1467
|
+
if (state.position === state.size) {
|
|
1468
|
+
object = state.array;
|
|
1469
|
+
stack.release(state);
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
continue DECODE;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
else if (state.type === STATE_MAP_KEY) {
|
|
1476
|
+
if (object === "__proto__") {
|
|
1477
|
+
throw new DecodeError("The key __proto__ is not allowed");
|
|
1478
|
+
}
|
|
1479
|
+
state.key = this.mapKeyConverter(object);
|
|
1480
|
+
state.type = STATE_MAP_VALUE;
|
|
1481
|
+
continue DECODE;
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
// it must be `state.type === State.MAP_VALUE` here
|
|
1485
|
+
state.map[state.key] = object;
|
|
1486
|
+
state.readCount++;
|
|
1487
|
+
if (state.readCount === state.size) {
|
|
1488
|
+
object = state.map;
|
|
1489
|
+
stack.release(state);
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
state.key = null;
|
|
1493
|
+
state.type = STATE_MAP_KEY;
|
|
1494
|
+
continue DECODE;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return object;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
readHeadByte() {
|
|
1502
|
+
if (this.headByte === HEAD_BYTE_REQUIRED) {
|
|
1503
|
+
this.headByte = this.readU8();
|
|
1504
|
+
// console.log("headByte", prettyByte(this.headByte));
|
|
1505
|
+
}
|
|
1506
|
+
return this.headByte;
|
|
1507
|
+
}
|
|
1508
|
+
complete() {
|
|
1509
|
+
this.headByte = HEAD_BYTE_REQUIRED;
|
|
1510
|
+
}
|
|
1511
|
+
readArraySize() {
|
|
1512
|
+
const headByte = this.readHeadByte();
|
|
1513
|
+
switch (headByte) {
|
|
1514
|
+
case 0xdc:
|
|
1515
|
+
return this.readU16();
|
|
1516
|
+
case 0xdd:
|
|
1517
|
+
return this.readU32();
|
|
1518
|
+
default: {
|
|
1519
|
+
if (headByte < 0xa0) {
|
|
1520
|
+
return headByte - 0x90;
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
throw new DecodeError(`Unrecognized array type byte: ${prettyByte(headByte)}`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
pushMapState(size) {
|
|
1529
|
+
if (size > this.maxMapLength) {
|
|
1530
|
+
throw new DecodeError(`Max length exceeded: map length (${size}) > maxMapLengthLength (${this.maxMapLength})`);
|
|
1531
|
+
}
|
|
1532
|
+
this.stack.pushMapState(size);
|
|
1533
|
+
}
|
|
1534
|
+
pushArrayState(size) {
|
|
1535
|
+
if (size > this.maxArrayLength) {
|
|
1536
|
+
throw new DecodeError(`Max length exceeded: array length (${size}) > maxArrayLength (${this.maxArrayLength})`);
|
|
1537
|
+
}
|
|
1538
|
+
this.stack.pushArrayState(size);
|
|
1539
|
+
}
|
|
1540
|
+
decodeString(byteLength, headerOffset) {
|
|
1541
|
+
if (!this.rawStrings || this.stateIsMapKey()) {
|
|
1542
|
+
return this.decodeUtf8String(byteLength, headerOffset);
|
|
1543
|
+
}
|
|
1544
|
+
return this.decodeBinary(byteLength, headerOffset);
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* @throws {@link RangeError}
|
|
1548
|
+
*/
|
|
1549
|
+
decodeUtf8String(byteLength, headerOffset) {
|
|
1550
|
+
if (byteLength > this.maxStrLength) {
|
|
1551
|
+
throw new DecodeError(`Max length exceeded: UTF-8 byte length (${byteLength}) > maxStrLength (${this.maxStrLength})`);
|
|
1552
|
+
}
|
|
1553
|
+
if (this.bytes.byteLength < this.pos + headerOffset + byteLength) {
|
|
1554
|
+
throw MORE_DATA;
|
|
1555
|
+
}
|
|
1556
|
+
const offset = this.pos + headerOffset;
|
|
1557
|
+
let object;
|
|
1558
|
+
if (this.stateIsMapKey() && this.keyDecoder?.canBeCached(byteLength)) {
|
|
1559
|
+
object = this.keyDecoder.decode(this.bytes, offset, byteLength);
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
object = utf8Decode(this.bytes, offset, byteLength);
|
|
1563
|
+
}
|
|
1564
|
+
this.pos += headerOffset + byteLength;
|
|
1565
|
+
return object;
|
|
1566
|
+
}
|
|
1567
|
+
stateIsMapKey() {
|
|
1568
|
+
if (this.stack.length > 0) {
|
|
1569
|
+
const state = this.stack.top();
|
|
1570
|
+
return state.type === STATE_MAP_KEY;
|
|
1571
|
+
}
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* @throws {@link RangeError}
|
|
1576
|
+
*/
|
|
1577
|
+
decodeBinary(byteLength, headOffset) {
|
|
1578
|
+
if (byteLength > this.maxBinLength) {
|
|
1579
|
+
throw new DecodeError(`Max length exceeded: bin length (${byteLength}) > maxBinLength (${this.maxBinLength})`);
|
|
1580
|
+
}
|
|
1581
|
+
if (!this.hasRemaining(byteLength + headOffset)) {
|
|
1582
|
+
throw MORE_DATA;
|
|
1583
|
+
}
|
|
1584
|
+
const offset = this.pos + headOffset;
|
|
1585
|
+
const object = this.bytes.subarray(offset, offset + byteLength);
|
|
1586
|
+
this.pos += headOffset + byteLength;
|
|
1587
|
+
return object;
|
|
1588
|
+
}
|
|
1589
|
+
decodeExtension(size, headOffset) {
|
|
1590
|
+
if (size > this.maxExtLength) {
|
|
1591
|
+
throw new DecodeError(`Max length exceeded: ext length (${size}) > maxExtLength (${this.maxExtLength})`);
|
|
1592
|
+
}
|
|
1593
|
+
const extType = this.view.getInt8(this.pos + headOffset);
|
|
1594
|
+
const data = this.decodeBinary(size, headOffset + 1 /* extType */);
|
|
1595
|
+
return this.extensionCodec.decode(data, extType, this.context);
|
|
1596
|
+
}
|
|
1597
|
+
lookU8() {
|
|
1598
|
+
return this.view.getUint8(this.pos);
|
|
1599
|
+
}
|
|
1600
|
+
lookU16() {
|
|
1601
|
+
return this.view.getUint16(this.pos);
|
|
1602
|
+
}
|
|
1603
|
+
lookU32() {
|
|
1604
|
+
return this.view.getUint32(this.pos);
|
|
1605
|
+
}
|
|
1606
|
+
readU8() {
|
|
1607
|
+
const value = this.view.getUint8(this.pos);
|
|
1608
|
+
this.pos++;
|
|
1609
|
+
return value;
|
|
1610
|
+
}
|
|
1611
|
+
readI8() {
|
|
1612
|
+
const value = this.view.getInt8(this.pos);
|
|
1613
|
+
this.pos++;
|
|
1614
|
+
return value;
|
|
1615
|
+
}
|
|
1616
|
+
readU16() {
|
|
1617
|
+
const value = this.view.getUint16(this.pos);
|
|
1618
|
+
this.pos += 2;
|
|
1619
|
+
return value;
|
|
1620
|
+
}
|
|
1621
|
+
readI16() {
|
|
1622
|
+
const value = this.view.getInt16(this.pos);
|
|
1623
|
+
this.pos += 2;
|
|
1624
|
+
return value;
|
|
1625
|
+
}
|
|
1626
|
+
readU32() {
|
|
1627
|
+
const value = this.view.getUint32(this.pos);
|
|
1628
|
+
this.pos += 4;
|
|
1629
|
+
return value;
|
|
1630
|
+
}
|
|
1631
|
+
readI32() {
|
|
1632
|
+
const value = this.view.getInt32(this.pos);
|
|
1633
|
+
this.pos += 4;
|
|
1634
|
+
return value;
|
|
1635
|
+
}
|
|
1636
|
+
readU64() {
|
|
1637
|
+
const value = getUint64(this.view, this.pos);
|
|
1638
|
+
this.pos += 8;
|
|
1639
|
+
return value;
|
|
1640
|
+
}
|
|
1641
|
+
readI64() {
|
|
1642
|
+
const value = getInt64(this.view, this.pos);
|
|
1643
|
+
this.pos += 8;
|
|
1644
|
+
return value;
|
|
1645
|
+
}
|
|
1646
|
+
readU64AsBigInt() {
|
|
1647
|
+
const value = this.view.getBigUint64(this.pos);
|
|
1648
|
+
this.pos += 8;
|
|
1649
|
+
return value;
|
|
1650
|
+
}
|
|
1651
|
+
readI64AsBigInt() {
|
|
1652
|
+
const value = this.view.getBigInt64(this.pos);
|
|
1653
|
+
this.pos += 8;
|
|
1654
|
+
return value;
|
|
1655
|
+
}
|
|
1656
|
+
readF32() {
|
|
1657
|
+
const value = this.view.getFloat32(this.pos);
|
|
1658
|
+
this.pos += 4;
|
|
1659
|
+
return value;
|
|
1660
|
+
}
|
|
1661
|
+
readF64() {
|
|
1662
|
+
const value = this.view.getFloat64(this.pos);
|
|
1663
|
+
this.pos += 8;
|
|
1664
|
+
return value;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* It decodes a single MessagePack object in a buffer.
|
|
1670
|
+
*
|
|
1671
|
+
* This is a synchronous decoding function.
|
|
1672
|
+
* See other variants for asynchronous decoding: {@link decodeAsync}, {@link decodeStream}, or {@link decodeArrayStream}.
|
|
1673
|
+
*
|
|
1674
|
+
* @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty.
|
|
1675
|
+
* @throws {@link DecodeError} if the buffer contains invalid data.
|
|
1676
|
+
*/
|
|
1677
|
+
function decode(buffer, options) {
|
|
1678
|
+
const decoder = new Decoder(options);
|
|
1679
|
+
return decoder.decode(buffer);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
var crdtManager;
|
|
1683
|
+
var hasRequiredCrdtManager;
|
|
1684
|
+
|
|
1685
|
+
function requireCrdtManager () {
|
|
1686
|
+
if (hasRequiredCrdtManager) return crdtManager;
|
|
1687
|
+
hasRequiredCrdtManager = 1;
|
|
1688
|
+
// Custom conflict-free replicated data type system with vector clocks
|
|
1689
|
+
|
|
1690
|
+
const CONSOLE_PREFIX = "CRDT Manager: ";
|
|
1691
|
+
|
|
1692
|
+
class CRDTManager {
|
|
1693
|
+
// Storage
|
|
1694
|
+
#replicaId;
|
|
1695
|
+
#keyOperations = new Map();
|
|
1696
|
+
|
|
1697
|
+
// Vector clock
|
|
1698
|
+
#vectorClock = new Map();
|
|
1699
|
+
|
|
1700
|
+
// Local only
|
|
1701
|
+
#propertyStore = {}; // Current local values per key, as object
|
|
1702
|
+
#lastPropertyStore = {}; // Last property store to compare against
|
|
1703
|
+
|
|
1704
|
+
// Local Garbage Collection
|
|
1705
|
+
#lastGCCheck = 0;
|
|
1706
|
+
#opUuuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Create a new instance
|
|
1710
|
+
* @param {string} [replicaId] - Choose a uuid (falls back to random uuid)
|
|
1711
|
+
*/
|
|
1712
|
+
constructor(replicaId) {
|
|
1713
|
+
this.#replicaId = replicaId || crypto.randomUUID();
|
|
1714
|
+
this.#vectorClock.set(this.#replicaId, 0);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Import the entire state of the CRDT manager (this overwrites the old state)
|
|
1719
|
+
* @param {Object} state
|
|
1720
|
+
*/
|
|
1721
|
+
importState(state) {
|
|
1722
|
+
try {
|
|
1723
|
+
const { keyOperations, vectorClock } = state;
|
|
1724
|
+
|
|
1725
|
+
this.#opUuuidTimestamp.clear(); // Reset the uuid timestamps since it might contain uuid's that no longer exist in the new state
|
|
1726
|
+
this.#keyOperations = new Map(keyOperations); // Rebuild the map
|
|
1727
|
+
this.#vectorClock = new Map(vectorClock); // Also rebuild the map here
|
|
1728
|
+
if (!this.#vectorClock.has(this.#replicaId)) this.#vectorClock.set(this.#replicaId, 0); // Reset own vector clock if it wasn't present in the imported state
|
|
1729
|
+
|
|
1730
|
+
// Map operation uuids to timestamp
|
|
1731
|
+
this.#keyOperations.forEach((value, key) => {
|
|
1732
|
+
value?.forEach((operation) => {
|
|
1733
|
+
this.#opUuuidTimestamp.set(operation?.uuid, Date.now());
|
|
1734
|
+
});
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// Process each key to update local values
|
|
1738
|
+
for (const key of this.#keyOperations.keys()) this.#processLocalProperty(key);
|
|
1739
|
+
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
console.error(CONSOLE_PREFIX + "Failed to import state:", error);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Export only the last operation of a property for a single key
|
|
1747
|
+
* This can still be imported using importProperty
|
|
1748
|
+
* @param {string} key
|
|
1749
|
+
* @returns {Object} - Data export
|
|
1750
|
+
*/
|
|
1751
|
+
exportPropertyLastOpOnly(key) {
|
|
1752
|
+
const lastOp = this.#keyOperations.get(key)?.[this.#keyOperations.get(key)?.length - 1];
|
|
1753
|
+
return {
|
|
1754
|
+
key,
|
|
1755
|
+
operations: lastOp ? [lastOp] : [],
|
|
1756
|
+
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Export property for a single key
|
|
1762
|
+
* @param {string} key
|
|
1763
|
+
* @returns {Object} - Data export
|
|
1764
|
+
*/
|
|
1765
|
+
exportProperty(key) {
|
|
1766
|
+
return {
|
|
1767
|
+
key,
|
|
1768
|
+
operations: [...(this.#keyOperations.get(key) || [])],
|
|
1769
|
+
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Import property for a single key and merge changes
|
|
1775
|
+
* @param {Object} data - Data to import
|
|
1776
|
+
*/
|
|
1777
|
+
importProperty(data) {
|
|
1778
|
+
try {
|
|
1779
|
+
const { key, operations, vectorClock } = data;
|
|
1780
|
+
|
|
1781
|
+
// Get COPY of current ops (or empty array if none yet)
|
|
1782
|
+
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1783
|
+
|
|
1784
|
+
// Merge vector clocks (always take max value)
|
|
1785
|
+
if (vectorClock) {
|
|
1786
|
+
for (const [id, counter] of vectorClock) {
|
|
1787
|
+
if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
|
|
1788
|
+
this.#vectorClock.set(id, counter);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Safeguard against attack where new clients would rapidly connect and disconnect to fill up the vectorClock
|
|
1794
|
+
if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
|
|
1795
|
+
|
|
1796
|
+
// Add new operations (diff) in correct order
|
|
1797
|
+
const existingUuids = new Set(currentOps.map(op => op.uuid));
|
|
1798
|
+
if (operations?.length) {
|
|
1799
|
+
for (const op of operations) {
|
|
1800
|
+
if (op?.uuid && !existingUuids.has(op.uuid)) {
|
|
1801
|
+
currentOps.push({ ...op }); // Add operation
|
|
1802
|
+
this.#opUuuidTimestamp.set(op?.uuid, Date.now()); // Add timestamp for gc
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// Sort, update operations & local value
|
|
1808
|
+
this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
|
|
1809
|
+
this.#processLocalProperty(key);
|
|
1810
|
+
|
|
1811
|
+
this.#checkGarbageCollection(); // Check if GC should run
|
|
1812
|
+
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
console.error(CONSOLE_PREFIX + "Failed to import property:", error);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Update a property
|
|
1820
|
+
* @param {string} key
|
|
1821
|
+
* @param {string} operation
|
|
1822
|
+
* @param {*} value
|
|
1823
|
+
* @param {*} updateValue
|
|
1824
|
+
*/
|
|
1825
|
+
updateProperty(key, operation, value, updateValue) {
|
|
1826
|
+
try {
|
|
1827
|
+
// Increment vector clock
|
|
1828
|
+
const counter = this.#vectorClock.get(this.#replicaId) || 0;
|
|
1829
|
+
this.#vectorClock.set(this.#replicaId, counter + 1);
|
|
1830
|
+
|
|
1831
|
+
// Assign shallow COPY of currennt ops or fall back to empty array
|
|
1832
|
+
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1833
|
+
|
|
1834
|
+
// Add operation
|
|
1835
|
+
const newOp = this.#createOperation({ operation, value, updateValue }, Array.from(this.#vectorClock.entries()));
|
|
1836
|
+
currentOps.push(newOp);
|
|
1837
|
+
|
|
1838
|
+
// Sort and process
|
|
1839
|
+
this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
|
|
1840
|
+
this.#opUuuidTimestamp.set(newOp.uuid, Date.now());
|
|
1841
|
+
this.#processLocalProperty(key);
|
|
1842
|
+
|
|
1843
|
+
this.#checkGarbageCollection(); // Check if GC should run
|
|
1844
|
+
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
console.error(CONSOLE_PREFIX + `Failed to add operation for key "${key}":`, error);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
#checkGarbageCollection() {
|
|
1851
|
+
const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
|
|
1852
|
+
const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
|
|
1853
|
+
|
|
1854
|
+
if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
|
|
1855
|
+
this.#lastGCCheck = Date.now();
|
|
1856
|
+
|
|
1857
|
+
for (const [key, operations] of this.#keyOperations.entries()) {
|
|
1858
|
+
if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
|
|
1859
|
+
|
|
1860
|
+
let retainCount = operations.length;
|
|
1861
|
+
operations.forEach((op, index) => {
|
|
1862
|
+
// Count how many ops, from last to latest, are >10s old in a row
|
|
1863
|
+
if (op.uuid && this.#opUuuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
|
|
1864
|
+
const removeCount = index + 1;
|
|
1865
|
+
retainCount = operations.length - removeCount;
|
|
1866
|
+
this.#opUuuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
if (retainCount < operations.length) {
|
|
1871
|
+
const retainOps = operations.slice(-retainCount); // newest ops
|
|
1872
|
+
const removeOps = operations.slice(0, -retainCount); // oldest ops (start = idx 0, end = retainCount counted from right side)
|
|
1873
|
+
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
1874
|
+
|
|
1875
|
+
// Calculate the value at the point where retained operations start
|
|
1876
|
+
let baselineValue = null;
|
|
1877
|
+
for (const op of removeOps) {
|
|
1878
|
+
if (!op.data) continue;
|
|
1879
|
+
if (op.data.operation.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
|
|
1880
|
+
baselineValue = this.#handleOperation(
|
|
1881
|
+
baselineValue,
|
|
1882
|
+
op.data.operation,
|
|
1883
|
+
op.data.value,
|
|
1884
|
+
op.data.updateValue
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Create a compact operation with baseline value and appropriate vector clock
|
|
1889
|
+
const compactOp = this.#createOperation({ operation: "set", value: baselineValue }, baselineVectorClock);
|
|
1890
|
+
|
|
1891
|
+
// Combine compact op with retained operations
|
|
1892
|
+
this.#keyOperations.set(key, [compactOp, ...retainOps]);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* Create an operation object
|
|
1899
|
+
* @param {Object} data
|
|
1900
|
+
* @param {Array} vectorClock
|
|
1901
|
+
* @returns {Object} - Operation object
|
|
1902
|
+
*/
|
|
1903
|
+
#createOperation(data, vectorClock) {
|
|
1904
|
+
const newOperation = {
|
|
1905
|
+
data,
|
|
1906
|
+
vectorClock,
|
|
1907
|
+
source: this.#replicaId,
|
|
1908
|
+
uuid: crypto.randomUUID()
|
|
1909
|
+
};
|
|
1910
|
+
this.#opUuuidTimestamp.set(newOperation.uuid, Date.now());
|
|
1911
|
+
return newOperation;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Process a property's value by applying all operations and set it in the property store
|
|
1916
|
+
* @param {string} key
|
|
1917
|
+
*/
|
|
1918
|
+
#processLocalProperty(key) {
|
|
1919
|
+
try {
|
|
1920
|
+
const ops = this.#keyOperations.get(key);
|
|
1921
|
+
if (!ops?.length) return;
|
|
1922
|
+
|
|
1923
|
+
// Apply all operations in order
|
|
1924
|
+
let value = null;
|
|
1925
|
+
|
|
1926
|
+
for (const op of ops) {
|
|
1927
|
+
if (!op.data) continue;
|
|
1928
|
+
if (op.data.operation.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
|
|
1929
|
+
value = this.#handleOperation(
|
|
1930
|
+
value,
|
|
1931
|
+
op.data.operation,
|
|
1932
|
+
op.data.value,
|
|
1933
|
+
op.data.updateValue
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
this.#propertyStore[key] = value; // Save locally
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
console.error(CONSOLE_PREFIX + `Failed to process property for "${key}":`, error);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Sort by vector clock (causal order)
|
|
1944
|
+
#sortByVectorClock(operations) {
|
|
1945
|
+
return [...operations].sort((a, b) => {
|
|
1946
|
+
const clockA = new Map(a.vectorClock || []);
|
|
1947
|
+
const clockB = new Map(b.vectorClock || []);
|
|
1948
|
+
|
|
1949
|
+
// First sort by causal relationship
|
|
1950
|
+
let aGreater = false, bGreater = false;
|
|
1951
|
+
|
|
1952
|
+
const allIds = new Set([...clockA.keys(), ...clockB.keys()]);
|
|
1953
|
+
for (const id of allIds) {
|
|
1954
|
+
const countA = clockA.get(id) || 0;
|
|
1955
|
+
const countB = clockB.get(id) || 0;
|
|
1956
|
+
|
|
1957
|
+
if (countA > countB) aGreater = true;
|
|
1958
|
+
if (countA < countB) bGreater = true;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// Determine causal relationship (if one is greater, and the other is smaller, it's clear)
|
|
1962
|
+
if (aGreater && !bGreater) return 1; // a happens after b
|
|
1963
|
+
if (!aGreater && bGreater) return -1; // a happens before b
|
|
1964
|
+
|
|
1965
|
+
// Concurrent (unclear): Compare highest counter in each (sort by which one is ahead)
|
|
1966
|
+
const maxA = Math.max(...Array.from(clockA.values()), 0);
|
|
1967
|
+
const maxB = Math.max(...Array.from(clockB.values()), 0);
|
|
1968
|
+
if (maxA !== maxB) return maxA - maxB;
|
|
1969
|
+
|
|
1970
|
+
// Final tiebreak: Sort by ID of Replica that actually generated the operation
|
|
1971
|
+
return a.source.localeCompare(b.source);
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// Handle an operation
|
|
1976
|
+
#handleOperation(curValue, operation, value, updateValue) {
|
|
1977
|
+
try {
|
|
1978
|
+
// Clone to avoid reference issues
|
|
1979
|
+
curValue = JSON.parse(JSON.stringify(curValue || null));
|
|
1980
|
+
|
|
1981
|
+
// Set operation
|
|
1982
|
+
if (operation === "set") return value;
|
|
1983
|
+
|
|
1984
|
+
// Array operations
|
|
1985
|
+
if (operation.startsWith('array')) {
|
|
1986
|
+
if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
|
|
1987
|
+
|
|
1988
|
+
// Comparison function
|
|
1989
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
1990
|
+
const compare = (item) => {
|
|
1991
|
+
if (isObject && typeof item === 'object' && item !== null) {
|
|
1992
|
+
return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
|
|
1993
|
+
}
|
|
1994
|
+
return item === value; // Value comparison
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
switch (operation) {
|
|
1998
|
+
case 'array-add':
|
|
1999
|
+
curValue.push(value);
|
|
2000
|
+
break;
|
|
2001
|
+
case 'array-add-unique':
|
|
2002
|
+
if (!curValue.some(compare)) curValue.push(value);
|
|
2003
|
+
break;
|
|
2004
|
+
case 'array-remove-matching':
|
|
2005
|
+
curValue = curValue.filter(item => !compare(item));
|
|
2006
|
+
break;
|
|
2007
|
+
case 'array-update-matching':
|
|
2008
|
+
const index = curValue.findIndex(compare);
|
|
2009
|
+
if (index !== -1) curValue[index] = updateValue;
|
|
2010
|
+
break;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
return curValue;
|
|
2015
|
+
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
console.error(CONSOLE_PREFIX + `Failed to process operation with curValue ${curValue}:`, error);
|
|
2018
|
+
return curValue;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// Get property store
|
|
2023
|
+
get getPropertyStore() {
|
|
2024
|
+
try {
|
|
2025
|
+
return JSON.parse(JSON.stringify(this.#propertyStore)); // Return full copy
|
|
2026
|
+
} catch {
|
|
2027
|
+
console.error(CONSOLE_PREFIX + "Failed to parse property store");
|
|
2028
|
+
return {};
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Check for changes in the local property store
|
|
2033
|
+
get didPropertiesChange() {
|
|
2034
|
+
try {
|
|
2035
|
+
if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
|
|
2036
|
+
this.#lastPropertyStore = JSON.parse(JSON.stringify(this.#propertyStore));
|
|
2037
|
+
return true;
|
|
2038
|
+
}
|
|
2039
|
+
} catch { }
|
|
2040
|
+
return false;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// Get state (can be imported using importState, converts the maps to arrays for serialization)
|
|
2044
|
+
get getState() {
|
|
2045
|
+
return {
|
|
2046
|
+
keyOperations: [...this.#keyOperations.entries()],
|
|
2047
|
+
vectorClock: [...this.#vectorClock.entries()]
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
crdtManager = { CRDTManager };
|
|
2053
|
+
return crdtManager;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
var crdtManagerExports = requireCrdtManager();
|
|
2057
|
+
|
|
1
2058
|
/**
|
|
2
2059
|
* PlaySocket - WebSocket-based multiplayer for games
|
|
3
2060
|
*/
|
|
4
2061
|
|
|
2062
|
+
|
|
5
2063
|
const ERROR_PREFIX = "PlaySocket error: ";
|
|
6
2064
|
const WARNING_PREFIX = "PlaySocket warning: ";
|
|
7
2065
|
const TIMEOUT_MS = 5000; // 5 second timeout for operations
|
|
@@ -16,12 +2074,10 @@ class PlaySocket {
|
|
|
16
2074
|
#customData;
|
|
17
2075
|
|
|
18
2076
|
// Room properties
|
|
19
|
-
#storage = {}; // Shared storage object
|
|
20
|
-
#storageTimestamps = {}; // Tracks update timestamps per key to ensure that not-yet synced updates don't get overwritten
|
|
21
2077
|
#roomHost;
|
|
22
2078
|
#roomId; // ID of the host (client only)
|
|
23
2079
|
#connectionCount = 0;
|
|
24
|
-
#
|
|
2080
|
+
#crdtManager;
|
|
25
2081
|
|
|
26
2082
|
// Event handling
|
|
27
2083
|
#callbacks = new Map(); // Event callbacks
|
|
@@ -49,6 +2105,7 @@ class PlaySocket {
|
|
|
49
2105
|
this.#id = id;
|
|
50
2106
|
if (options.endpoint) this.#endpoint = options.endpoint;
|
|
51
2107
|
if (options.customData) this.#customData = { ...options.customData };
|
|
2108
|
+
this.#crdtManager = new crdtManagerExports.CRDTManager(this.#id);
|
|
52
2109
|
}
|
|
53
2110
|
|
|
54
2111
|
/**
|
|
@@ -61,23 +2118,13 @@ class PlaySocket {
|
|
|
61
2118
|
);
|
|
62
2119
|
}
|
|
63
2120
|
|
|
64
|
-
/**
|
|
65
|
-
* Get synchronized timestamp
|
|
66
|
-
* @private
|
|
67
|
-
*/
|
|
68
|
-
#getTimestamp() {
|
|
69
|
-
return Date.now() + this.#serverTimeOffset;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
2121
|
/**
|
|
73
2122
|
* Register an event callback
|
|
74
2123
|
* @param {string} event - Event name
|
|
75
2124
|
* @param {Function} callback - Callback function
|
|
76
2125
|
*/
|
|
77
2126
|
onEvent(event, callback) {
|
|
78
|
-
const validEvents = [
|
|
79
|
-
"status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"
|
|
80
|
-
];
|
|
2127
|
+
const validEvents = ["status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"];
|
|
81
2128
|
if (!validEvents.includes(event)) return console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
|
|
82
2129
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
83
2130
|
this.#callbacks.get(event).push(callback);
|
|
@@ -105,6 +2152,7 @@ class PlaySocket {
|
|
|
105
2152
|
async init() {
|
|
106
2153
|
if (this.#initialized) return Promise.reject(new Error("Already initialized"));
|
|
107
2154
|
if (!this.#endpoint) return Promise.reject(new Error("No websocket endpoint provided"));
|
|
2155
|
+
if (!this.#id) return Promise.reject(new Error("No id provided"));
|
|
108
2156
|
this.#triggerEvent("status", "Initializing...");
|
|
109
2157
|
|
|
110
2158
|
// Connect to WS server
|
|
@@ -119,8 +2167,7 @@ class PlaySocket {
|
|
|
119
2167
|
this.#sendToServer({
|
|
120
2168
|
type: 'register',
|
|
121
2169
|
id: this.#id,
|
|
122
|
-
customData: this.#customData || undefined
|
|
123
|
-
clientTime: Date.now()
|
|
2170
|
+
customData: this.#customData || undefined
|
|
124
2171
|
});
|
|
125
2172
|
}),
|
|
126
2173
|
this.#createTimeout("Registration")
|
|
@@ -139,6 +2186,7 @@ class PlaySocket {
|
|
|
139
2186
|
new Promise((resolve, reject) => {
|
|
140
2187
|
this.#pendingConnect = { reject };
|
|
141
2188
|
this.#socket = new WebSocket(this.#endpoint);
|
|
2189
|
+
this.#socket.binaryType = 'arraybuffer'; // Important for MessagePack binary data
|
|
142
2190
|
this.#setupSocketHandlers(); // Message & close events
|
|
143
2191
|
this.#socket.onopen = resolve;
|
|
144
2192
|
}),
|
|
@@ -155,19 +2203,18 @@ class PlaySocket {
|
|
|
155
2203
|
#setupSocketHandlers() {
|
|
156
2204
|
this.#socket.onmessage = (event) => {
|
|
157
2205
|
try {
|
|
158
|
-
const message =
|
|
2206
|
+
const message = decode(new Uint8Array(event.data));
|
|
159
2207
|
if (!message.type) return;
|
|
160
2208
|
|
|
161
2209
|
switch (message.type) {
|
|
162
2210
|
case 'join_accepted':
|
|
163
2211
|
// Connected to room
|
|
164
2212
|
if (this.#pendingJoin) {
|
|
165
|
-
this.#
|
|
166
|
-
this.#storageTimestamps = message.storageTimestamps;
|
|
2213
|
+
this.#crdtManager.importState(message.state);
|
|
167
2214
|
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
168
2215
|
this.#roomHost = message.host;
|
|
2216
|
+
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
169
2217
|
this.#triggerEvent("status", `Connected to room.`);
|
|
170
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
171
2218
|
this.#pendingJoin.resolve();
|
|
172
2219
|
}
|
|
173
2220
|
break;
|
|
@@ -175,8 +2222,8 @@ class PlaySocket {
|
|
|
175
2222
|
case 'join_rejected':
|
|
176
2223
|
// Connection to room failed
|
|
177
2224
|
if (this.#pendingJoin) {
|
|
178
|
-
this.#triggerEvent("status", "
|
|
179
|
-
this.#pendingJoin.reject(new Error("
|
|
2225
|
+
this.#triggerEvent("status", "Failed to join room: " + message.reason);
|
|
2226
|
+
this.#pendingJoin.reject(new Error("Failed to join room: " + message.reason));
|
|
180
2227
|
}
|
|
181
2228
|
break;
|
|
182
2229
|
|
|
@@ -186,16 +2233,14 @@ class PlaySocket {
|
|
|
186
2233
|
this.#isReconnecting = false;
|
|
187
2234
|
this.#reconnectCount = 0;
|
|
188
2235
|
if (message.roomData) {
|
|
189
|
-
this.#
|
|
190
|
-
this.#storageTimestamps = message.roomData.storageTimestamps;
|
|
2236
|
+
this.#crdtManager.importState(message.roomData.state);
|
|
191
2237
|
this.#connectionCount = message.roomData.participantCount - 1; // Counted without the user themselves
|
|
192
|
-
this.#setHost(message.roomData.host);
|
|
193
|
-
this.#triggerEvent("storageUpdated",
|
|
194
|
-
} else {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
2238
|
+
this.#setHost(message.roomData.host); // Set host before in case there are .isHost checks in the storageUpdate fallback
|
|
2239
|
+
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2240
|
+
} else if (this.#roomId) {
|
|
2241
|
+
// If no room data received, but client thinks they were in a room...
|
|
2242
|
+
this.#triggerEvent("error", "Reconnected, but room no longer exists.");
|
|
2243
|
+
return this.destroy();
|
|
199
2244
|
}
|
|
200
2245
|
this.#triggerEvent("status", "Reconnected.");
|
|
201
2246
|
this.#pendingReconnect.resolve();
|
|
@@ -213,7 +2258,6 @@ class PlaySocket {
|
|
|
213
2258
|
if (this.#pendingHost) {
|
|
214
2259
|
this.#pendingHost.resolve(this.#roomId);
|
|
215
2260
|
this.#triggerEvent("status", `Room created${this.#pendingHost.maxSize ? ` with max size ${this.#pendingHost.maxSize}.` : '.'}`);
|
|
216
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
217
2261
|
}
|
|
218
2262
|
break;
|
|
219
2263
|
|
|
@@ -233,9 +2277,6 @@ class PlaySocket {
|
|
|
233
2277
|
|
|
234
2278
|
case 'registered':
|
|
235
2279
|
if (this.#pendingRegistration) {
|
|
236
|
-
const roundTripTime = Date.now() - message.clientTime;
|
|
237
|
-
const serverTimeAtClientNow = message.serverTime + (roundTripTime / 2);
|
|
238
|
-
this.#serverTimeOffset = serverTimeAtClientNow - Date.now();
|
|
239
2280
|
this.#sessionToken = message.sessionToken;
|
|
240
2281
|
this.#initialized = true;
|
|
241
2282
|
this.#pendingRegistration.resolve();
|
|
@@ -243,32 +2284,14 @@ class PlaySocket {
|
|
|
243
2284
|
}
|
|
244
2285
|
break;
|
|
245
2286
|
|
|
246
|
-
case '
|
|
247
|
-
if (message.
|
|
248
|
-
|
|
249
|
-
if (message.timestamp > localTimestamp && JSON.stringify(this.#storage[message.key]) !== JSON.stringify(message.value)) {
|
|
250
|
-
this.#storage[message.key] = message.value;
|
|
251
|
-
this.#storageTimestamps[message.key] = message.timestamp;
|
|
252
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
break;
|
|
256
|
-
|
|
257
|
-
case 'storage_operation':
|
|
258
|
-
if (message.key) {
|
|
259
|
-
const updatedArray = this.#handleArrayUpdate(message.key, message.operation, message.value, message.updateValue);
|
|
260
|
-
if (JSON.stringify(this.#storage[message.key]) !== JSON.stringify(updatedArray)) {
|
|
261
|
-
this.#storage[message.key] = updatedArray;
|
|
262
|
-
this.#storageTimestamps[message.key] = message.timestamp;
|
|
263
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
2287
|
+
case 'property_sync':
|
|
2288
|
+
if (message.property) this.#crdtManager.importProperty(message.property);
|
|
2289
|
+
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage);
|
|
266
2290
|
break;
|
|
267
2291
|
|
|
268
2292
|
case 'client_disconnected':
|
|
269
2293
|
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
270
2294
|
this.#setHost(message.host);
|
|
271
|
-
|
|
272
2295
|
this.#triggerEvent("clientDisconnected", message.client);
|
|
273
2296
|
this.#triggerEvent("status", `Client ${message.client} disconnected.`);
|
|
274
2297
|
break;
|
|
@@ -353,7 +2376,7 @@ class PlaySocket {
|
|
|
353
2376
|
return console.error(ERROR_PREFIX + "Cannot send message - not connected.");
|
|
354
2377
|
}
|
|
355
2378
|
try {
|
|
356
|
-
this.#socket.send(
|
|
2379
|
+
this.#socket.send(encode(data));
|
|
357
2380
|
} catch (error) {
|
|
358
2381
|
console.error(ERROR_PREFIX + "Error sending message:", error);
|
|
359
2382
|
this.#triggerEvent("error", "Error sending message: " + error.message);
|
|
@@ -374,17 +2397,18 @@ class PlaySocket {
|
|
|
374
2397
|
|
|
375
2398
|
return Promise.race([
|
|
376
2399
|
new Promise((resolve, reject) => {
|
|
377
|
-
|
|
2400
|
+
Object.entries(initialStorage)?.forEach(([key, value]) => {
|
|
2401
|
+
this.#crdtManager.updateProperty(key, "set", value);
|
|
2402
|
+
});
|
|
378
2403
|
this.#roomId = this.#id; // Create room with your own ID as the room ID to mimic p2p
|
|
379
2404
|
this.#roomHost = this.#id;
|
|
380
2405
|
this.#pendingHost = { maxSize, resolve, reject };
|
|
381
|
-
|
|
382
2406
|
this.#sendToServer({
|
|
383
2407
|
type: 'create_room',
|
|
384
|
-
|
|
385
|
-
size: maxSize
|
|
386
|
-
from: this.#id,
|
|
2408
|
+
state: this.#crdtManager.getState,
|
|
2409
|
+
size: maxSize
|
|
387
2410
|
});
|
|
2411
|
+
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
388
2412
|
}),
|
|
389
2413
|
this.#createTimeout("Room creation")
|
|
390
2414
|
]).finally(() => {
|
|
@@ -427,75 +2451,30 @@ class PlaySocket {
|
|
|
427
2451
|
* @param {*} value - New value
|
|
428
2452
|
*/
|
|
429
2453
|
updateStorage(key, value) {
|
|
430
|
-
|
|
431
|
-
const timestamp = this.#getTimestamp();
|
|
2454
|
+
this.#crdtManager.updateProperty(key, "set", value);
|
|
432
2455
|
this.#sendToServer({
|
|
433
|
-
type: '
|
|
2456
|
+
type: 'property_update',
|
|
434
2457
|
key,
|
|
435
|
-
|
|
436
|
-
timestamp
|
|
2458
|
+
property: this.#crdtManager.exportPropertyLastOpOnly(key)
|
|
437
2459
|
});
|
|
438
|
-
//
|
|
439
|
-
this.#storage[key] = value;
|
|
440
|
-
this.#storageTimestamps[key] = timestamp;
|
|
441
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
2460
|
+
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage); // Always trigger callback after send in case msgs are sent in the callback (which would break the order)
|
|
442
2461
|
}
|
|
443
2462
|
|
|
444
2463
|
/**
|
|
445
|
-
* Update an array
|
|
2464
|
+
* Update an array with special operations in the shared storage
|
|
446
2465
|
* @param {string} key - Storage key
|
|
447
2466
|
* @param {string} operation - Operation type: add, add-unique, remove-matching, update-matching
|
|
448
2467
|
* @param {*} value - Value to operate on
|
|
449
2468
|
* @param {*} updateValue - New value for update-matching
|
|
450
2469
|
*/
|
|
451
2470
|
updateStorageArray(key, operation, value, updateValue) {
|
|
452
|
-
|
|
453
|
-
if (JSON.stringify(this.#storage[key]) === JSON.stringify(updatedArray)) return; // Prevent updates without changes
|
|
2471
|
+
this.#crdtManager.updateProperty(key, "array-" + operation, value, updateValue);
|
|
454
2472
|
this.#sendToServer({
|
|
455
|
-
type: '
|
|
2473
|
+
type: 'property_update',
|
|
456
2474
|
key,
|
|
457
|
-
|
|
458
|
-
value,
|
|
459
|
-
updateValue
|
|
2475
|
+
property: this.#crdtManager.exportPropertyLastOpOnly(key)
|
|
460
2476
|
});
|
|
461
|
-
|
|
462
|
-
this.#storage[key] = updatedArray;
|
|
463
|
-
this.#storageTimestamps[key] = this.#getTimestamp();
|
|
464
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Handle array operations client-side
|
|
469
|
-
* @private
|
|
470
|
-
*/
|
|
471
|
-
#handleArrayUpdate(key, operation, value, updateValue) {
|
|
472
|
-
let array = (!this.#storage[key] || !Array.isArray(this.#storage[key])) ? [] : [...this.#storage[key]];
|
|
473
|
-
const isObject = typeof value === 'object' && value !== null;
|
|
474
|
-
const compare = (item) => isObject ? JSON.stringify(item) === JSON.stringify(value) : item === value;
|
|
475
|
-
|
|
476
|
-
switch (operation) {
|
|
477
|
-
case 'add':
|
|
478
|
-
array.push(value);
|
|
479
|
-
break;
|
|
480
|
-
|
|
481
|
-
case 'add-unique':
|
|
482
|
-
if (!array.some(compare)) array.push(value);
|
|
483
|
-
break;
|
|
484
|
-
|
|
485
|
-
case 'remove-matching':
|
|
486
|
-
array = array.filter(item => !compare(item));
|
|
487
|
-
break;
|
|
488
|
-
|
|
489
|
-
case 'update-matching':
|
|
490
|
-
const index = array.findIndex(compare);
|
|
491
|
-
if (index !== -1) array[index] = updateValue;
|
|
492
|
-
break;
|
|
493
|
-
|
|
494
|
-
default:
|
|
495
|
-
console.error(ERROR_PREFIX + `Unknown array operation: ${operation}`);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return array;
|
|
2477
|
+
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage);
|
|
499
2478
|
}
|
|
500
2479
|
|
|
501
2480
|
/**
|
|
@@ -505,7 +2484,7 @@ class PlaySocket {
|
|
|
505
2484
|
this.#initialized = false; // Set this immediately to prevent automatic reconnection
|
|
506
2485
|
|
|
507
2486
|
if (this.#socket) {
|
|
508
|
-
// Signal to the server that it can immediately
|
|
2487
|
+
// Signal to the server that it can immediately remove this user
|
|
509
2488
|
if (this.#socket.readyState === WebSocket.OPEN) this.#sendToServer({ type: 'disconnect' });
|
|
510
2489
|
this.#socket.close();
|
|
511
2490
|
this.#socket = null;
|
|
@@ -518,18 +2497,22 @@ class PlaySocket {
|
|
|
518
2497
|
// Reset state
|
|
519
2498
|
clearTimeout(this.#reconnectTimeout);
|
|
520
2499
|
this.#roomHost = null;
|
|
521
|
-
this.#storage = {};
|
|
522
|
-
this.#storageTimestamps = {};
|
|
523
|
-
this.#serverTimeOffset = 0;
|
|
524
2500
|
this.#roomId = null;
|
|
525
2501
|
this.#connectionCount = 0;
|
|
526
2502
|
this.#isReconnecting = false;
|
|
527
2503
|
this.#reconnectCount = 0;
|
|
2504
|
+
|
|
2505
|
+
// Reject timeouts if currently active
|
|
2506
|
+
if (this.#pendingJoin) this.#pendingJoin.reject();
|
|
2507
|
+
if (this.#pendingHost) this.#pendingHost.reject();
|
|
2508
|
+
if (this.#pendingRegistration) this.#pendingRegistration.reject();
|
|
2509
|
+
if (this.#pendingConnect) this.#pendingConnect.reject();
|
|
2510
|
+
if (this.#pendingReconnect) this.#pendingReconnect.reject();
|
|
528
2511
|
}
|
|
529
2512
|
|
|
530
2513
|
// Public getters
|
|
531
2514
|
get connectionCount() { return this.#connectionCount; }
|
|
532
|
-
get getStorage() { return
|
|
2515
|
+
get getStorage() { return this.#crdtManager.getPropertyStore; }
|
|
533
2516
|
get isHost() { return this.#id == this.#roomHost; }
|
|
534
2517
|
get id() { return this.#id; }
|
|
535
2518
|
}
|