Skip to content

Commit 9737a9b

Browse files
committed
WIP for structured clone algo
1 parent 9600b30 commit 9737a9b

File tree

3 files changed

+174
-44
lines changed

3 files changed

+174
-44
lines changed

src/JavaScriptCodec.ts

Lines changed: 130 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,151 @@
1+
// Implementation of "Structured Clone" algorithm in MessagPack
2+
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
3+
14
import { ExtensionCodec, ExtensionCodecType } from "./ExtensionCodec";
25
import { encode } from "./encode";
36
import { decode } from "./decode";
47

58
export const EXT_JAVASCRIPT = 0;
69

7-
const enum JSData {
8-
Map,
9-
Set,
10-
Date,
11-
RegExp,
12-
BigInt,
10+
const enum JS {
11+
// defined in "structured clone algorithm"
12+
// commente-outed ones are TODOs
13+
14+
// Boolean = "Boolean",
15+
// String = "String",
16+
Date = "Date",
17+
RegExp = "RegExp",
18+
// Blob = "Blob",
19+
// File = "File",
20+
// FileList = "FileList",
21+
ArrayBuffer = "ArrayBuffer",
22+
Int8Array = "Int8Array",
23+
Uint8Array = "Uint8Array",
24+
Uint8ClampedArray = "Uint8ClampedArray",
25+
Int16Array = "Int16Array",
26+
Uint16Array = "Uint16Array",
27+
Int32Array = "Int32Array",
28+
Uint32Array = "Uint32Array",
29+
Float32Array = "Float32Array",
30+
Float64Array = "Float64Array",
31+
BigInt64Array = "BigInt64Array",
32+
BigUint64Array = "BigUint64Array",
33+
DataView = "DataView",
34+
// ImageBitMap = "ImageBitMap",
35+
// ImageData = "ImageData",
36+
Map = "Map",
37+
Set = "Set",
38+
39+
// and more
40+
BigInt = "BigInt",
1341
}
1442

15-
export function encodeJavaScriptData(input: unknown): Uint8Array | null {
16-
if (input instanceof Map) {
17-
return encode([JSData.Map, [...input]]);
18-
} else if (input instanceof Set) {
19-
return encode([JSData.Set, [...input]]);
20-
} else if (input instanceof Date) {
21-
// Not a MessagePack timestamp because
22-
// it may be overrided by users
23-
return encode([JSData.Date, input.getTime()]);
24-
} else if (input instanceof RegExp) {
25-
return encode([JSData.RegExp, [input.source, input.flags]]);
26-
} else if (typeof input === "bigint") {
27-
return encode([JSData.BigInt, input.toString()]);
43+
export function encodeJavaScriptStructure(input: unknown): Uint8Array | null {
44+
if (!(input instanceof Object)) {
45+
if (typeof input === "bigint") {
46+
return encode([JS.BigInt, input.toString()]);
47+
} else {
48+
return null;
49+
}
50+
}
51+
const type = input.constructor.name;
52+
53+
if (ArrayBuffer.isView(input)) {
54+
if (type === JS.Uint8Array) {
55+
return null; // fall through to the default encoder
56+
} else if (type === JS.DataView || type === JS.Int8Array || type === JS.Uint8ClampedArray) {
57+
// handles them as a byte buffer
58+
const v = new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
59+
return encode([type, v]);
60+
} else {
61+
// handles them as a number array for portability
62+
return encode([type, ...(input as unknown as Iterable<number>)]);
63+
}
64+
} else if (type === JS.ArrayBuffer) {
65+
const bufferView = new Uint8Array(input as ArrayBuffer);
66+
return encode([type, bufferView]);
67+
} else if (type === JS.Map) {
68+
return encode([JS.Map, ...input as Map<unknown, unknown>]);
69+
} else if (type === JS.Set) {
70+
return encode([JS.Set, ...input as Set<unknown>]);
71+
} else if (type === JS.Date) {
72+
return encode([JS.Date, (input as Date).getTime()]);
73+
} else if (type === JS.RegExp) {
74+
return encode([JS.RegExp, (input as RegExp).source, (input as RegExp).flags]);
2875
} else {
2976
return null;
3077
}
3178
}
3279

33-
export function decodeJavaScriptData(data: Uint8Array) {
34-
const [jsDataType, source] = decode(data) as [JSData, any];
35-
36-
switch (jsDataType) {
37-
case JSData.Map: {
38-
return new Map<unknown, unknown>(source);
39-
}
40-
case JSData.Set: {
41-
return new Set<unknown>(source);
80+
export function decodeJavaScriptStructure(data: Uint8Array) {
81+
const [type, ...source] = decode(data) as [JS, ...any];
82+
switch (type) {
83+
case JS.BigInt: {
84+
const [str] = source;
85+
return BigInt(str);
4286
}
43-
case JSData.Date: {
44-
return new Date(source);
87+
case JS.Date: {
88+
const [millis] = source;
89+
return new Date(millis);
4590
}
46-
case JSData.RegExp: {
91+
case JS.RegExp: {
4792
const [pattern, flags] = source;
4893
return new RegExp(pattern, flags);
4994
}
50-
case JSData.BigInt: {
51-
return BigInt(source);
95+
case JS.ArrayBuffer: {
96+
const [buffer] = source as [Uint8Array];
97+
return buffer.slice(0).buffer;
98+
}
99+
case JS.Int8Array: {
100+
const [v] = source as [Uint8Array];
101+
return new Int8Array(v.buffer, v.byteOffset, v.byteLength);
102+
}
103+
case JS.Uint8Array: {
104+
// unlikely because it is handled by the default decoder,
105+
// but technically possible with no conflict.
106+
const [v] = source as [Uint8Array];
107+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
108+
}
109+
case JS.Uint8ClampedArray: {
110+
const [v] = source as [Uint8Array];
111+
return new Uint8ClampedArray(v.buffer, v.byteOffset, v.byteLength);
112+
}
113+
case JS.Int16Array: {
114+
return Int16Array.from(source as ReadonlyArray<number>);
115+
}
116+
case JS.Uint16Array: {
117+
return Uint16Array.from(source as ReadonlyArray<number>);
118+
}
119+
case JS.Int32Array: {
120+
return Int32Array.from(source as ReadonlyArray<number>);
121+
}
122+
case JS.Uint32Array: {
123+
return Uint32Array.from(source as ReadonlyArray<number>);
124+
}
125+
case JS.Float32Array: {
126+
return Float32Array.from(source as ReadonlyArray<number>);
127+
}
128+
case JS.Float64Array: {
129+
return Float64Array.from(source as ReadonlyArray<number>);
130+
}
131+
case JS.BigInt64Array: {
132+
return BigInt64Array.from(source as ReadonlyArray<bigint>);
133+
}
134+
case JS.BigUint64Array: {
135+
return BigUint64Array.from(source as ReadonlyArray<bigint>);
136+
}
137+
case JS.DataView: {
138+
const [v] = source as [Uint8Array];
139+
return new DataView(v.buffer, v.byteOffset, v.byteLength);
140+
}
141+
case JS.Map: {
142+
return new Map(source);
143+
}
144+
case JS.Set: {
145+
return new Set(source);
52146
}
53147
default: {
54-
throw new Error(`Unknown data type: ${jsDataType}`);
148+
throw new Error(`Unknown data type: ${type}`);
55149
}
56150
}
57151
}
@@ -61,8 +155,8 @@ export const JavaScriptCodec: ExtensionCodecType<undefined> = (() => {
61155

62156
ext.register({
63157
type: EXT_JAVASCRIPT,
64-
encode: encodeJavaScriptData,
65-
decode: decodeJavaScriptData,
158+
encode: encodeJavaScriptStructure,
159+
decode: decodeJavaScriptStructure,
66160
});
67161

68162
return ext;

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,15 @@ export {
4545
decodeTimestampExtension,
4646
};
4747

48-
export { JavaScriptCodec, EXT_JAVASCRIPT, encodeJavaScriptData, decodeJavaScriptData } from "./JavaScriptCodec";
49-
48+
import {
49+
JavaScriptCodec,
50+
EXT_JAVASCRIPT,
51+
encodeJavaScriptStructure,
52+
decodeJavaScriptStructure,
53+
} from "./JavaScriptCodec";
54+
export {
55+
JavaScriptCodec,
56+
EXT_JAVASCRIPT,
57+
encodeJavaScriptStructure,
58+
decodeJavaScriptStructure,
59+
};

test/javascript-codec.test.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,51 @@ import { encode, decode, JavaScriptCodec } from "@msgpack/msgpack";
33

44
describe("JavaScriptCodec", () => {
55
context("mixed", () => {
6-
// this data comes from https://github.com/yahoo/serialize-javascript
7-
8-
it("encodes and decodes the object", () => {
6+
it("encodes and decodes structured data", () => {
97
const object = {
8+
// basic
109
str: "string",
1110
num: 0,
1211
obj: { foo: "foo", bar: "bar" },
1312
arr: [1, 2, 3],
1413
bool: true,
1514
nil: null,
16-
// undef: undefined, // not supported
15+
16+
// JavaScript structures
1717
date: new Date("Thu, 28 Apr 2016 22:02:17 GMT"),
18+
regexp: /foo\n/i,
19+
arrayBuffer: Uint8Array.from([0, 1, 2, 0xff]).buffer,
20+
int8Array: Int8Array.from([0, 1, 2, 0xff]),
21+
uint8ClampedArray: Uint8ClampedArray.from([0, 1, 2, 0xff]),
22+
int16Array: Int16Array.from([0, 1, 2, 0xffff]),
23+
uint16Array: Uint16Array.from([0, 1, 2, -1]),
24+
int32Array: Int32Array.from([0, 1, 2, 0xffff]),
25+
uint32Array: Uint32Array.from([0, 1, 2, -1]),
26+
float32Array: Float32Array.from([0, 1, 2, Math.PI, Math.E]),
27+
float64Array: Float64Array.from([0, 1, 2, Math.PI, Math.E]),
1828
map: new Map([["foo", 10], ["bar", 20]]),
1929
set: new Set([123, 456]),
20-
regexp: /foo\n/i,
21-
bigint: typeof(BigInt) !== "undefined" ? BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1) : null,
2230
};
2331
const encoded = encode(object, { extensionCodec: JavaScriptCodec });
32+
assert.deepStrictEqual(decode(encoded, { extensionCodec: JavaScriptCodec }), object);
33+
});
34+
});
35+
36+
context("bigint and its family", () => {
37+
it("encodes and decodes structured data", function () {
38+
if (typeof BigInt === "undefined" || typeof BigInt64Array === "undefined" || typeof BigUint64Array === "undefined") {
39+
this.skip();
40+
}
41+
42+
const object = {
43+
bigint: BigInt(42),
44+
bigintArray: [BigInt(42)],
2445

46+
// TODO:
47+
//bigInt64array: BigInt64Array.from([BigInt(0), BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1)]),
48+
//bigUint64array: BigUint64Array.from([BigInt(0), BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1)]),
49+
};
50+
const encoded = encode(object, { extensionCodec: JavaScriptCodec });
2551
assert.deepStrictEqual(decode(encoded, { extensionCodec: JavaScriptCodec }), object);
2652
});
2753
});

0 commit comments

Comments
 (0)