mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
refactor(utils): use WeakSet for cycle detection in toJSONObject (#10832)
* refactor(utils): use WeakSet for cycle detection in toJSONObject Replace fixed-size Array(10)+indexOf stack with WeakSet using add-on-descent/delete-on-ascent to preserve path (ancestor-only) semantics. The delete on ascent is the critical correctness detail — a naive WeakSet swap without delete treats shared DAG siblings as already-seen and drops them as undefined, the exact regression introduced in #7230. Perf is a secondary benefit: WeakSet.has is O(1) amortised vs O(n) Array.indexOf, measurable on deeply nested objects. - Compress verbose 3-line comment to one line (references #7230) - Group new DAG regression tests under describe('cycle / DAG handling') Fixes #10807 * chore: added should serialize non-cyclic structures deeper than the old Array(10) cap --------- Co-authored-by: AKIBUZZAMAN AKIB <> Co-authored-by: Jay <jasonsaayman@gmail.com>
This commit is contained in:
+8
-7
@@ -763,11 +763,11 @@ function isSpecCompliantForm(thing) {
|
|||||||
* @returns {Object} The JSON-compatible object.
|
* @returns {Object} The JSON-compatible object.
|
||||||
*/
|
*/
|
||||||
const toJSONObject = (obj) => {
|
const toJSONObject = (obj) => {
|
||||||
const stack = new Array(10);
|
const visited = new WeakSet();
|
||||||
|
|
||||||
const visit = (source, i) => {
|
const visit = (source) => {
|
||||||
if (isObject(source)) {
|
if (isObject(source)) {
|
||||||
if (stack.indexOf(source) >= 0) {
|
if (visited.has(source)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,15 +777,16 @@ const toJSONObject = (obj) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!('toJSON' in source)) {
|
if (!('toJSON' in source)) {
|
||||||
stack[i] = source;
|
// add-on descent / delete-on-ascent: preserves path semantics, so DAG nodes serialise at every occurrence (see #7230).
|
||||||
|
visited.add(source);
|
||||||
const target = isArray(source) ? [] : {};
|
const target = isArray(source) ? [] : {};
|
||||||
|
|
||||||
forEach(source, (value, key) => {
|
forEach(source, (value, key) => {
|
||||||
const reducedValue = visit(value, i + 1);
|
const reducedValue = visit(value);
|
||||||
!isUndefined(reducedValue) && (target[key] = reducedValue);
|
!isUndefined(reducedValue) && (target[key] = reducedValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
stack[i] = undefined;
|
visited.delete(source);
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
@@ -794,7 +795,7 @@ const toJSONObject = (obj) => {
|
|||||||
return source;
|
return source;
|
||||||
};
|
};
|
||||||
|
|
||||||
return visit(obj, 0);
|
return visit(obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -86,6 +86,56 @@ describe('utils', () => {
|
|||||||
JSON.stringify({ x: 1, y: 2, obj: { ok: 1 } })
|
JSON.stringify({ x: 1, y: 2, obj: { ok: 1 } })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cycle / DAG handling', () => {
|
||||||
|
it('should serialize a shared sibling object at every occurrence (DAG, not cycle)', () => {
|
||||||
|
const shared = { val: 42 };
|
||||||
|
const source = { x: shared, y: shared };
|
||||||
|
|
||||||
|
const result = utils.toJSONObject(source);
|
||||||
|
|
||||||
|
// Both branches must serialize — shared reference is not a cycle
|
||||||
|
assert.deepStrictEqual(result, { x: { val: 42 }, y: { val: 42 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serialize a shared sibling array at every occurrence (DAG, not cycle)', () => {
|
||||||
|
const shared = [1, 2, 3];
|
||||||
|
const source = { a: shared, b: shared };
|
||||||
|
|
||||||
|
const result = utils.toJSONObject(source);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, { a: [1, 2, 3], b: [1, 2, 3] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serialize shared sibling that itself contains a self-cycle', () => {
|
||||||
|
const shared = { v: 1 };
|
||||||
|
shared.self = shared; // self-cycle inside the shared node
|
||||||
|
const source = { x: shared, y: shared };
|
||||||
|
|
||||||
|
const result = utils.toJSONObject(source);
|
||||||
|
|
||||||
|
// The self-cycle is stripped, but both x and y must be serialized
|
||||||
|
assert.deepStrictEqual(result, { x: { v: 1 }, y: { v: 1 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serialize non-cyclic structures deeper than the old Array(10) cap', () => {
|
||||||
|
// The previous implementation used a fixed-size Array(10) for path tracking.
|
||||||
|
// A non-cyclic chain deeper than 10 levels must serialise end-to-end.
|
||||||
|
let leaf = { v: 'leaf' };
|
||||||
|
let source = leaf;
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
source = { next: source };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = utils.toJSONObject(source);
|
||||||
|
|
||||||
|
let cursor = result;
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
cursor = cursor.next;
|
||||||
|
}
|
||||||
|
assert.deepStrictEqual(cursor, { v: 'leaf' });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Buffer RangeError Fix', () => {
|
describe('Buffer RangeError Fix', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user