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.
|
||||
*/
|
||||
const toJSONObject = (obj) => {
|
||||
const stack = new Array(10);
|
||||
const visited = new WeakSet();
|
||||
|
||||
const visit = (source, i) => {
|
||||
const visit = (source) => {
|
||||
if (isObject(source)) {
|
||||
if (stack.indexOf(source) >= 0) {
|
||||
if (visited.has(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -777,15 +777,16 @@ const toJSONObject = (obj) => {
|
||||
}
|
||||
|
||||
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) ? [] : {};
|
||||
|
||||
forEach(source, (value, key) => {
|
||||
const reducedValue = visit(value, i + 1);
|
||||
const reducedValue = visit(value);
|
||||
!isUndefined(reducedValue) && (target[key] = reducedValue);
|
||||
});
|
||||
|
||||
stack[i] = undefined;
|
||||
visited.delete(source);
|
||||
|
||||
return target;
|
||||
}
|
||||
@@ -794,7 +795,7 @@ const toJSONObject = (obj) => {
|
||||
return source;
|
||||
};
|
||||
|
||||
return visit(obj, 0);
|
||||
return visit(obj);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,6 +86,56 @@ describe('utils', () => {
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user