/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import {isProduction} from 'flipper-common'; /** * makeShallowSerializable will prepare common data structures, like Map and Set, for JSON serialization. * However, this will happen only for the root object and not recursively to keep things efficiently. * * The function does not take care of actual stringification; use JSON.serialize. */ export function makeShallowSerializable(obj: any): any { if (!obj || typeof obj !== 'object') { assertSerializable(obj); return obj; } if (obj instanceof Map) { const data = Array.from(obj.entries()); assertSerializable(data); return { __flipper_object_type__: 'Map', data, }; } else if (obj instanceof Set) { const data = Array.from(obj.values()); assertSerializable(data); return { __flipper_object_type__: 'Set', data, }; } else if (obj instanceof Date) { return { __flipper_object_type__: 'Date', data: obj.getTime(), }; } else { assertSerializable(obj); return obj; } } /** * Inverse of makeShallowSerializable */ export function deserializeShallowObject(obj: any): any { if (!obj || typeof obj !== 'object') { return obj; } if (obj['__flipper_object_type__']) { const type = obj['__flipper_object_type__']; switch (type) { case 'Map': { return new Map(obj.data); } case 'Set': { return new Set(obj.data); } case 'Date': return new Date(obj.data); } } return obj; } /** * Asserts a value is JSON serializable. * Will print a warning if a value is JSON serializable, but isn't a pure tree */ export function assertSerializable(obj: any) { if (isProduction()) { return; } // path to current object const path: string[] = []; // current object stack const stack = new Set(); // past objects, object -> path to reach it const seen = new Set(); // to safe a lot of memory allocations, if we find a duplicate, we just start over again to search for the first, // rather than storing all paths at first encounter let duplicateFound = false; let duplicatePath: string[] | undefined; let duplicateObject: any = undefined; let done = false; function check(value: any) { if (value === null || done) { return; } switch (typeof value) { case 'undefined': // undefined is not strictly speaking serializable, but behaves fine. // JSON.stringify({x : undefined}) ==> '{}' break; case 'boolean': case 'number': case 'string': break; case 'object': // A cycle is truly not serializable, as it would create an unending serialization loop... if (stack.has(value)) { throw new Error( `Cycle detected: object at path '.${path.join( '.', )}' is referring to itself: '${value}'`, ); } // Encountering an object multiple times is bad, as reference equality will be lost upon // deserialization, so the data isn't properly normalised. // But it *might* work fine, and can serialize, so we just warn // Warning is only printed during the second check loop, so that we know *both* paths // - Second walk (which finds first object) if (duplicateFound && duplicateObject && value === duplicateObject) { console.warn( `Duplicate value, object lives at path '.${duplicatePath!.join( '.', )}', but also at path '.${path!.join( '.', )}': '${value}'. This might not behave correct after import and lead to unnecessary big exports.`, ); done = true; // no need to finish the second walk break; } // - First walk (which detects the duplicate and stores location of duplicate) if (!duplicateFound) { if (seen.has(value)) { duplicateFound = true; duplicateObject = value; duplicatePath = path.slice(); } seen.add(value); } stack.add(value); const proto = Object.getPrototypeOf(value); if (Array.isArray(value)) { value.forEach((child, index) => { path.push('' + index); check(child); path.pop(); }); } else if (proto === null || proto === Object.prototype) { for (const key in value) { path.push(key); check(value[key]); path.pop(); } } else { throw new Error( `Unserializable object type (${ proto?.constructor?.name ?? 'Unknown' }) at path '.${path.join('')}': ${value}.`, ); } stack.delete(value); break; case 'bigint': case 'function': case 'symbol': default: throw new Error( `Unserializable value (${typeof value}) at path '.${path.join( '.', )}': '${value}'`, ); } } check(obj); // if there is a duplicate found, re-walk the tree so that we can print both of the paths and report it // this setup is slightly more confusion in code than walking once and storing past paths, // but a lot more efficient :) if (duplicateFound) { path.splice(0); seen.clear(); stack.clear(); check(obj); } }