@statewalker/webrun-msgpack 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +45 -0
- package/README.md +152 -33
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +540 -12
- package/dist/msgpack-core.d.ts +70 -0
- package/dist/msgpack-core.d.ts.map +1 -0
- package/dist/msgpack.d.ts +12 -4
- package/dist/msgpack.d.ts.map +1 -1
- package/dist/port-codec.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +8 -0
- package/src/msgpack-core.ts +617 -0
- package/src/msgpack.ts +18 -12
- package/src/port-codec.ts +1 -3
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* MessagePack serializer and deserializer.
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript port of msgpack.js by Yves Goergen:
|
|
5
|
+
* https://github.com/ygoe/msgpack.js
|
|
6
|
+
* ported from commit 05733cfb43a2974cf669f0eb8693f43b548bdcd4 (2024-04-16) on `master`,
|
|
7
|
+
* which carries three fixes that the last npm release, `@ygoe/msgpack@1.0.3`, does not.
|
|
8
|
+
* Every change made to the upstream logic is listed in this package's README, section
|
|
9
|
+
* "Provenance and credits".
|
|
10
|
+
*
|
|
11
|
+
* Original work: Copyright © 2019, Yves Goergen, https://unclassified.software/source/msgpack-js
|
|
12
|
+
* TypeScript port and modifications: Copyright (c) 2026 statewalker
|
|
13
|
+
*
|
|
14
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
15
|
+
* associated documentation files (the “Software”), to deal in the Software without restriction,
|
|
16
|
+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
17
|
+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
* furnished to do so, subject to the following conditions:
|
|
19
|
+
*
|
|
20
|
+
* The above copyright notice and this permission notice shall be included in all copies or
|
|
21
|
+
* substantial portions of the Software.
|
|
22
|
+
*
|
|
23
|
+
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
24
|
+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
25
|
+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
26
|
+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Options for {@link serialize}. */
|
|
31
|
+
export interface SerializeOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Treat `data` as an array of values and concatenate their encodings, one MessagePack document
|
|
34
|
+
* after another, instead of encoding the array itself. Default: `false`.
|
|
35
|
+
*/
|
|
36
|
+
multiple?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* What to write in place of a value whose type cannot be serialized (a function, a symbol, a
|
|
39
|
+
* bigint): either the replacement value itself, or a function that returns one given the
|
|
40
|
+
* original. When absent, such a value throws.
|
|
41
|
+
*/
|
|
42
|
+
invalidTypeReplacement?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Options for {@link deserialize}. */
|
|
46
|
+
export interface DeserializeOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Read every concatenated MessagePack document in the input and return them as an array.
|
|
49
|
+
* Default: `false` — read one document and ignore any bytes after it.
|
|
50
|
+
*/
|
|
51
|
+
multiple?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The byte shapes {@link deserialize} accepts. */
|
|
55
|
+
export type MsgpackInput = Uint8Array | ArrayBuffer | ArrayLike<number>;
|
|
56
|
+
|
|
57
|
+
/** An extension value whose type this implementation does not interpret (anything but a timestamp). */
|
|
58
|
+
export interface MsgpackExtension {
|
|
59
|
+
type: number;
|
|
60
|
+
data: Uint8Array;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pow32 = 0x100000000; // 2^32
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Serializes a value to a MessagePack byte array.
|
|
67
|
+
*
|
|
68
|
+
* The result is a view over a larger internal buffer; send the view, not its `.buffer`.
|
|
69
|
+
*/
|
|
70
|
+
export function serialize(data: unknown, options?: SerializeOptions): Uint8Array<ArrayBuffer> {
|
|
71
|
+
if (options?.multiple && !Array.isArray(data)) {
|
|
72
|
+
throw new Error("Invalid argument type: Expected an Array to serialize multiple values.");
|
|
73
|
+
}
|
|
74
|
+
let floatBuffer: ArrayBuffer | undefined;
|
|
75
|
+
let floatView: DataView | undefined;
|
|
76
|
+
let array = new Uint8Array(128);
|
|
77
|
+
let length = 0;
|
|
78
|
+
if (options?.multiple) {
|
|
79
|
+
const values = data as unknown[];
|
|
80
|
+
for (let i = 0; i < values.length; i++) {
|
|
81
|
+
append(values[i]);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
append(data);
|
|
85
|
+
}
|
|
86
|
+
return array.subarray(0, length);
|
|
87
|
+
|
|
88
|
+
function append(data: unknown, isReplacement?: boolean): void {
|
|
89
|
+
switch (typeof data) {
|
|
90
|
+
case "undefined":
|
|
91
|
+
appendNull();
|
|
92
|
+
break;
|
|
93
|
+
case "boolean":
|
|
94
|
+
appendBoolean(data);
|
|
95
|
+
break;
|
|
96
|
+
case "number":
|
|
97
|
+
appendNumber(data);
|
|
98
|
+
break;
|
|
99
|
+
case "string":
|
|
100
|
+
appendString(data);
|
|
101
|
+
break;
|
|
102
|
+
case "object":
|
|
103
|
+
if (data === null) appendNull();
|
|
104
|
+
else if (data instanceof Date) appendDate(data);
|
|
105
|
+
else if (Array.isArray(data)) appendArray(data);
|
|
106
|
+
else if (data instanceof Uint8Array || data instanceof Uint8ClampedArray)
|
|
107
|
+
appendBinArray(data);
|
|
108
|
+
else if (
|
|
109
|
+
data instanceof Int8Array ||
|
|
110
|
+
data instanceof Int16Array ||
|
|
111
|
+
data instanceof Uint16Array ||
|
|
112
|
+
data instanceof Int32Array ||
|
|
113
|
+
data instanceof Uint32Array ||
|
|
114
|
+
data instanceof Float32Array ||
|
|
115
|
+
data instanceof Float64Array
|
|
116
|
+
)
|
|
117
|
+
appendArray(data);
|
|
118
|
+
else appendObject(data as Record<string, unknown>);
|
|
119
|
+
break;
|
|
120
|
+
default: {
|
|
121
|
+
const replacement = options?.invalidTypeReplacement;
|
|
122
|
+
if (!isReplacement && options && replacement) {
|
|
123
|
+
if (typeof replacement === "function") append(replacement(data), true);
|
|
124
|
+
else append(replacement, true);
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(`Invalid argument type: The type '${typeof data}' cannot be serialized.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function appendNull(): void {
|
|
133
|
+
appendByte(0xc0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function appendBoolean(data: boolean): void {
|
|
137
|
+
appendByte(data ? 0xc3 : 0xc2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function appendNumber(data: number): void {
|
|
141
|
+
if (Number.isFinite(data) && Number.isSafeInteger(data)) {
|
|
142
|
+
// Integer
|
|
143
|
+
if (data >= 0 && data <= 0x7f) {
|
|
144
|
+
appendByte(data);
|
|
145
|
+
} else if (data < 0 && data >= -0x20) {
|
|
146
|
+
appendByte(data);
|
|
147
|
+
} else if (data > 0 && data <= 0xff) {
|
|
148
|
+
// uint8
|
|
149
|
+
appendBytes([0xcc, data]);
|
|
150
|
+
} else if (data >= -0x80 && data <= 0x7f) {
|
|
151
|
+
// int8
|
|
152
|
+
appendBytes([0xd0, data]);
|
|
153
|
+
} else if (data > 0 && data <= 0xffff) {
|
|
154
|
+
// uint16
|
|
155
|
+
appendBytes([0xcd, data >>> 8, data]);
|
|
156
|
+
} else if (data >= -0x8000 && data <= 0x7fff) {
|
|
157
|
+
// int16
|
|
158
|
+
appendBytes([0xd1, data >>> 8, data]);
|
|
159
|
+
} else if (data > 0 && data <= 0xffffffff) {
|
|
160
|
+
// uint32
|
|
161
|
+
appendBytes([0xce, data >>> 24, data >>> 16, data >>> 8, data]);
|
|
162
|
+
} else if (data >= -0x80000000 && data <= 0x7fffffff) {
|
|
163
|
+
// int32
|
|
164
|
+
appendBytes([0xd2, data >>> 24, data >>> 16, data >>> 8, data]);
|
|
165
|
+
} else if (data > 0 && data <= 2 ** 64) {
|
|
166
|
+
// uint64 (upstream writes the bound as 0xffffffffffffffff, which is 2^64 as a double)
|
|
167
|
+
appendByte(0xcf);
|
|
168
|
+
appendInt64(data);
|
|
169
|
+
} else if (data >= -(2 ** 63) && data <= 2 ** 63) {
|
|
170
|
+
// int64 (upstream: -0x8000000000000000 and 0x7fffffffffffffff, i.e. -2^63 and 2^63)
|
|
171
|
+
appendByte(0xd3);
|
|
172
|
+
appendInt64(data);
|
|
173
|
+
} else if (data < 0) {
|
|
174
|
+
// below int64
|
|
175
|
+
appendBytes([0xd3, 0x80, 0, 0, 0, 0, 0, 0, 0]);
|
|
176
|
+
} else {
|
|
177
|
+
// above uint64
|
|
178
|
+
appendBytes([0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Float
|
|
182
|
+
if (!floatView || !floatBuffer) {
|
|
183
|
+
floatBuffer = new ArrayBuffer(8);
|
|
184
|
+
floatView = new DataView(floatBuffer);
|
|
185
|
+
}
|
|
186
|
+
floatView.setFloat64(0, data);
|
|
187
|
+
appendByte(0xcb);
|
|
188
|
+
appendBytes(new Uint8Array(floatBuffer));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function appendString(data: string): void {
|
|
193
|
+
const bytes = encodeUtf8(data);
|
|
194
|
+
const length = bytes.length;
|
|
195
|
+
|
|
196
|
+
if (length <= 0x1f) appendByte(0xa0 + length);
|
|
197
|
+
else if (length <= 0xff) appendBytes([0xd9, length]);
|
|
198
|
+
else if (length <= 0xffff) appendBytes([0xda, length >>> 8, length]);
|
|
199
|
+
else appendBytes([0xdb, length >>> 24, length >>> 16, length >>> 8, length]);
|
|
200
|
+
|
|
201
|
+
appendBytes(bytes);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function appendArray(data: ArrayLike<unknown>): void {
|
|
205
|
+
const length = data.length;
|
|
206
|
+
|
|
207
|
+
if (length <= 0xf) appendByte(0x90 + length);
|
|
208
|
+
else if (length <= 0xffff) appendBytes([0xdc, length >>> 8, length]);
|
|
209
|
+
else appendBytes([0xdd, length >>> 24, length >>> 16, length >>> 8, length]);
|
|
210
|
+
|
|
211
|
+
for (let index = 0; index < length; index++) {
|
|
212
|
+
append(data[index]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function appendBinArray(data: Uint8Array | Uint8ClampedArray): void {
|
|
217
|
+
const length = data.length;
|
|
218
|
+
|
|
219
|
+
if (length <= 0xff) appendBytes([0xc4, length]);
|
|
220
|
+
else if (length <= 0xffff) appendBytes([0xc5, length >>> 8, length]);
|
|
221
|
+
else appendBytes([0xc6, length >>> 24, length >>> 16, length >>> 8, length]);
|
|
222
|
+
|
|
223
|
+
appendBytes(data);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function appendObject(data: Record<string, unknown>): void {
|
|
227
|
+
// Modified from upstream, which walked the keys with for...in and so also wrote enumerable
|
|
228
|
+
// properties inherited from the prototype. Only own enumerable string keys are written, as
|
|
229
|
+
// JSON.stringify and structured clone do.
|
|
230
|
+
const keys = Object.keys(data);
|
|
231
|
+
let length = 0;
|
|
232
|
+
for (const key of keys) {
|
|
233
|
+
if (data[key] !== undefined) {
|
|
234
|
+
length++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (length <= 0xf) appendByte(0x80 + length);
|
|
239
|
+
else if (length <= 0xffff) appendBytes([0xde, length >>> 8, length]);
|
|
240
|
+
else appendBytes([0xdf, length >>> 24, length >>> 16, length >>> 8, length]);
|
|
241
|
+
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
const value = data[key];
|
|
244
|
+
if (value !== undefined) {
|
|
245
|
+
append(key);
|
|
246
|
+
append(value);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function appendDate(data: Date): void {
|
|
252
|
+
// Modified from upstream, which took `getTime() / 1000` unfloored and `getMilliseconds()`:
|
|
253
|
+
// before 1970 that wrote the wrong second (-1002 ms became second -1 plus 998 ms). The
|
|
254
|
+
// second is floored and the nanoseconds are what remains, so both are right on either side
|
|
255
|
+
// of the epoch. An invalid Date is refused instead of being written as second -1.
|
|
256
|
+
const time = data.getTime();
|
|
257
|
+
if (Number.isNaN(time)) {
|
|
258
|
+
throw new Error("Invalid argument: an invalid Date cannot be serialized.");
|
|
259
|
+
}
|
|
260
|
+
const sec = Math.floor(time / 1000);
|
|
261
|
+
const ns = (time - sec * 1000) * 1000000;
|
|
262
|
+
if (ns === 0 && sec >= 0 && sec < 0x100000000) {
|
|
263
|
+
// 32 bit seconds
|
|
264
|
+
appendBytes([0xd6, 0xff, sec >>> 24, sec >>> 16, sec >>> 8, sec]);
|
|
265
|
+
} else if (sec >= 0 && sec < 0x400000000) {
|
|
266
|
+
// 30 bit nanoseconds, 34 bit seconds
|
|
267
|
+
appendBytes([
|
|
268
|
+
0xd7,
|
|
269
|
+
0xff,
|
|
270
|
+
ns >>> 22,
|
|
271
|
+
ns >>> 14,
|
|
272
|
+
ns >>> 6,
|
|
273
|
+
((ns << 2) >>> 0) | (sec / pow32),
|
|
274
|
+
sec >>> 24,
|
|
275
|
+
sec >>> 16,
|
|
276
|
+
sec >>> 8,
|
|
277
|
+
sec,
|
|
278
|
+
]);
|
|
279
|
+
} else {
|
|
280
|
+
// 32 bit nanoseconds, 64 bit seconds, negative values allowed
|
|
281
|
+
appendBytes([0xc7, 12, 0xff, ns >>> 24, ns >>> 16, ns >>> 8, ns]);
|
|
282
|
+
appendInt64(sec);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function appendByte(byte: number): void {
|
|
287
|
+
if (array.length < length + 1) {
|
|
288
|
+
let newLength = array.length * 2;
|
|
289
|
+
while (newLength < length + 1) newLength *= 2;
|
|
290
|
+
const newArray = new Uint8Array(newLength);
|
|
291
|
+
newArray.set(array);
|
|
292
|
+
array = newArray;
|
|
293
|
+
}
|
|
294
|
+
array[length] = byte;
|
|
295
|
+
length++;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function appendBytes(bytes: ArrayLike<number>): void {
|
|
299
|
+
if (array.length < length + bytes.length) {
|
|
300
|
+
let newLength = array.length * 2;
|
|
301
|
+
while (newLength < length + bytes.length) newLength *= 2;
|
|
302
|
+
const newArray = new Uint8Array(newLength);
|
|
303
|
+
newArray.set(array);
|
|
304
|
+
array = newArray;
|
|
305
|
+
}
|
|
306
|
+
array.set(bytes, length);
|
|
307
|
+
length += bytes.length;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function appendInt64(value: number): void {
|
|
311
|
+
// Split 64 bit number into two 32 bit numbers because JavaScript only regards 32 bits for
|
|
312
|
+
// bitwise operations.
|
|
313
|
+
let hi: number;
|
|
314
|
+
let lo: number;
|
|
315
|
+
if (value >= 0) {
|
|
316
|
+
// Same as uint64
|
|
317
|
+
hi = value / pow32;
|
|
318
|
+
lo = value % pow32;
|
|
319
|
+
} else {
|
|
320
|
+
// Split absolute value to high and low, then NOT and ADD(1) to restore negativity
|
|
321
|
+
value++;
|
|
322
|
+
hi = Math.abs(value) / pow32;
|
|
323
|
+
lo = Math.abs(value) % pow32;
|
|
324
|
+
hi = ~hi;
|
|
325
|
+
lo = ~lo;
|
|
326
|
+
}
|
|
327
|
+
appendBytes([hi >>> 24, hi >>> 16, hi >>> 8, hi, lo >>> 24, lo >>> 16, lo >>> 8, lo]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Deserializes a MessagePack byte array to a value.
|
|
333
|
+
*
|
|
334
|
+
* `bin` values are returned as `Uint8Array` views into the input, not copies.
|
|
335
|
+
*/
|
|
336
|
+
export function deserialize(input: MsgpackInput, options?: DeserializeOptions): unknown {
|
|
337
|
+
let pos = 0;
|
|
338
|
+
let array: Uint8Array;
|
|
339
|
+
if (input instanceof Uint8Array) {
|
|
340
|
+
array = input;
|
|
341
|
+
} else if (input instanceof ArrayBuffer) {
|
|
342
|
+
array = new Uint8Array(input);
|
|
343
|
+
} else if (typeof input !== "object" || input === null || typeof input.length === "undefined") {
|
|
344
|
+
throw new Error(
|
|
345
|
+
"Invalid argument type: Expected a byte array (Array or Uint8Array) to deserialize.",
|
|
346
|
+
);
|
|
347
|
+
} else {
|
|
348
|
+
array = new Uint8Array(input);
|
|
349
|
+
}
|
|
350
|
+
if (!array.length) {
|
|
351
|
+
throw new Error("Invalid argument: The byte array to deserialize is empty.");
|
|
352
|
+
}
|
|
353
|
+
let data: unknown;
|
|
354
|
+
if (options?.multiple) {
|
|
355
|
+
// Read as many messages as are available
|
|
356
|
+
const values: unknown[] = [];
|
|
357
|
+
while (pos < array.length) {
|
|
358
|
+
values.push(read());
|
|
359
|
+
}
|
|
360
|
+
data = values;
|
|
361
|
+
} else {
|
|
362
|
+
// Read only one message and ignore additional data
|
|
363
|
+
data = read();
|
|
364
|
+
}
|
|
365
|
+
return data;
|
|
366
|
+
|
|
367
|
+
// Modified from upstream, which read past the end of the input without noticing: a cut integer
|
|
368
|
+
// came back as NaN, a cut bin as a shorter view, and other cuts threw assorted TypeErrors after
|
|
369
|
+
// logging the whole input with console.debug. Every read now checks that its bytes are present
|
|
370
|
+
// and throws a RangeError when they are not, and nothing is logged.
|
|
371
|
+
function need(size: number): void {
|
|
372
|
+
if (pos + size > array.length) {
|
|
373
|
+
throw new RangeError(
|
|
374
|
+
`Insufficient data: ${size} byte(s) needed at index ${pos} in the MessagePack binary data (length ${array.length}).`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function read(): unknown {
|
|
380
|
+
need(1);
|
|
381
|
+
const byte = array[pos++] as number;
|
|
382
|
+
if (byte >= 0x00 && byte <= 0x7f) return byte; // positive fixint
|
|
383
|
+
if (byte >= 0x80 && byte <= 0x8f) return readMap(byte - 0x80); // fixmap
|
|
384
|
+
if (byte >= 0x90 && byte <= 0x9f) return readArray(byte - 0x90); // fixarray
|
|
385
|
+
if (byte >= 0xa0 && byte <= 0xbf) return readStr(byte - 0xa0); // fixstr
|
|
386
|
+
if (byte === 0xc0) return null; // nil
|
|
387
|
+
if (byte === 0xc1) throw new Error("Invalid byte code 0xc1 found."); // never used
|
|
388
|
+
if (byte === 0xc2) return false; // false
|
|
389
|
+
if (byte === 0xc3) return true; // true
|
|
390
|
+
if (byte === 0xc4) return readBin(-1, 1); // bin 8
|
|
391
|
+
if (byte === 0xc5) return readBin(-1, 2); // bin 16
|
|
392
|
+
if (byte === 0xc6) return readBin(-1, 4); // bin 32
|
|
393
|
+
if (byte === 0xc7) return readExt(-1, 1); // ext 8
|
|
394
|
+
if (byte === 0xc8) return readExt(-1, 2); // ext 16
|
|
395
|
+
if (byte === 0xc9) return readExt(-1, 4); // ext 32
|
|
396
|
+
if (byte === 0xca) return readFloat(4); // float 32
|
|
397
|
+
if (byte === 0xcb) return readFloat(8); // float 64
|
|
398
|
+
if (byte === 0xcc) return readUInt(1); // uint 8
|
|
399
|
+
if (byte === 0xcd) return readUInt(2); // uint 16
|
|
400
|
+
if (byte === 0xce) return readUInt(4); // uint 32
|
|
401
|
+
if (byte === 0xcf) return readUInt(8); // uint 64
|
|
402
|
+
if (byte === 0xd0) return readInt(1); // int 8
|
|
403
|
+
if (byte === 0xd1) return readInt(2); // int 16
|
|
404
|
+
if (byte === 0xd2) return readInt(4); // int 32
|
|
405
|
+
if (byte === 0xd3) return readInt(8); // int 64
|
|
406
|
+
if (byte === 0xd4) return readExt(1); // fixext 1
|
|
407
|
+
if (byte === 0xd5) return readExt(2); // fixext 2
|
|
408
|
+
if (byte === 0xd6) return readExt(4); // fixext 4
|
|
409
|
+
if (byte === 0xd7) return readExt(8); // fixext 8
|
|
410
|
+
if (byte === 0xd8) return readExt(16); // fixext 16
|
|
411
|
+
if (byte === 0xd9) return readStr(-1, 1); // str 8
|
|
412
|
+
if (byte === 0xda) return readStr(-1, 2); // str 16
|
|
413
|
+
if (byte === 0xdb) return readStr(-1, 4); // str 32
|
|
414
|
+
if (byte === 0xdc) return readArray(-1, 2); // array 16
|
|
415
|
+
if (byte === 0xdd) return readArray(-1, 4); // array 32
|
|
416
|
+
if (byte === 0xde) return readMap(-1, 2); // map 16
|
|
417
|
+
if (byte === 0xdf) return readMap(-1, 4); // map 32
|
|
418
|
+
if (byte >= 0xe0 && byte <= 0xff) return byte - 256; // negative fixint
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Invalid byte value '${byte}' at index ${pos - 1} in the MessagePack binary data (length ${array.length}): Expecting a range of 0 to 255. This is not a byte array.`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function readInt(size: number): number {
|
|
425
|
+
need(size);
|
|
426
|
+
let value = 0;
|
|
427
|
+
let first = true;
|
|
428
|
+
while (size-- > 0) {
|
|
429
|
+
if (first) {
|
|
430
|
+
const byte = array[pos++] as number;
|
|
431
|
+
value += byte & 0x7f;
|
|
432
|
+
if (byte & 0x80) {
|
|
433
|
+
value -= 0x80; // Treat most-significant bit as -2^i instead of 2^i
|
|
434
|
+
}
|
|
435
|
+
first = false;
|
|
436
|
+
} else {
|
|
437
|
+
value *= 256;
|
|
438
|
+
value += array[pos++] as number;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function readUInt(size: number): number {
|
|
445
|
+
need(size);
|
|
446
|
+
let value = 0;
|
|
447
|
+
while (size-- > 0) {
|
|
448
|
+
value *= 256;
|
|
449
|
+
value += array[pos++] as number;
|
|
450
|
+
}
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function readFloat(size: 4 | 8): number {
|
|
455
|
+
need(size);
|
|
456
|
+
const view = new DataView(array.buffer, pos + array.byteOffset, size);
|
|
457
|
+
pos += size;
|
|
458
|
+
return size === 4 ? view.getFloat32(0, false) : view.getFloat64(0, false);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function readBin(size: number, lengthSize?: number): Uint8Array {
|
|
462
|
+
if (size < 0) size = readUInt(lengthSize as number);
|
|
463
|
+
need(size);
|
|
464
|
+
const data = array.subarray(pos, pos + size);
|
|
465
|
+
pos += size;
|
|
466
|
+
return data;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function readMap(size: number, lengthSize?: number): Record<string, unknown> {
|
|
470
|
+
if (size < 0) size = readUInt(lengthSize as number);
|
|
471
|
+
const data: Record<string, unknown> = {};
|
|
472
|
+
while (size-- > 0) {
|
|
473
|
+
const key = String(read());
|
|
474
|
+
const value = read();
|
|
475
|
+
// Modified from upstream, which assigned every key: `data["__proto__"] = value` replaces the
|
|
476
|
+
// object's prototype instead of adding a key (or does nothing, for a primitive value). That
|
|
477
|
+
// one key is defined as an own data property, as JSON.parse does.
|
|
478
|
+
if (key === "__proto__") {
|
|
479
|
+
Object.defineProperty(data, key, {
|
|
480
|
+
value,
|
|
481
|
+
enumerable: true,
|
|
482
|
+
writable: true,
|
|
483
|
+
configurable: true,
|
|
484
|
+
});
|
|
485
|
+
} else {
|
|
486
|
+
data[key] = value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return data;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function readArray(size: number, lengthSize?: number): unknown[] {
|
|
493
|
+
if (size < 0) size = readUInt(lengthSize as number);
|
|
494
|
+
const data: unknown[] = [];
|
|
495
|
+
while (size-- > 0) {
|
|
496
|
+
data.push(read());
|
|
497
|
+
}
|
|
498
|
+
return data;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function readStr(size: number, lengthSize?: number): string {
|
|
502
|
+
if (size < 0) size = readUInt(lengthSize as number);
|
|
503
|
+
need(size);
|
|
504
|
+
const start = pos;
|
|
505
|
+
pos += size;
|
|
506
|
+
return decodeUtf8(array, start, size);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function readExt(size: number, lengthSize?: number): Date | MsgpackExtension {
|
|
510
|
+
if (size < 0) size = readUInt(lengthSize as number);
|
|
511
|
+
const type = readUInt(1);
|
|
512
|
+
const data = readBin(size);
|
|
513
|
+
switch (type) {
|
|
514
|
+
case 255:
|
|
515
|
+
return readExtDate(data);
|
|
516
|
+
}
|
|
517
|
+
return { type: type, data: data };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Modified from upstream, which built the Date from `sec * 1000 + ns / 1000000`. That sum is a
|
|
521
|
+
// double: it rounds 2038-01-19T03:14:07.999999999Z up to the next second, and before 1970 the
|
|
522
|
+
// Date constructor truncates it toward zero. Flooring the nanoseconds to whole milliseconds
|
|
523
|
+
// first keeps the arithmetic exact and gives the millisecond the instant falls in.
|
|
524
|
+
function readExtDate(data: Uint8Array): Date {
|
|
525
|
+
const b = (i: number) => data[i] as number;
|
|
526
|
+
if (data.length === 4) {
|
|
527
|
+
const sec = ((b(0) << 24) >>> 0) + ((b(1) << 16) >>> 0) + ((b(2) << 8) >>> 0) + b(3);
|
|
528
|
+
return new Date(sec * 1000);
|
|
529
|
+
}
|
|
530
|
+
if (data.length === 8) {
|
|
531
|
+
const ns = ((b(0) << 22) >>> 0) + ((b(1) << 14) >>> 0) + ((b(2) << 6) >>> 0) + (b(3) >>> 2);
|
|
532
|
+
const sec =
|
|
533
|
+
(b(3) & 0x3) * pow32 +
|
|
534
|
+
((b(4) << 24) >>> 0) +
|
|
535
|
+
((b(5) << 16) >>> 0) +
|
|
536
|
+
((b(6) << 8) >>> 0) +
|
|
537
|
+
b(7);
|
|
538
|
+
return new Date(sec * 1000 + Math.floor(ns / 1000000));
|
|
539
|
+
}
|
|
540
|
+
if (data.length === 12) {
|
|
541
|
+
const ns = ((b(0) << 24) >>> 0) + ((b(1) << 16) >>> 0) + ((b(2) << 8) >>> 0) + b(3);
|
|
542
|
+
pos -= 8;
|
|
543
|
+
const sec = readInt(8);
|
|
544
|
+
return new Date(sec * 1000 + Math.floor(ns / 1000000));
|
|
545
|
+
}
|
|
546
|
+
throw new Error("Invalid data length for a date value.");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Encodes a string to UTF-8 bytes. */
|
|
551
|
+
function encodeUtf8(str: string): Uint8Array {
|
|
552
|
+
// Prevent excessive array allocation and slicing for all 7-bit characters
|
|
553
|
+
let ascii = true;
|
|
554
|
+
const length = str.length;
|
|
555
|
+
for (let x = 0; x < length; x++) {
|
|
556
|
+
if (str.charCodeAt(x) > 127) {
|
|
557
|
+
ascii = false;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Based on: https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330
|
|
563
|
+
let i = 0;
|
|
564
|
+
const bytes = new Uint8Array(str.length * (ascii ? 1 : 4));
|
|
565
|
+
for (let ci = 0; ci !== length; ci++) {
|
|
566
|
+
let c = str.charCodeAt(ci);
|
|
567
|
+
if (c < 128) {
|
|
568
|
+
bytes[i++] = c;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (c < 2048) {
|
|
572
|
+
bytes[i++] = (c >> 6) | 192;
|
|
573
|
+
} else {
|
|
574
|
+
// Modified from upstream, which threw on a lone high surrogate and wrote a lone low one as
|
|
575
|
+
// three bytes that are not valid UTF-8. A surrogate without its partner is written as
|
|
576
|
+
// U+FFFD, as TextEncoder does, so the output is always valid UTF-8.
|
|
577
|
+
const c2 = c > 0xd7ff && c < 0xdc00 && ci + 1 < length ? str.charCodeAt(ci + 1) : 0;
|
|
578
|
+
if (c2 >= 0xdc00 && c2 <= 0xdfff) {
|
|
579
|
+
ci++;
|
|
580
|
+
c = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff);
|
|
581
|
+
bytes[i++] = (c >> 18) | 240;
|
|
582
|
+
bytes[i++] = ((c >> 12) & 63) | 128;
|
|
583
|
+
} else {
|
|
584
|
+
if (c > 0xd7ff && c < 0xe000) c = 0xfffd;
|
|
585
|
+
bytes[i++] = (c >> 12) | 224;
|
|
586
|
+
}
|
|
587
|
+
bytes[i++] = ((c >> 6) & 63) | 128;
|
|
588
|
+
}
|
|
589
|
+
bytes[i++] = (c & 63) | 128;
|
|
590
|
+
}
|
|
591
|
+
return ascii ? bytes : bytes.subarray(0, i);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Modified from upstream, whose hand-written decoder did not validate: it accepted overlong forms
|
|
595
|
+
// (C0 AF read as "/"), took any byte for a continuation byte, and threw on a sequence cut short
|
|
596
|
+
// by the end of the string. Anything that is not plain ASCII now goes through TextDecoder, which
|
|
597
|
+
// replaces each malformed sequence with U+FFFD per the WHATWG Encoding standard. `ignoreBOM`
|
|
598
|
+
// keeps a leading U+FEFF, which TextDecoder would otherwise strip from the string's content.
|
|
599
|
+
const utf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
600
|
+
|
|
601
|
+
/** Decodes a string from UTF-8 bytes. */
|
|
602
|
+
function decodeUtf8(bytes: Uint8Array, start: number, length: number): string {
|
|
603
|
+
const end = start + length;
|
|
604
|
+
// A very short ASCII string — a typical map key — is cheaper to build here than to hand to
|
|
605
|
+
// TextDecoder. Measured on Node 24, 2M decodes: 4 bytes 51 ms here vs 234 ms, break-even near
|
|
606
|
+
// 12 bytes, 32 bytes 422 ms vs 197 ms.
|
|
607
|
+
if (length <= 12) {
|
|
608
|
+
let str = "";
|
|
609
|
+
for (let i = start; i < end; i++) {
|
|
610
|
+
const c = bytes[i] as number;
|
|
611
|
+
if (c > 127) return utf8Decoder.decode(bytes.subarray(start, end));
|
|
612
|
+
str += String.fromCharCode(c);
|
|
613
|
+
}
|
|
614
|
+
return str;
|
|
615
|
+
}
|
|
616
|
+
return utf8Decoder.decode(bytes.subarray(start, end));
|
|
617
|
+
}
|
package/src/msgpack.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const { serialize, deserialize } = msgpack;
|
|
1
|
+
import { deserialize, serialize } from "./msgpack-core.js";
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Encode each value as a length-prefixed msgpack frame.
|
|
7
5
|
* Frame format: [4-byte big-endian length][msgpack bytes]
|
|
6
|
+
*
|
|
7
|
+
* The input may be synchronous (an array, a generator) or asynchronous.
|
|
8
8
|
*/
|
|
9
|
-
export async function* encodeMsgpack<T>(
|
|
9
|
+
export async function* encodeMsgpack<T>(
|
|
10
|
+
input: Iterable<T> | AsyncIterable<T>,
|
|
11
|
+
): AsyncGenerator<Uint8Array> {
|
|
10
12
|
for await (const item of input) {
|
|
11
13
|
const payload = serialize(item);
|
|
12
14
|
const frame = new Uint8Array(4 + payload.length);
|
|
@@ -19,8 +21,12 @@ export async function* encodeMsgpack<T>(input: AsyncIterable<T>): AsyncGenerator
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Decode length-prefixed msgpack frames, reassembling across chunk boundaries.
|
|
24
|
+
*
|
|
25
|
+
* The input may be synchronous (an array of chunks) or asynchronous.
|
|
22
26
|
*/
|
|
23
|
-
export async function* decodeMsgpack<T>(
|
|
27
|
+
export async function* decodeMsgpack<T>(
|
|
28
|
+
input: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
29
|
+
): AsyncGenerator<T> {
|
|
24
30
|
let buffer: Uint8Array = new Uint8Array(0);
|
|
25
31
|
|
|
26
32
|
for await (const chunk of input) {
|
|
@@ -41,9 +47,11 @@ export async function* decodeMsgpack<T>(input: AsyncIterable<Uint8Array>): Async
|
|
|
41
47
|
/**
|
|
42
48
|
* Encode each Float32Array as a msgpack frame.
|
|
43
49
|
* The Float32Array is converted to a Uint8Array view (zero-copy) before encoding as msgpack bin.
|
|
50
|
+
*
|
|
51
|
+
* The input may be synchronous (an array, a generator) or asynchronous.
|
|
44
52
|
*/
|
|
45
53
|
export async function* encodeFloat32Arrays(
|
|
46
|
-
input: AsyncIterable<Float32Array>,
|
|
54
|
+
input: Iterable<Float32Array> | AsyncIterable<Float32Array>,
|
|
47
55
|
): AsyncGenerator<Uint8Array> {
|
|
48
56
|
for await (const arr of input) {
|
|
49
57
|
const bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
@@ -59,11 +67,13 @@ export async function* encodeFloat32Arrays(
|
|
|
59
67
|
/**
|
|
60
68
|
* Decode msgpack frames back to Float32Array.
|
|
61
69
|
* Each frame contains a msgpack bin value (Uint8Array), reinterpreted as Float32Array.
|
|
70
|
+
*
|
|
71
|
+
* The input may be synchronous (an array of chunks) or asynchronous.
|
|
62
72
|
*/
|
|
63
73
|
export async function* decodeFloat32Arrays(
|
|
64
|
-
input: AsyncIterable<Uint8Array>,
|
|
74
|
+
input: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
65
75
|
): AsyncGenerator<Float32Array> {
|
|
66
|
-
for await (const item of decodeMsgpack<Uint8Array>(
|
|
76
|
+
for await (const item of decodeMsgpack<Uint8Array>(input)) {
|
|
67
77
|
const aligned = alignBuffer(item);
|
|
68
78
|
yield new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4);
|
|
69
79
|
}
|
|
@@ -83,7 +93,3 @@ function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
83
93
|
result.set(b, a.length);
|
|
84
94
|
return result;
|
|
85
95
|
}
|
|
86
|
-
|
|
87
|
-
async function* wrapIterable<T>(input: AsyncIterable<T>): AsyncGenerator<T> {
|
|
88
|
-
yield* input;
|
|
89
|
-
}
|
package/src/port-codec.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { PortCodec, PortEnvelope } from "@statewalker/webrun-rpc";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
const { serialize, deserialize } = msgpack;
|
|
2
|
+
import { deserialize, serialize } from "./msgpack-core.js";
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Same shape check as `structuredCodec`'s, applied after decoding. A shared
|