Skip to content

Commit

Permalink
fix: createSharedReferences not comparing values (#549)
Browse files Browse the repository at this point in the history
* fix: check for values in shared references

* fix: code cleanup

* fix: refactor createSharedReferences

* fix: formatting updates

* fix: remove test file

* fix: remove comments in test

* fix: formatting issues
  • Loading branch information
joshhowenstine authored Dec 3, 2024
1 parent f4f5b41 commit 7cf0ab4
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -209,39 +209,51 @@ export function removeEmptyObjects(obj) {
return obj; // Always return obj, even if it's empty
}

// This map will store hashes of objects to detect duplicates.
export function safeStringify(originalObj) {
const obj = { ...originalObj };

export function createSharedReferences(obj = {}) {
const seenObjects = new Map();
const seen = new WeakSet(); // WeakSet is used to store references to objects we've processed

// Generates a hash for an object.
// Sorting keys ensures consistent hash regardless of property order.
function hash(object) {
return JSON.stringify(object, Object.keys(object).sort());
}
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'; // Replace circular references with a string
}
seen.add(value); // Mark this object as seen
}
return value; // Return the value as is
});
}

export function createSharedReferences(obj = {}) {
const seenObjects = new Map(); // Store original reference -> shared reference

function process(currentObj) {
for (const key in currentObj) {
if (currentObj.hasOwnProperty(key)) {
const value = currentObj[key];
if (typeof value === 'object' && value !== null) {
// Ensure it's an object
const valueHash = hash(value);
if (seenObjects.has(valueHash)) {
// If we've seen this object before, replace the current reference
// with the original reference.
currentObj[key] = seenObjects.get(valueHash);
} else {
seenObjects.set(valueHash, value);
process(value); // Recursively process this object
const queue = [currentObj]; // Use a queue for breadth-first traversal

while (queue.length > 0) {
const current = queue.shift();

for (const key in current) {
if (current.hasOwnProperty(key)) {
const value = current[key];
if (typeof value === 'object' && value !== null) {
const cacheKey = safeStringify(value);
if (seenObjects.has(cacheKey)) {
// Replace duplicate reference with the shared reference
current[key] = seenObjects.get(cacheKey);
} else {
// Add child objects to the queue for processing
seenObjects.set(cacheKey, value);
queue.push(value);
}
}
}
}
}
}

process(obj);

return obj;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,34 @@ describe('createSharedReferences', () => {
const result = createSharedReferences(input);
expect(result.a).not.toBe(result.b);
});

it('should preserve circular references', () => {
const obj = {};
obj.self = obj;
const result = createSharedReferences(obj);
expect(result.self).toBe(result);
});

it('should preserve nested circular references', () => {
const obj = { a: {} };
obj.a.self = obj.a;
const result = createSharedReferences(obj);
expect(result.a.self).toBe(result.a);
});

it('should preserve shared references for the same object', () => {
const shared = {};
const obj = { a: shared, b: shared };
const result = createSharedReferences(obj);
expect(result.a).toBe(result.b);
});

it('should preserve deeply nested circular references', () => {
const obj = { a: { b: { c: { d: {} } } } };
obj.a.b.c.d.self = obj.a.b.c.d;
const result = createSharedReferences(obj);
expect(result.a.b.c.d.self).toBe(result.a.b.c.d);
});
});

describe('getUniqueProperties', () => {
Expand Down

0 comments on commit 7cf0ab4

Please sign in to comment.