Skip to content

Commit 1d8eae1

Browse files
authored
fix: Decycling of objects (#1839)
* fix: Decycling of objects * fix: Add changelog * fix: CodeReview * fix: Use isPrimitive
1 parent afe627e commit 1d8eae1

File tree

4 files changed

+87
-43
lines changed

4 files changed

+87
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## 4.5.2
66

7+
- [utils] fix: Decycling for objects to no produce an endless loop
78
- [browser] fix: <unlabeled> event for unhandledRejection
89
- [loader] fix: Handle unhandledRejection the same way as it would be thrown
910

packages/utils/src/memo.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// tslint:disable:no-unsafe-any
2+
/**
3+
* Memo class used for decycle json objects. Uses WeakSet if available otherwise array.
4+
*/
5+
export class Memo {
6+
/** Determines if WeakSet is available */
7+
private readonly hasWeakSet: boolean;
8+
/** Either WeakSet or Array */
9+
private readonly inner: any;
10+
11+
public constructor() {
12+
// tslint:disable-next-line
13+
this.hasWeakSet = typeof WeakSet === 'function';
14+
this.inner = this.hasWeakSet ? new WeakSet() : [];
15+
}
16+
17+
/**
18+
* Sets obj to remember.
19+
* @param obj Object to remember
20+
*/
21+
public memoize(obj: any): boolean {
22+
if (this.hasWeakSet) {
23+
if (this.inner.has(obj)) {
24+
return true;
25+
}
26+
this.inner.add(obj);
27+
return false;
28+
} else {
29+
for (const value of this.inner) {
30+
if (value === obj) {
31+
return true;
32+
}
33+
}
34+
this.inner.push(obj);
35+
return false;
36+
}
37+
}
38+
39+
/**
40+
* Removes object from internal storage.
41+
* @param obj Object to forget
42+
*/
43+
public unmemoize(obj: any): void {
44+
if (this.hasWeakSet) {
45+
this.inner.delete(obj);
46+
} else {
47+
for (let i = 0; i < this.inner.length; i++) {
48+
if (this.inner[i] === obj) {
49+
this.inner.splice(i, 1);
50+
break;
51+
}
52+
}
53+
}
54+
}
55+
}

packages/utils/src/object.ts

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SentryWrappedFunction } from '@sentry/types';
2-
import { isNaN, isPlainObject, isUndefined } from './is';
2+
import { isNaN, isPlainObject, isPrimitive, isUndefined } from './is';
3+
import { Memo } from './memo';
34
import { truncate } from './string';
45

56
/**
@@ -294,6 +295,27 @@ function normalizeValue(value: any, key?: any): any {
294295
return value;
295296
}
296297

298+
/**
299+
* Decycles an object to make it safe for json serialization.
300+
*
301+
* @param obj Object to be decycled
302+
* @param memo Optional Memo class handling decycling
303+
*/
304+
function decycle(obj: any, memo: Memo = new Memo()): any {
305+
if (!isPrimitive(obj)) {
306+
if (memo.memoize(obj)) {
307+
return '[Circular ~]';
308+
}
309+
// tslint:disable-next-line
310+
for (const key in obj) {
311+
// tslint:disable-next-line
312+
obj[key] = decycle(obj[key], memo);
313+
}
314+
memo.unmemoize(obj);
315+
}
316+
return obj;
317+
}
318+
297319
/**
298320
* serializer()
299321
*
@@ -302,39 +324,8 @@ function normalizeValue(value: any, key?: any): any {
302324
* and takes care of Error objects serialization
303325
*/
304326
function serializer(options: { normalize: boolean } = { normalize: true }): (key: string, value: any) => any {
305-
const stack: any[] = [];
306-
const keys: string[] = [];
307-
308-
/** recursive */
309-
function cycleserializer(_key: string, value: any): any {
310-
if (stack[0] === value) {
311-
return '[Circular ~]';
312-
}
313-
return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]`;
314-
}
315-
316-
return function(this: any, key: string, value: any): any {
317-
if (stack.length > 0) {
318-
const thisPos = stack.indexOf(this);
319-
320-
if (thisPos === -1) {
321-
stack.push(this);
322-
keys.push(key);
323-
} else {
324-
stack.splice(thisPos + 1);
325-
keys.splice(thisPos, Infinity, key);
326-
}
327-
328-
if (stack.indexOf(value) !== -1) {
329-
// tslint:disable-next-line:no-parameter-reassignment
330-
value = cycleserializer.call(this, key, value);
331-
}
332-
} else {
333-
stack.push(value);
334-
}
335-
336-
return options.normalize ? normalizeValue(value, key) : value;
337-
};
327+
// tslint:disable-next-line
328+
return (key: string, value: object) => (options.normalize ? normalizeValue(decycle(value), key) : decycle(value));
338329
}
339330

340331
/**

packages/utils/test/object.test.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('serialize()', () => {
5353
expect(serialize(obj)).toEqual(
5454
JSON.stringify({
5555
name: 'Alice',
56-
child: { name: 'Bob', self: '[Circular ~.child]' },
56+
child: { name: 'Bob', self: '[Circular ~]' },
5757
}),
5858
);
5959
});
@@ -65,7 +65,7 @@ describe('serialize()', () => {
6565
expect(serialize(obj)).toEqual(
6666
JSON.stringify({
6767
name: 'Alice',
68-
child: { name: 'Bob', identity: { self: '[Circular ~.child]' } },
68+
child: { name: 'Bob', identity: { self: '[Circular ~]' } },
6969
}),
7070
);
7171
});
@@ -94,10 +94,7 @@ describe('serialize()', () => {
9494
expect(serialize(obj)).toEqual(
9595
JSON.stringify({
9696
name: 'Alice',
97-
children: [
98-
{ name: 'Bob', self: '[Circular ~.children.0]' },
99-
{ name: 'Eve', self: '[Circular ~.children.1]' },
100-
],
97+
children: [{ name: 'Bob', self: '[Circular ~]' }, { name: 'Eve', self: '[Circular ~]' }],
10198
}),
10299
);
103100
});
@@ -310,7 +307,7 @@ describe('safeNormalize()', () => {
310307
obj.child.self = obj.child;
311308
expect(safeNormalize(obj)).toEqual({
312309
name: 'Alice',
313-
child: { name: 'Bob', self: '[Circular ~.child]' },
310+
child: { name: 'Bob', self: '[Circular ~]' },
314311
});
315312
});
316313

@@ -320,7 +317,7 @@ describe('safeNormalize()', () => {
320317
obj.child.identity = { self: obj.child };
321318
expect(safeNormalize(obj)).toEqual({
322319
name: 'Alice',
323-
child: { name: 'Bob', identity: { self: '[Circular ~.child]' } },
320+
child: { name: 'Bob', identity: { self: '[Circular ~]' } },
324321
});
325322
});
326323

@@ -345,7 +342,7 @@ describe('safeNormalize()', () => {
345342
obj.children[1].self = obj.children[1];
346343
expect(safeNormalize(obj)).toEqual({
347344
name: 'Alice',
348-
children: [{ name: 'Bob', self: '[Circular ~.children.0]' }, { name: 'Eve', self: '[Circular ~.children.1]' }],
345+
children: [{ name: 'Bob', self: '[Circular ~]' }, { name: 'Eve', self: '[Circular ~]' }],
349346
});
350347
});
351348

0 commit comments

Comments
 (0)