diff --git a/__tests__/deep-equals.test.js b/__tests__/deep-equals.test.js new file mode 100644 index 0000000..4305691 --- /dev/null +++ b/__tests__/deep-equals.test.js @@ -0,0 +1,104 @@ + +import { deepEquals } from '../src/deep-equals' + +describe('DeepEquals', () => { + it('Should export deepEquals as named export', () => { + expect(deepEquals).toBeInstanceOf(Function) + }) + + it('Should compare strings', () => { + expect(deepEquals('string', 'string')).toBe(true) + expect(deepEquals('string', 'other-string')).toBe(false) + }) + + it('Should compare numbers', () => { + expect(deepEquals(1, 1)).toBe(true) + expect(deepEquals(1, 2)).toBe(false) + }) + + it('Should compare booleans', () => { + expect(deepEquals(true, true)).toBe(true) + expect(deepEquals(true, false)).toBe(false) + }) + + it('Should compare nulls', () => { + expect(deepEquals(null, null)).toBe(true) + expect(deepEquals(null, false)).toBe(false) + }) + + it('Should compare arrays', () => { + expect(deepEquals([1, 2, 3], [1, 2, 3])).toBe(true) + expect(deepEquals([1, 2, 3], [3, 2, 1])).toBe(false) + }) + + it('Should compare arrays of objects', () => { + expect(deepEquals( + [{ a: 1, b: 2, c: 'c' }, { a: 1, b: 2, c: 'c' }], + [{ a: 1, b: 2, c: 'c' }, { a: 1, b: 2, c: 'c' }] + )).toBe(true) + expect(deepEquals( + [{ a: 1, b: 2, c: 'c' }, { a: 1, b: 2, c: 'c' }], + [{ a: 2, b: 1, c: 'c' }, { a: 1, b: 2, c: 'c' }] + )).toBe(false) + }) + + it('Should compare simple objects', () => { + expect(deepEquals({ a: 1, b: 2, c: 'c' }, { a: 1, b: 2, c: 'c' })).toBe(true) + expect(deepEquals({ a: 1, b: 2, c: 'c' }, { a: 2, b: 1 })).toBe(false) + }) + + it('Should compare nested objects', () => { + expect(deepEquals( + { a: 1, b: 2, c: { y: 2, z: '1' } }, + { a: 1, b: 2, c: { y: 2, z: '1' } } + )).toBe(true) + expect(deepEquals( + { a: 1, b: 2, c: { y: 2, z: '1' } }, + { a: 1, b: 2, c: { y: 2, z: '2' } } + )).toBe(false) + }) + + it('Should compare functions', () => { + const foo = () => 'foo' + const bar = () => 'bar' + + expect(deepEquals(foo, foo)).toBe(true) + expect(deepEquals(foo, bar)).toBe(false) + }) + + it('Should compare Maps', () => { + const firstMap = new Map([['Map', { name: 'I am a map', phone: '213-555-1234' }]]) + const secondMap = new Map([['Map', { name: 'I am a map', phone: '213-555-1234' }]]) + const thirdMap = new Map([ + ['Map', { name: 'I am third map', phone: '213-555-1234' }], + [1, 1] + ]) + + expect(deepEquals(firstMap, firstMap)).toBe(true) + expect(deepEquals(firstMap, secondMap)).toBe(true) + expect(deepEquals(firstMap, thirdMap)).toBe(false) + }) + + it('Should compare Sets', () => { + const firstSet = new Set(['1', { name: 'I am a set', phone: '213-555-1234' }]) + const secondSet = new Set(['1', { name: 'I am a set', phone: '213-555-1234' }]) + const thirdSet = new Set( + ['2', { name: 'I am another', phone: '213-555-1234' }, 2] + ) + + expect(deepEquals(firstSet, secondSet)).toBe(true) + expect(deepEquals(firstSet, thirdSet)).toBe(false) + }) + + it('Should compare Dates', () => { + expect(deepEquals( + new Date('2023-01-04T00:00:00.000Z'), + new Date('2023-01-04T00:00:00.000Z') + )).toBe(true) + + expect(deepEquals( + new Date('2023-01-04T00:00:00.000Z'), + new Date('2023-01-05T00:00:00.000Z') + )).toBe(false) + }) +}) diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html index 2b3c2fc..f046bea 100644 --- a/examples/vanilla/index.html +++ b/examples/vanilla/index.html @@ -6,7 +6,7 @@ Store - Vanilla -

Count: 0

+

Counter: 0

@@ -14,32 +14,40 @@ diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..0e760cf --- /dev/null +++ b/src/constants.js @@ -0,0 +1 @@ +export const SYMBOL_LISTENER = Symbol('@@killa-listener') diff --git a/src/core.js b/src/core.js index 53e3e79..88a31d2 100644 --- a/src/core.js +++ b/src/core.js @@ -1,5 +1,8 @@ import clone from 'clone' +import { SYMBOL_LISTENER } from './constants' +import { deepEquals } from './deep-equals' + export const createStore = (initialState = {}, options = {}) => { const listeners = new Set() let state = clone(initialState) @@ -25,11 +28,11 @@ export const createStore = (initialState = {}, options = {}) => { const _newState = getState() if (listener.LISTENER) { - const currentState = listener.STATE - const nextState = listener.SELECTOR(state) + const selectorState = listener.SELECTOR_STATE + const nextselectorState = listener.SELECTOR(state) - if (!Object.is(currentState, nextState)) { - listener.STATE = nextState + if (!deepEquals(selectorState, nextselectorState)) { + listener.SELECTOR_STATE = nextselectorState listener(_newState, _prevState) } @@ -43,8 +46,8 @@ export const createStore = (initialState = {}, options = {}) => { const subscribe = (listener, selector) => { if (selector) { - listener.LISTENER = Symbol('@@killa-listener') - listener.STATE = selector(state) + listener.LISTENER = SYMBOL_LISTENER + listener.SELECTOR_STATE = selector(state) listener.SELECTOR = selector } diff --git a/src/deep-equals.js b/src/deep-equals.js new file mode 100644 index 0000000..1d29385 --- /dev/null +++ b/src/deep-equals.js @@ -0,0 +1,67 @@ +const isObject = (object) => { + return object !== null && typeof object === 'object' +} + +export const deepEquals = (typeA, typeB) => { + if (!isObject(typeA) && !isObject(typeB) && Object.is(typeA, typeB)) { + return true + } + + if (typeA instanceof Date && typeB instanceof Date) { + return Object.is(typeA.getTime(), typeB.getTime()) + } + + if ( + !isObject(typeA) || typeA === null || + !isObject(typeB) || typeB === null + ) { + return false + } + + if (typeA instanceof Map && typeB instanceof Map) { + if (typeA.size !== typeB.size) { + return false + } + + for (const item of typeA) { + const key = item[0] + const value = item[1] + + if (!Object.is(value, typeB.get(key))) { + return deepEquals(value, typeB.get(key)) + } + } + + return true + } + + if (typeA instanceof Set && typeB instanceof Set) { + if (typeA.size !== typeB.size) { + return false + } + + return deepEquals([...typeA.values()], [...typeB.values()]) + } + + const keysA = Object.keys(typeA) + const keysB = Object.keys(typeB) + + if (keysA.length !== keysB.length) { + return false + } + + for (const key of keysA) { + const valueA = typeA[key] + const valueB = typeB[key] + const areObjects = isObject(valueA) && isObject(valueB) + + if ( + (areObjects && !deepEquals(valueA, valueB)) || + (!areObjects && !Object.is(valueA, valueB)) + ) { + return false + } + } + + return true +}