Skip to content

Commit cc1454b

Browse files
committed
Changed cyclic structure handling to a custom WeakMap implementation of Crockfords Decycle method.
1 parent b7371e8 commit cc1454b

File tree

4 files changed

+49
-20
lines changed

4 files changed

+49
-20
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,15 @@ app.get('/api/users', (req, res, next) => JSONStreamStreamify(Users.find().strea
9090

9191
## TODO
9292
- Space option
93-
- Circular dependency detection/handling (infinite loops may occur as it is)
9493

9594
Feel free to contribute.
9695

9796
## Technical Notes
9897
Uses toJSON when available, and JSON.stringify to stringify everything but objects and arrays.
9998
Streams with ObjectMode=true are output as arrays while ObjectMode=false output as a concatinated string (each chunk is piped with transforms).
10099

100+
Circular structures are handled using a WeakMap based implementation of [Douglas Crockfords Decycle method](https://github.com/douglascrockford/JSON-js/blob/master/cycle.js). To restore circular structures; use Crockfords Retrocycle method on the parsed object.
101+
101102
## Requirements
102103
NodeJS >4.2.2
103104

jsonStreamify.js

Lines changed: 6 additions & 5 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, space, _visited) {
10+
constructor(value, replacer, space, _visited, _stack) {
1111
super(arguments);
12-
this._iter = new RecursiveIterable(replacer instanceof Function ? replacer(undefined, value) : value, replacer, space, _visited);
12+
this._iter = new RecursiveIterable(replacer instanceof Function ? replacer(undefined, value) : value, replacer, space, _visited, _stack);
1313
}
1414

1515
* _makeGenerator(value, replacer) {
@@ -52,6 +52,7 @@ class JSONStreamify extends CoStream {
5252
yield this.push('[');
5353
let first = true;
5454
const pass = new PassThrough();
55+
let i = 0;
5556
obj.value.pipe(new Transform({
5657
objectMode: true,
5758
transform: (data, enc, next) => {
@@ -60,7 +61,7 @@ class JSONStreamify extends CoStream {
6061
}
6162
first = false;
6263
let stream = new JSONStreamify(data, this._iter.replacer, this._iter.space, this._iter.visited);
63-
stream._iter._stack = this._iter._stack;
64+
stream._iter._stack = obj.stack.concat(i++);
6465
stream._iter._parentCtxType = Array;
6566
stream.once('end', () => next(null, undefined)).pipe(pass, {
6667
end: false
@@ -73,11 +74,11 @@ class JSONStreamify extends CoStream {
7374
}
7475

7576
if (obj.state === 'circular') {
76-
yield this.push(JSON.stringify(`~${obj.value.join('~')}`));
77+
yield this.push(JSON.stringify({ $ref: `$${obj.value.map(v => `[${JSON.stringify(v)}]`).join('')}` }));
7778
}
7879

7980
if (obj.value instanceof Promise) {
80-
let childIterator = new RecursiveIterable(yield obj.value, this._iter.replacer, this._iter.space, this._iter.visited, this._iter._stack)[Symbol.iterator]();
81+
let childIterator = new RecursiveIterable(yield obj.value, this._iter.replacer, this._iter.space, this._iter.visited, obj.stack.concat(obj.key || []))[Symbol.iterator]();
8182
obj.value = obj.attachChild(childIterator, obj.key);
8283
insertSeparator = false;
8384
continue;

recursiveIterable.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ class RecursiveIterable {
1515
this._stack = stack || [];
1616
this.visited = visited || new WeakMap();
1717
if (this._shouldIterate(obj)) {
18-
this.visited.set(obj, this._stack.slice(0));
18+
this.isVisited = this.visited.has(obj);
19+
if (!this.isVisited) {
20+
// Save only unvisited stack to weakmap
21+
this.visited.set(obj, this._stack.slice(0));
22+
}
1923
}
20-
this.obj = this._shouldIterate(obj) ? (Array.isArray(obj) ? obj.slice(0) : Object.assign({}, obj)) : obj;
24+
this.obj = this._shouldIterate(obj) && !this.isVisited ? (Array.isArray(obj) ? obj.slice(0) : Object.assign({}, obj)) : obj;
2125
this.replacerIsArray = Array.isArray(replacer);
2226
this.replacer = replacer instanceof Function || this.replacerIsArray ? replacer : undefined;
2327
this.space = space;
@@ -63,9 +67,16 @@ class RecursiveIterable {
6367
let type;
6468

6569
if (!opened) {
66-
state = 'open';
67-
type = ctxType;
68-
opened = true;
70+
if (this.isVisited) {
71+
state = 'circular';
72+
val = this.visited.get(this.obj);
73+
opened = closed = true;
74+
keys.length = 0;
75+
} else {
76+
state = 'open';
77+
type = ctxType;
78+
opened = true;
79+
}
6980
} else if (!closed && !keys.length) {
7081
state = 'close';
7182
type = ctxType;

test.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,29 @@ describe('Streamify', () => {
139139
})
140140
}, `{"a":[{"name":"name","arr":[],"date":"${date.toJSON()}"}]}`));
141141

142-
let circularData0 = {};
143-
circularData0.a = circularData0;
144-
it(`{ a: a } should be {"a":"~"}`, createTest(circularData0, `{"a":"~"}`));
145-
146-
let circularData1 = {};
147-
circularData1.a = circularData1;
148-
circularData1.b = [circularData1, { a: circularData1 }]
149-
circularData1.b[3] = circularData1.b[1];
150-
it('{a: a, b: [a, { a: a },,a.b.1]} should be {"a":"~","b":["~",{"a":"~"},null,"~b~1"]}', createTest(circularData1, '{"a":"~","b":["~",{"a":"~"},null,"~b~1"]}'));
142+
describe('circular structure', function() {
143+
144+
let circularData0 = {};
145+
circularData0.a = circularData0;
146+
it(`{ a: a } should be {"a":{"$ref":"$"}}`, createTest(circularData0, `{"a":{"$ref":"$"}}`));
147+
148+
let circularData1 = {};
149+
circularData1.a = circularData1;
150+
circularData1.b = [circularData1, {
151+
a: circularData1
152+
}]
153+
circularData1.b[3] = ReadableStream(circularData1.b[1]);
154+
it('{a: a, b: [a, { a: a },,ReadableStream(b.1)]} should be {"a":{"$ref":"$"},"b":[{"$ref":"$"},{"a":{"$ref":"$"}},null,[{"$ref":"$[\\"b\\"][1]"}]]}', createTest(circularData1, '{"a":{"$ref":"$"},"b":[{"$ref":"$"},{"a":{"$ref":"$"}},null,[{"$ref":"$[\\"b\\"][1]"}]]}'));
155+
156+
let circularData2 = {};
157+
let data2 = {
158+
a: 'deep'
159+
};
160+
circularData2.a = Promise.resolve({
161+
b: data2
162+
});
163+
circularData2.b = data2;
164+
it(`{ a: Promise({ b: { a: 'deep' } }), b: a.b } should be {"a":{"b":{"a":"deep"}},"b":{"$ref":"$[\\"a\\"][\\"b\\"]"}}`, createTest(circularData2, `{"a":{"b":{"a":"deep"}},"b":{"$ref":"$[\\"a\\"][\\"b\\"]"}}`));
165+
166+
});
151167
});

0 commit comments

Comments
 (0)