Skip to content

Commit f715e87

Browse files
authored
ref(utils): Simplify value normalization (#4666)
This makes a few changes to the internals of `utils.normalize`: - Instead of using `JSON.stringify` to handle the recursive visiting of nodes in an object tree (and then giving it a visit function (`walk`) which is itself a recursive visitor of nodes), forcing us then to call `JSON.parse` on the results, it simply uses said recursive visitor to recursively visit the object tree nodes. - It renames `normalizeValue` to `makeSerializable` (since that's more accurately and specifically what it's doing) and updates its out-of-date docstring to match its behavior. - It refers to the methods in our memoizer by name rather than number, splits out an inlined value, and moves a typecast, all to increase readability.
1 parent bc7b975 commit f715e87

File tree

2 files changed

+29
-18
lines changed

2 files changed

+29
-18
lines changed

packages/utils/src/memo.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
33

4-
export type MemoFunc = [(obj: any) => boolean, (obj: any) => void];
4+
export type MemoFunc = [
5+
// memoize
6+
(obj: any) => boolean,
7+
// unmemoize
8+
(obj: any) => void,
9+
];
510

611
/**
712
* Helper to decycle json objects

packages/utils/src/object.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -231,20 +231,20 @@ function serializeValue(value: any): any {
231231
return '[Array]';
232232
}
233233

234-
const normalized = normalizeValue(value);
235-
return isPrimitive(normalized) ? normalized : type;
234+
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
235+
// pass-through.
236+
const serializable = makeSerializable(value);
237+
return isPrimitive(serializable) ? serializable : type;
236238
}
237239

238240
/**
239-
* normalizeValue()
241+
* makeSerializable()
240242
*
241-
* Takes unserializable input and make it serializable friendly
243+
* Takes unserializable input and make it serializer-friendly.
242244
*
243-
* - translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
244-
* - serializes Error objects
245-
* - filter global objects
245+
* Handles globals, functions, `undefined`, `NaN`, and other non-serializable values.
246246
*/
247-
function normalizeValue<T>(value: T, key?: any): T | string {
247+
function makeSerializable<T>(value: T, key?: any): T | string {
248248
if (key === 'domain' && value && typeof value === 'object' && (value as unknown as { _events: any })._events) {
249249
return '[Domain]';
250250
}
@@ -310,6 +310,8 @@ function normalizeValue<T>(value: T, key?: any): T | string {
310310
*/
311311
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
312312
export function walk(key: string, value: any, depth: number = +Infinity, memo: MemoFunc = memoBuilder()): any {
313+
const [memoize, unmemoize] = memo;
314+
313315
// If we reach the maximum depth, serialize whatever is left
314316
if (depth === 0) {
315317
return serializeValue(value);
@@ -322,21 +324,23 @@ export function walk(key: string, value: any, depth: number = +Infinity, memo: M
322324
}
323325
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
324326

325-
// If normalized value is a primitive, there are no branches left to walk, so bail out
326-
const normalized = normalizeValue(value, key);
327-
if (isPrimitive(normalized)) {
328-
return normalized;
327+
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
328+
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
329+
// all along), we're done.
330+
const serializable = makeSerializable(value, key);
331+
if (isPrimitive(serializable)) {
332+
return serializable;
329333
}
330334

331335
// Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
332336
// with extracted key:value pairs) or the input itself.
333337
const source = getWalkSource(value);
334338

335339
// Create an accumulator that will act as a parent for all future itterations of that branch
336-
const acc = Array.isArray(value) ? [] : {};
340+
const acc: { [key: string]: any } = Array.isArray(value) ? [] : {};
337341

338342
// If we already walked that branch, bail out, as it's circular reference
339-
if (memo[0](value)) {
343+
if (memoize(value)) {
340344
return '[Circular ~]';
341345
}
342346

@@ -347,11 +351,12 @@ export function walk(key: string, value: any, depth: number = +Infinity, memo: M
347351
continue;
348352
}
349353
// Recursively walk through all the child nodes
350-
(acc as { [key: string]: any })[innerKey] = walk(innerKey, source[innerKey], depth - 1, memo);
354+
const innerValue: any = source[innerKey];
355+
acc[innerKey] = walk(innerKey, innerValue, depth - 1, memo);
351356
}
352357

353358
// Once walked through all the branches, remove the parent from memo storage
354-
memo[1](value);
359+
unmemoize(value);
355360

356361
// Return accumulated values
357362
return acc;
@@ -372,7 +377,8 @@ export function walk(key: string, value: any, depth: number = +Infinity, memo: M
372377
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
373378
export function normalize(input: any, depth?: number): any {
374379
try {
375-
return JSON.parse(JSON.stringify(input, (key: string, value: any) => walk(key, value, depth)));
380+
// since we're at the outermost level, there is no key
381+
return walk('', input, depth);
376382
} catch (_oO) {
377383
return '**non-serializable**';
378384
}

0 commit comments

Comments
 (0)