-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(store/persistance): ensure preloaded state is in valid shape
- Loading branch information
Showing
6 changed files
with
140 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { mergeSafe } from '@/common/utils' | ||
|
||
describe('mergeSafe', () => { | ||
it('should not merge when types are different', () => { | ||
const target = { a: 1 } | ||
const source = 'string' | ||
const result = mergeSafe(target, source) | ||
expect(result).toEqual(target) | ||
}) | ||
|
||
it('should deeply merge when key from source exists in target', () => { | ||
const target = { a: { b: { c: 1 }, d: 3 } } | ||
const source = { a: { b: { c: 2 } } } | ||
const result = mergeSafe(target, source) | ||
expect(result).toEqual({ a: { b: { c: 2 }, d: 3 } }) | ||
}) | ||
|
||
it('should not merge when key from source does not exist in target', () => { | ||
const target = { a: 1 } | ||
const source = { b: 2 } | ||
const result = mergeSafe(target, source) | ||
expect(result).toEqual(target) | ||
}) | ||
|
||
it('should not deeply merge when nested key from source does not exist in target', () => { | ||
const target = { a: { b: 1 } } | ||
const source = { a: { c: 2 } } | ||
const result = mergeSafe(target, source) | ||
expect(result).toEqual(target) | ||
}) | ||
|
||
it('should not merge arrays', () => { | ||
const target = { a: [1, 2] } | ||
const source = { a: [3, 4] } | ||
const result = mergeSafe(target, source) | ||
expect(result).toEqual(source) | ||
}) | ||
|
||
it('should handle symbols as keys', () => { | ||
const key = Symbol('key') | ||
const target = { [key]: 1 } | ||
const source = { [key]: 2 } | ||
const result = mergeSafe(target, source) | ||
expect(result[key]).toBe(2) | ||
}) | ||
|
||
it('should not mutate the original objects', () => { | ||
const target = { a: 1 } | ||
const source = { a: 2 } | ||
const result = mergeSafe(target, source) | ||
expect(result).not.toBe(target) | ||
expect(result).not.toBe(source) | ||
}) | ||
|
||
it('should not deeply mutate the original objects', () => { | ||
const target = { a: { b: 1 } } | ||
const source = { a: { b: 2 } } | ||
const result = mergeSafe(target, source) | ||
expect(result.a).not.toBe(target.a) | ||
expect(result.a).not.toBe(source.a) | ||
}) | ||
|
||
it('should handle undefined and null values', () => { | ||
const target = { a: 1, b: undefined } | ||
const source = { b: null, c: 3 } | ||
const result = mergeSafe(target, source) | ||
expect(result).toEqual({ a: 1, b: undefined }) | ||
}) | ||
|
||
it('should return the target when no sources are provided', () => { | ||
const target = { a: 1 } | ||
const result = mergeSafe(target) | ||
expect(result).toEqual(target) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export * from './classNames' | ||
export * from './common' | ||
export * from './merge' | ||
export * from './mergeSafe' | ||
export * from './types' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { isPlainObject, isSameType, type PlainObject } from './common' | ||
|
||
const mergeSafeRecursive = <Target>(target: Target, source: unknown): Target => { | ||
if (!isSameType(target, source)) { | ||
return target | ||
} | ||
// source is guaranteed to be a plain object here but TypeScript doesn't know that | ||
if (!isPlainObject(target) || !isPlainObject(source)) { | ||
return source | ||
} | ||
const result: PlainObject = {} | ||
const targetPropertyNames = Object.getOwnPropertyNames(target) | ||
const targetPropertySymbols = Object.getOwnPropertySymbols(target) | ||
const assignTargetProperty = (key: string | symbol): void => { | ||
Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(target, key)!) | ||
} | ||
targetPropertyNames.forEach(assignTargetProperty) | ||
targetPropertySymbols.forEach(assignTargetProperty) | ||
const sourcePropertyNames = Object.getOwnPropertyNames(source) | ||
const sourcePropertySymbols = Object.getOwnPropertySymbols(source) | ||
const assignSourceProperty = (key: string | symbol): void => { | ||
const isKeySymbol = typeof key === 'symbol' | ||
if ( | ||
(!isKeySymbol && targetPropertyNames.includes(key)) || | ||
(isKeySymbol && targetPropertySymbols.includes(key)) | ||
) { | ||
const targetPropertyValue = result[key] | ||
const sourcePropertyValue: unknown = source[key] | ||
Object.defineProperty(result, key, { | ||
...Object.getOwnPropertyDescriptor(source, key), | ||
value: mergeSafeRecursive(targetPropertyValue, sourcePropertyValue), | ||
}) | ||
} | ||
} | ||
sourcePropertyNames.forEach(assignSourceProperty) | ||
sourcePropertySymbols.forEach(assignSourceProperty) | ||
return result | ||
} | ||
|
||
export const mergeSafe = <Target, Sources extends unknown[]>( | ||
target: Target, | ||
...sources: Sources | ||
): Target => sources.reduce<Target>(mergeSafeRecursive, target) |