4.2 KB140 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8/**
9 * Types that are compatible with JSON.stringify and can be sent over the transport,
10 * Plus Map, Set, Date, Error are supported (they are converted to objects before serializing)
11 */
12export type Serializable =
13 | string
14 | number
15 | boolean
16 | null
17 | undefined
18 | Map<Serializable, Serializable>
19 | Set<Serializable>
20 | Error
21 | Date
22 | {[key: string]: Serializable}
23 | ReadonlyArray<Serializable>;
24
25export type Serialized =
26 | string
27 | number
28 | boolean
29 | null
30 | undefined
31 | Array<Serialized>
32 | CustomSerialized;
33
34export type CustomSerialized =
35 | {__rpcType: 'undefined'}
36 | {__rpcType: 'object'; [key: string]: Serialized}
37 | {__rpcType: 'Error'; data: {message: string; stack?: string}}
38 | {__rpcType: 'Map'; data: Array<[Serialized, Serialized]>}
39 | {__rpcType: 'Set'; data: Array<Serialized>}
40 | {__rpcType: 'Date'; data: number};
41
42const UNDEFINED_SERIALIZED = {__rpcType: 'undefined' as const};
43
44/**
45 * Prepare function arguments/return value to be serialized. This lets you pass Map/Set/RegExp to rpc functions.
46 * Note that we need to do this recursively for arguments to Map/Set, since you can have complex nesting like Map<Set<>, Map<>>
47 */
48export function serialize(arg: Serializable): Serialized {
49 // 'undefined' is not valid JSON, so it will be converted to 'null' when serialized
50 // Therefore, we must serialize it ourselves
51 if (arg === undefined) {
52 return UNDEFINED_SERIALIZED;
53 }
54
55 if (
56 typeof arg === 'number' ||
57 typeof arg === 'boolean' ||
58 typeof arg === 'string' ||
59 arg === null
60 ) {
61 return arg;
62 }
63
64 if (arg instanceof Map) {
65 return {
66 __rpcType: 'Map',
67 data: Array.from(arg.entries()).map(([key, val]) => [serialize(key), serialize(val)]),
68 } as CustomSerialized;
69 } else if (arg instanceof Set) {
70 return {__rpcType: 'Set', data: Array.from(arg.values()).map(serialize)} as CustomSerialized;
71 } else if (arg instanceof Error) {
72 return {__rpcType: 'Error', data: {message: arg.message, stack: arg.stack}} as CustomSerialized;
73 } else if (arg instanceof Date) {
74 return {__rpcType: 'Date', data: arg.valueOf()} as CustomSerialized;
75 } else if (Array.isArray(arg)) {
76 return arg.map(a => serialize(a));
77 } else if (typeof arg === 'object') {
78 const newObj: CustomSerialized & {__rpcType: 'object'} = {__rpcType: 'object'};
79 for (const [propertyName, propertyValue] of Object.entries(arg)) {
80 newObj[propertyName] = serialize(propertyValue);
81 }
82
83 return newObj;
84 }
85
86 throw new Error(`cannot serialize argument ${arg}`);
87}
88
89export function serializeToString(data: Serializable): string {
90 return JSON.stringify(serialize(data));
91}
92
93/**
94 * Restore function arguments/return value after deserializing. This lets you recover passed Map/Set/Date/Error during remote transport.
95 */
96export function deserialize(arg: Serialized): Serializable {
97 if (typeof arg !== 'object' || arg == null) {
98 return arg;
99 }
100
101 if (Array.isArray(arg)) {
102 return arg.map(a => deserialize(a));
103 }
104
105 const specific = arg as CustomSerialized;
106 switch (specific.__rpcType) {
107 case 'undefined':
108 return undefined;
109 case 'Map':
110 return new Map(specific.data.map(([key, value]) => [deserialize(key), deserialize(value)]));
111 case 'Set':
112 return new Set(specific.data.map(deserialize));
113 case 'Error': {
114 const e = new Error();
115 e.stack = specific.data.stack;
116 e.message = specific.data.message;
117 return e;
118 }
119 case 'Date':
120 return new Date(specific.data);
121 case 'object': {
122 const standardObject = arg as {[key: string]: Serialized};
123 const newObj: {[key: string]: Serializable} = {};
124 for (const [propertyName, propertyValue] of Object.entries(standardObject)) {
125 if (propertyName !== '__rpcType') {
126 newObj[propertyName] = deserialize(propertyValue);
127 }
128 }
129 return newObj;
130 }
131 default: {
132 throw new Error(`cannot deserialize unknown type ${specific}`);
133 }
134 }
135}
136
137export function deserializeFromString(data: string): Serializable {
138 return deserialize(JSON.parse(data));
139}
140