Split flipper-plugin package
Summary: flipper-server-companion depends on flipper-plugin. flipper-plugin includes dependencies that run only in a browser. Splitting flipper-plugin into core and browser packages helps to avoid including browser-only dependencies into flipper-server bundle. As a result, bundle size could be cut in half. Subsequently, RSS usage drops as there is twice as less code to process for V8. Note: it currently breaks external flipper-data-source package. It will be restored in subsequent diffs Reviewed By: lblasa Differential Revision: D38658285 fbshipit-source-id: 751b11fa9f3a2d938ce166687b8310ba8b059dee
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2090120cda
commit
97b8b8a1c4
186
desktop/flipper-plugin-core/src/utils/shallowSerialization.tsx
Normal file
186
desktop/flipper-plugin-core/src/utils/shallowSerialization.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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<any>();
|
||||
// past objects, object -> path to reach it
|
||||
const seen = new Set<any>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user