Skip to content

Commit

Permalink
fix: refactor createSharedReferences
Browse files Browse the repository at this point in the history
  • Loading branch information
joshhowenstine committed Nov 25, 2024
1 parent d60cfd2 commit 45358be
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -209,50 +209,52 @@ export function removeEmptyObjects(obj) {
return obj; // Always return obj, even if it's empty
}

export function createSharedReferences(obj = {}) {
const seenObjects = new Map();

// Generates a hash for an object.
// Sorting keys ensures consistent hash regardless of property order.
function hash(object) {
let result = '';
if (typeof object !== 'object' || object === null) {
// If it's a primitive, return its string representation.
return JSON.stringify(object);
} else if (Array.isArray(object)) {
// If it's an array, we hash each element.
result += '[' + object.map(hash).join(',') + ']';
} else {
// If it's an object, we take sorted keys and include their values.
const keys = Object.keys(object).sort();
result +=
'{' + keys.map(key => `${key}:${hash(object[key])}`).join(',') + '}';
export function safeStringify(originalObj) {
const obj = { ...originalObj };

const seen = new WeakSet(); // WeakSet is used to store references to objects we've processed

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 result;
}
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);

Check failure on line 245 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `⏎`
} else {
// Add child objects to the queue for processing
seenObjects.set(cacheKey, value);
queue.push(value);
}
}
}
}
}
}

process(obj);

return obj;
}

Expand Down Expand Up @@ -623,9 +625,8 @@ export function generateNameFromPrototypeChain(obj, name = '') {
if (!obj) return name;
const proto = Object.getPrototypeOf(obj);
if (!proto || !proto.constructor) return name;
const componentName = `${name ? name + '.' : ''}${
proto?.constructor?.__componentName || ''
}`
const componentName = `${name ? name + '.' : ''}${proto?.constructor?.__componentName || ''

Check failure on line 628 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Insert `⏎····`
}`

Check failure on line 629 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `··`
.replace(/\.*$/, '')
.trim();
const result = generateNameFromPrototypeChain(proto, componentName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ describe('isPlainObject', () => {
expect(isPlainObject('')).toBe(false);
expect(isPlainObject(42)).toBe(false);
expect(isPlainObject(true)).toBe(false);
expect(isPlainObject(() => {})).toBe(false);
expect(isPlainObject(() => { })).toBe(false);

Check failure on line 215 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.test.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `·`
expect(isPlainObject(/foo/)).toBe(false);
expect(isPlainObject(new Error())).toBe(false);
});
Expand Down Expand Up @@ -452,6 +452,38 @@ describe('createSharedReferences', () => {
const result = createSharedReferences(input);
expect(result.a).not.toBe(result.b);
});

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

// Test: Nested circular reference
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);
});

// Test: Shared references
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);
});

// Test: Deeply nested circular reference
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 Expand Up @@ -1128,7 +1160,7 @@ describe('generateNameFromPrototypeChain', () => {
});

it('should handle an object with missing __componentName', () => {
class ComponentWithoutName {}
class ComponentWithoutName { }

Check failure on line 1163 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.test.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `·`

const obj = new ComponentWithoutName();
const result = generateNameFromPrototypeChain(obj);
Expand Down Expand Up @@ -1218,7 +1250,7 @@ class ComponentB extends ComponentA {
}
}

class ComponentC extends ComponentB {}
class ComponentC extends ComponentB { }

Check failure on line 1253 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.test.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `·`

describe('getStyleChain', () => {
it('should return an array of style objects from the prototype chain', () => {
Expand Down Expand Up @@ -1453,7 +1485,7 @@ describe('replaceAliasValues', () => {

const aliasStyles = [{ prev: 'testW', curr: 'testWidth', skipWarn: false }];

const consoleWarnSpy = jest.spyOn(log, 'warn').mockImplementation(() => {});
const consoleWarnSpy = jest.spyOn(log, 'warn').mockImplementation(() => { });

Check failure on line 1488 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.test.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `·`

const result = replaceAliasValues(styleObject, aliasStyles);

Expand All @@ -1474,7 +1506,7 @@ describe('replaceAliasValues', () => {

const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
.mockImplementation(() => { });

Check failure on line 1509 in packages/@lightningjs/ui-components/src/mixins/withThemeStyles/utils.test.js

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Delete `·`

const result = replaceAliasValues(styleObject, aliasStyles);

Expand Down
64 changes: 64 additions & 0 deletions packages/@lightningjs/ui-components/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export function safeStringify(originalObj) {
const obj = { ...originalObj };

const seen = new WeakSet(); // WeakSet is used to store references to objects we've processed

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) {
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;
}

const input = {
unfocused_neutral: { color: 'primary', selected: { color: 'brand' } },
unfocused_inverse: { color: 'primary', selected: { color: 'brand' } },
unfocused_brand: { color: 'primary', selected: { color: 'brand' } },
focused_neutral: { color: 'primary', selected: { color: 'brand' } },
focused_inverse: { color: 'primary', selected: { color: 'brand' } },
focused_brand: { color: 'primary', selected: { color: 'brand' } },
disabled_neutral: { color: 'primary', selected: { color: 'brand' } },
disabled_inverse: { color: 'primary', selected: { color: 'brand' } },
disabled_brand: { color: 'primary', selected: { color: 'brand' } }
}

input.self = input; // Create a circular reference
const result = createSharedReferences(input);
console.log(result);

0 comments on commit 45358be

Please sign in to comment.