flipper-plugin
Version:
Flipper Desktop plugin SDK and components
178 lines • 6.87 kB
JavaScript
;
/**
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertSerializable = exports.deserializeShallowObject = exports.makeShallowSerializable = void 0;
const flipper_common_1 = require("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.
*/
function makeShallowSerializable(obj) {
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;
}
}
exports.makeShallowSerializable = makeShallowSerializable;
/**
* Inverse of makeShallowSerializable
*/
function deserializeShallowObject(obj) {
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;
}
exports.deserializeShallowObject = deserializeShallowObject;
/**
* Asserts a value is JSON serializable.
* Will print a warning if a value is JSON serializable, but isn't a pure tree
*/
function assertSerializable(obj) {
if ((0, flipper_common_1.isProduction)()) {
return;
}
// path to current object
const path = [];
// 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;
let duplicateObject = undefined;
let done = false;
function check(value) {
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(
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`Duplicate value, object lives at path '.${duplicatePath.join('.')}', but also at path '.${
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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);
}
}
exports.assertSerializable = assertSerializable;
//# sourceMappingURL=shallowSerialization.js.map