Skip to content

Commit 01b2496

Browse files
committed
Circular Structure handling like json-stringify-safe. Fixes replacer for deep objects.
1 parent fef356e commit 01b2496

File tree

3 files changed

+105
-12
lines changed

3 files changed

+105
-12
lines changed

jsonStreamify.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const RecursiveIterable = require('./recursiveIterable');
77
const isReadableStream = require('./utils').isReadableStream;
88

99
class JSONStreamify extends CoStream {
10-
constructor(value, replacer) {
10+
constructor(value, replacer, space, _visited) {
1111
super(arguments);
12-
this._iter = new RecursiveIterable(replacer instanceof Function ? replacer(undefined, value) : value, replacer);
12+
this._iter = new RecursiveIterable(replacer instanceof Function ? replacer(undefined, value) : value, replacer, space, _visited);
1313
}
1414

1515
* _makeGenerator(value, replacer) {
@@ -54,12 +54,12 @@ class JSONStreamify extends CoStream {
5454
const pass = new PassThrough();
5555
obj.value.pipe(new Transform({
5656
objectMode: true,
57-
transform: function(data, enc, next) {
57+
transform: (data, enc, next) => {
5858
if (!first) {
5959
pass.push(',');
6060
}
6161
first = false;
62-
let stream = new JSONStreamify(data);
62+
let stream = new JSONStreamify(data, this._iter.replacer, this._iter.space, this._iter.visited);
6363
stream._iter._parentCtxType = Array;
6464
stream.once('end', () => next(null, undefined)).pipe(pass, {
6565
end: false
@@ -71,8 +71,27 @@ class JSONStreamify extends CoStream {
7171
continue;
7272
}
7373

74+
if (obj.state === 'circular') {
75+
let replacer;
76+
this.emit('circular', Object.assign(obj, {
77+
replace: (promise) => {
78+
if (promise instanceof Promise) {
79+
obj.value = promise;
80+
}
81+
}
82+
}));
83+
84+
// Wait for replace
85+
yield new Promise(resolve => process.nextTick(resolve));
86+
87+
if (!(obj.value instanceof Promise)) {
88+
yield this.push('"[Circular]"');
89+
}
90+
}
91+
7492
if (obj.value instanceof Promise) {
75-
obj.value = obj.attachChild(new RecursiveIterable(yield obj.value)[Symbol.iterator]());
93+
let childIterator = new RecursiveIterable(yield obj.value, this._iter.replacer, this._iter.space, this._iter.visited)[Symbol.iterator]();
94+
obj.value = obj.attachChild(childIterator);
7695
insertSeparator = false;
7796
continue;
7897
}
@@ -87,6 +106,6 @@ class JSONStreamify extends CoStream {
87106
}
88107
}
89108

90-
module.exports = function (obj, replacer) {
109+
module.exports = function(obj, replacer) {
91110
return new JSONStreamify(obj, replacer);
92111
};

recursiveIterable.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ const Readable = require('stream').Readable;
44
const isReadableStream = require('./utils').isReadableStream;
55

66
class RecursiveIterable {
7-
constructor(obj, replacer) {
7+
constructor(obj, replacer, space, visited) {
88
// Save a copy of the root object so we can be memory effective
99
if (obj && typeof obj.toJSON === 'function') {
1010
obj = obj.toJSON();
1111
}
1212
this.exclude = [Promise, {
1313
__shouldExclude: isReadableStream
1414
}];
15+
this.visited = visited || new WeakSet();
16+
if (this._shouldIterate(obj)) {
17+
this.visited.add(obj);
18+
}
1519
this.obj = this._shouldIterate(obj) ? (Array.isArray(obj) ? obj.slice(0) : Object.assign({}, obj)) : obj;
1620
this.replacerIsArray = Array.isArray(replacer);
1721
this.replacer = replacer instanceof Function || this.replacerIsArray ? replacer : undefined;
22+
this.space = space;
1823
}
1924

2025
_shouldIterate(val) {
@@ -101,11 +106,15 @@ class RecursiveIterable {
101106
}
102107

103108
if (this._shouldIterate(val)) {
104-
state = 'child';
105-
childIterator = new RecursiveIterable(val)[Symbol.iterator]();
106-
childIterator.ctxType = ctx.type;
107-
childIterator.depth = ctx.depth + 1;
108-
childIterator.type = RecursiveIterable._getType(val);
109+
if (this.visited.has(val)) {
110+
state = 'circular';
111+
} else {
112+
state = 'child';
113+
childIterator = new RecursiveIterable(val, this.replacer, this.space, this.visited)[Symbol.iterator]();
114+
childIterator.ctxType = ctx.type;
115+
childIterator.depth = ctx.depth + 1;
116+
childIterator.type = RecursiveIterable._getType(val);
117+
}
109118
}
110119

111120
if (key) {

test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,69 @@ describe('Streamify', () => {
138138
date: date
139139
})
140140
}, `{"a":[{"name":"name","arr":[],"date":"${date.toJSON()}"}]}`));
141+
142+
it(`{ a: [Circular] } should be {"a":"[Circular]"}`, () => {
143+
let data = {};
144+
data.a = data;
145+
146+
let deferred0 = Promise.defer();
147+
let deferred1 = Promise.defer();
148+
let str = '';
149+
new JSONStreamify(data)
150+
.once('circular', err => {
151+
try {
152+
expect(err).to.be.ok();
153+
} catch (err) {
154+
return deferred1.reject(err);
155+
}
156+
deferred1.resolve();
157+
})
158+
.on('data', data => str += data.toString())
159+
.once('end', () => {
160+
try {
161+
expect(str).to.equal(`{"a":"[Circular]"}`);
162+
} catch (err) {
163+
return deferred0.reject(err);
164+
}
165+
deferred0.resolve();
166+
});
167+
168+
return Promise.all([deferred0.promise, deferred1.promise]);
169+
});
170+
171+
it(`{ a: [Circular] } should be {"a":"custom"}`, () => {
172+
let data = {};
173+
data.a = data;
174+
175+
let deferred0 = Promise.defer();
176+
let deferred1 = Promise.defer();
177+
let str = '';
178+
new JSONStreamify(data)
179+
.once('circular', err => {
180+
try {
181+
expect(err).to.be.ok();
182+
expect(err.replace).to.be.a(Function);
183+
err.replace(Promise.resolve('custom'));
184+
} catch (err) {
185+
return deferred1.reject(err);
186+
}
187+
deferred1.resolve();
188+
})
189+
.on('data', data => str += data.toString())
190+
.once('end', () => {
191+
try {
192+
expect(str).to.equal(`{"a":"custom"}`);
193+
} catch (err) {
194+
return deferred0.reject(err);
195+
}
196+
deferred0.resolve();
197+
});
198+
199+
return Promise.all([deferred0.promise, deferred1.promise]);
200+
});
201+
202+
let circularData0 = {};
203+
circularData0.a = circularData0;
204+
circularData0.b = [circularData0, { a: circularData0 }]
205+
it('{a: Circular, b: [Circular, { a: Circular }]} should be {"a":"[Circular]","b":["[Circular]",{"a":"[Circular]"}]}', createTest(circularData0, '{"a":"[Circular]","b":["[Circular]",{"a":"[Circular]"}]}'));
141206
});

0 commit comments

Comments
 (0)