addons/isl/src/serialize.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318/**
b69ab319 * Types that are compatible with JSON.stringify and can be sent over the transport,
b69ab3110 * Plus Map, Set, Date, Error are supported (they are converted to objects before serializing)
b69ab3111 */
b69ab3112export type Serializable =
b69ab3113 | string
b69ab3114 | number
b69ab3115 | boolean
b69ab3116 | null
b69ab3117 | undefined
b69ab3118 | Map<Serializable, Serializable>
b69ab3119 | Set<Serializable>
b69ab3120 | Error
b69ab3121 | Date
b69ab3122 | {[key: string]: Serializable}
b69ab3123 | ReadonlyArray<Serializable>;
b69ab3124
b69ab3125export type Serialized =
b69ab3126 | string
b69ab3127 | number
b69ab3128 | boolean
b69ab3129 | null
b69ab3130 | undefined
b69ab3131 | Array<Serialized>
b69ab3132 | CustomSerialized;
b69ab3133
b69ab3134export type CustomSerialized =
b69ab3135 | {__rpcType: 'undefined'}
b69ab3136 | {__rpcType: 'object'; [key: string]: Serialized}
b69ab3137 | {__rpcType: 'Error'; data: {message: string; stack?: string}}
b69ab3138 | {__rpcType: 'Map'; data: Array<[Serialized, Serialized]>}
b69ab3139 | {__rpcType: 'Set'; data: Array<Serialized>}
b69ab3140 | {__rpcType: 'Date'; data: number};
b69ab3141
b69ab3142const UNDEFINED_SERIALIZED = {__rpcType: 'undefined' as const};
b69ab3143
b69ab3144/**
b69ab3145 * Prepare function arguments/return value to be serialized. This lets you pass Map/Set/RegExp to rpc functions.
b69ab3146 * Note that we need to do this recursively for arguments to Map/Set, since you can have complex nesting like Map<Set<>, Map<>>
b69ab3147 */
b69ab3148export function serialize(arg: Serializable): Serialized {
b69ab3149 // 'undefined' is not valid JSON, so it will be converted to 'null' when serialized
b69ab3150 // Therefore, we must serialize it ourselves
b69ab3151 if (arg === undefined) {
b69ab3152 return UNDEFINED_SERIALIZED;
b69ab3153 }
b69ab3154
b69ab3155 if (
b69ab3156 typeof arg === 'number' ||
b69ab3157 typeof arg === 'boolean' ||
b69ab3158 typeof arg === 'string' ||
b69ab3159 arg === null
b69ab3160 ) {
b69ab3161 return arg;
b69ab3162 }
b69ab3163
b69ab3164 if (arg instanceof Map) {
b69ab3165 return {
b69ab3166 __rpcType: 'Map',
b69ab3167 data: Array.from(arg.entries()).map(([key, val]) => [serialize(key), serialize(val)]),
b69ab3168 } as CustomSerialized;
b69ab3169 } else if (arg instanceof Set) {
b69ab3170 return {__rpcType: 'Set', data: Array.from(arg.values()).map(serialize)} as CustomSerialized;
b69ab3171 } else if (arg instanceof Error) {
b69ab3172 return {__rpcType: 'Error', data: {message: arg.message, stack: arg.stack}} as CustomSerialized;
b69ab3173 } else if (arg instanceof Date) {
b69ab3174 return {__rpcType: 'Date', data: arg.valueOf()} as CustomSerialized;
b69ab3175 } else if (Array.isArray(arg)) {
b69ab3176 return arg.map(a => serialize(a));
b69ab3177 } else if (typeof arg === 'object') {
b69ab3178 const newObj: CustomSerialized & {__rpcType: 'object'} = {__rpcType: 'object'};
b69ab3179 for (const [propertyName, propertyValue] of Object.entries(arg)) {
b69ab3180 newObj[propertyName] = serialize(propertyValue);
b69ab3181 }
b69ab3182
b69ab3183 return newObj;
b69ab3184 }
b69ab3185
b69ab3186 throw new Error(`cannot serialize argument ${arg}`);
b69ab3187}
b69ab3188
b69ab3189export function serializeToString(data: Serializable): string {
b69ab3190 return JSON.stringify(serialize(data));
b69ab3191}
b69ab3192
b69ab3193/**
b69ab3194 * Restore function arguments/return value after deserializing. This lets you recover passed Map/Set/Date/Error during remote transport.
b69ab3195 */
b69ab3196export function deserialize(arg: Serialized): Serializable {
b69ab3197 if (typeof arg !== 'object' || arg == null) {
b69ab3198 return arg;
b69ab3199 }
b69ab31100
b69ab31101 if (Array.isArray(arg)) {
b69ab31102 return arg.map(a => deserialize(a));
b69ab31103 }
b69ab31104
b69ab31105 const specific = arg as CustomSerialized;
b69ab31106 switch (specific.__rpcType) {
b69ab31107 case 'undefined':
b69ab31108 return undefined;
b69ab31109 case 'Map':
b69ab31110 return new Map(specific.data.map(([key, value]) => [deserialize(key), deserialize(value)]));
b69ab31111 case 'Set':
b69ab31112 return new Set(specific.data.map(deserialize));
b69ab31113 case 'Error': {
b69ab31114 const e = new Error();
b69ab31115 e.stack = specific.data.stack;
b69ab31116 e.message = specific.data.message;
b69ab31117 return e;
b69ab31118 }
b69ab31119 case 'Date':
b69ab31120 return new Date(specific.data);
b69ab31121 case 'object': {
b69ab31122 const standardObject = arg as {[key: string]: Serialized};
b69ab31123 const newObj: {[key: string]: Serializable} = {};
b69ab31124 for (const [propertyName, propertyValue] of Object.entries(standardObject)) {
b69ab31125 if (propertyName !== '__rpcType') {
b69ab31126 newObj[propertyName] = deserialize(propertyValue);
b69ab31127 }
b69ab31128 }
b69ab31129 return newObj;
b69ab31130 }
b69ab31131 default: {
b69ab31132 throw new Error(`cannot deserialize unknown type ${specific}`);
b69ab31133 }
b69ab31134 }
b69ab31135}
b69ab31136
b69ab31137export function deserializeFromString(data: string): Serializable {
b69ab31138 return deserialize(JSON.parse(data));
b69ab31139}