diff --git a/packages/angular-store/src/index.ts b/packages/angular-store/src/index.ts index 130120a4..0db74930 100644 --- a/packages/angular-store/src/index.ts +++ b/packages/angular-store/src/index.ts @@ -91,17 +91,51 @@ function shallow(objA: T, objB: T) { } const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) { return false } - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { + if (keysA.length > 0) { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) + ) { + return false + } + } + return true + } + + if (keysA.length === 0) { + const descriptorsA = Object.getOwnPropertyDescriptors(objA) + const descriptorsB = Object.getOwnPropertyDescriptors(objB) + + const getterKeysA = Object.keys(descriptorsA).filter( + (key) => descriptorsA[key]?.get !== undefined, + ) + const getterKeysB = Object.keys(descriptorsB).filter( + (key) => descriptorsB[key]?.get !== undefined, + ) + + if (getterKeysA.length !== getterKeysB.length) { return false } + + for (const key of getterKeysA) { + if ( + !getterKeysB.includes(key) || + !Object.is( + (objA as Record)[key], + (objB as Record)[key], + ) + ) { + return false + } + } } + return true } diff --git a/packages/react-store/src/index.ts b/packages/react-store/src/index.ts index 7d8cb121..59eb87ed 100644 --- a/packages/react-store/src/index.ts +++ b/packages/react-store/src/index.ts @@ -67,17 +67,51 @@ export function shallow(objA: T, objB: T) { } const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) { return false } - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { + if (keysA.length > 0) { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) + ) { + return false + } + } + return true + } + + if (keysA.length === 0) { + const descriptorsA = Object.getOwnPropertyDescriptors(objA) + const descriptorsB = Object.getOwnPropertyDescriptors(objB) + + const getterKeysA = Object.keys(descriptorsA).filter( + (key) => descriptorsA[key]?.get !== undefined, + ) + const getterKeysB = Object.keys(descriptorsB).filter( + (key) => descriptorsB[key]?.get !== undefined, + ) + + if (getterKeysA.length !== getterKeysB.length) { return false } + + for (const key of getterKeysA) { + if ( + !getterKeysB.includes(key) || + !Object.is( + (objA as Record)[key], + (objB as Record)[key], + ) + ) { + return false + } + } } + return true } diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index c14c80ae..d9b78090 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -145,23 +145,27 @@ describe('shallow', () => { expect(shallow(objA, objB)).toBe(false) }) - test('should return false for one object being undefined', () => { - const objA = { a: 1, b: 'hello' } - const objB = undefined - expect(shallow(objA, objB)).toBe(false) - }) + test('should handle empty objects', () => { + expect(shallow({}, {})).toBe(true) + }) + + test('should handle getter-only objects', () => { + function createGetterOnlyObject(value: number) { + const obj = Object.create({}) + Object.defineProperty(obj, 'value', { + get: () => value, + enumerable: false, + configurable: true, + }) + return obj + } - test('should return true for two null objects', () => { - const objA = null - const objB = null - expect(shallow(objA, objB)).toBe(true) - }) + const objA = createGetterOnlyObject(42) + const objB = createGetterOnlyObject(42) + const objC = createGetterOnlyObject(24) - test('should return false for objects with different types', () => { - const objA = { a: 1, b: 'hello' } - const objB = { a: '1', b: 'hello' } - // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(shallow(objA, objB)).toBe(true) + expect(shallow(objA, objC)).toBe(false) }) test('should return true for shallowly equal maps', () => { diff --git a/packages/solid-store/src/index.tsx b/packages/solid-store/src/index.tsx index d36ed5cd..6093e9d8 100644 --- a/packages/solid-store/src/index.tsx +++ b/packages/solid-store/src/index.tsx @@ -74,17 +74,51 @@ export function shallow(objA: T, objB: T) { } const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) { return false } - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { + if (keysA.length > 0) { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) + ) { + return false + } + } + return true + } + + if (keysA.length === 0) { + const descriptorsA = Object.getOwnPropertyDescriptors(objA) + const descriptorsB = Object.getOwnPropertyDescriptors(objB) + + const getterKeysA = Object.keys(descriptorsA).filter( + (key) => descriptorsA[key]?.get !== undefined, + ) + const getterKeysB = Object.keys(descriptorsB).filter( + (key) => descriptorsB[key]?.get !== undefined, + ) + + if (getterKeysA.length !== getterKeysB.length) { return false } + + for (const key of getterKeysA) { + if ( + !getterKeysB.includes(key) || + !Object.is( + (objA as Record)[key], + (objB as Record)[key], + ) + ) { + return false + } + } } + return true } diff --git a/packages/svelte-store/src/index.svelte.ts b/packages/svelte-store/src/index.svelte.ts index 05f3cac5..1b843bfa 100644 --- a/packages/svelte-store/src/index.svelte.ts +++ b/packages/svelte-store/src/index.svelte.ts @@ -76,17 +76,51 @@ export function shallow(objA: T, objB: T) { } const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) { return false } - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { + if (keysA.length > 0) { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) + ) { + return false + } + } + return true + } + + if (keysA.length === 0) { + const descriptorsA = Object.getOwnPropertyDescriptors(objA) + const descriptorsB = Object.getOwnPropertyDescriptors(objB) + + const getterKeysA = Object.keys(descriptorsA).filter( + (key) => descriptorsA[key]?.get !== undefined, + ) + const getterKeysB = Object.keys(descriptorsB).filter( + (key) => descriptorsB[key]?.get !== undefined, + ) + + if (getterKeysA.length !== getterKeysB.length) { return false } + + for (const key of getterKeysA) { + if ( + !getterKeysB.includes(key) || + !Object.is( + (objA as Record)[key], + (objB as Record)[key], + ) + ) { + return false + } + } } + return true } diff --git a/packages/vue-store/src/index.ts b/packages/vue-store/src/index.ts index afd18e8d..746ca41d 100644 --- a/packages/vue-store/src/index.ts +++ b/packages/vue-store/src/index.ts @@ -80,17 +80,51 @@ export function shallow(objA: T, objB: T) { } const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) { return false } - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { + if (keysA.length > 0) { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) + ) { + return false + } + } + return true + } + + if (keysA.length === 0) { + const descriptorsA = Object.getOwnPropertyDescriptors(objA) + const descriptorsB = Object.getOwnPropertyDescriptors(objB) + + const getterKeysA = Object.keys(descriptorsA).filter( + (key) => descriptorsA[key]?.get !== undefined, + ) + const getterKeysB = Object.keys(descriptorsB).filter( + (key) => descriptorsB[key]?.get !== undefined, + ) + + if (getterKeysA.length !== getterKeysB.length) { return false } + + for (const key of getterKeysA) { + if ( + !getterKeysB.includes(key) || + !Object.is( + (objA as Record)[key], + (objB as Record)[key], + ) + ) { + return false + } + } } + return true }