-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move mergeDeep helper from apollo-cache-inmemory to apollo-utilities.
- Loading branch information
Showing
4 changed files
with
163 additions
and
85 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
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,88 @@ | ||
import { mergeDeep } from '../mergeDeep'; | ||
|
||
describe('mergeDeep', function() { | ||
it('should return an object if first argument falsy', function() { | ||
expect(mergeDeep()).toEqual({}); | ||
expect(mergeDeep(null)).toEqual({}); | ||
expect(mergeDeep(null, { foo: 42 })).toEqual({ foo: 42 }); | ||
}); | ||
|
||
it('should preserve identity for single arguments', function() { | ||
const arg = Object.create(null); | ||
expect(mergeDeep(arg)).toBe(arg); | ||
}); | ||
|
||
it('should preserve identity when merging non-conflicting objects', function() { | ||
const a = { a: { name: 'ay' } }; | ||
const b = { b: { name: 'bee' } }; | ||
const c = mergeDeep(a, b); | ||
expect(c.a).toBe(a.a); | ||
expect(c.b).toBe(b.b); | ||
expect(c).toEqual({ | ||
a: { name: 'ay' }, | ||
b: { name: 'bee' }, | ||
}); | ||
}); | ||
|
||
it('should shallow-copy conflicting fields', function() { | ||
const a = { conflict: { fromA: [1, 2, 3] } }; | ||
const b = { conflict: { fromB: [4, 5] } }; | ||
const c = mergeDeep(a, b); | ||
expect(c.conflict).not.toBe(a.conflict); | ||
expect(c.conflict).not.toBe(b.conflict); | ||
expect(c.conflict.fromA).toBe(a.conflict.fromA); | ||
expect(c.conflict.fromB).toBe(b.conflict.fromB); | ||
expect(c).toEqual({ | ||
conflict: { | ||
fromA: [1, 2, 3], | ||
fromB: [4, 5], | ||
}, | ||
}); | ||
}); | ||
|
||
it('should resolve conflicts among more than two objects', function() { | ||
const sources = []; | ||
|
||
for (let i = 0; i < 100; ++i) { | ||
sources.push({ | ||
['unique' + i]: { value: i }, | ||
conflict: { | ||
['from' + i]: { value: i }, | ||
nested: { | ||
['nested' + i]: { value: i }, | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
const merged = mergeDeep(...sources); | ||
|
||
sources.forEach((source, i) => { | ||
expect(merged['unique' + i].value).toBe(i); | ||
expect(source['unique' + i]).toBe(merged['unique' + i]); | ||
|
||
expect(merged.conflict).not.toBe(source.conflict); | ||
expect(merged.conflict['from' + i].value).toBe(i); | ||
expect(merged.conflict['from' + i]).toBe(source.conflict['from' + i]); | ||
|
||
expect(merged.conflict.nested).not.toBe(source.conflict.nested); | ||
expect(merged.conflict.nested['nested' + i].value).toBe(i); | ||
expect(merged.conflict.nested['nested' + i]).toBe( | ||
source.conflict.nested['nested' + i], | ||
); | ||
}); | ||
}); | ||
|
||
it('can merge array elements', function() { | ||
const a = [{ a: 1 }, { a: 'ay' }, 'a']; | ||
const b = [{ b: 2 }, { b: 'bee' }, 'b']; | ||
const c = [{ c: 3 }, { c: 'cee' }, 'c']; | ||
const d = { 1: { d: 'dee' } }; | ||
|
||
expect(mergeDeep(a, b, c, d)).toEqual([ | ||
{ a: 1, b: 2, c: 3 }, | ||
{ a: 'ay', b: 'bee', c: 'cee', d: 'dee' }, | ||
'a', | ||
]); | ||
}); | ||
}); |
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,21 +1,79 @@ | ||
function isObject(item: any): boolean { | ||
return item && typeof item === 'object' && !Array.isArray(item); | ||
const { hasOwnProperty } = Object.prototype; | ||
|
||
// These mergeDeep and mergeDeepArray utilities merge any number of objects | ||
// together, sharing as much memory as possible with the source objects, while | ||
// remaining careful to avoid modifying any source objects. | ||
|
||
export function mergeDeep(...sources: any[]) { | ||
return mergeDeepArray(sources); | ||
} | ||
|
||
export function mergeDeepArray(sources: any[]) { | ||
let first = sources[0] || {}; | ||
const count = sources.length; | ||
if (count > 1) { | ||
const pastCopies: any[] = []; | ||
first = shallowCopyForMerge(first, pastCopies); | ||
for (let i = 1; i < count; ++i) { | ||
mergeHelper(first, sources[i], pastCopies); | ||
} | ||
} | ||
return first; | ||
} | ||
|
||
export function mergeDeep(target: any, source: any): any { | ||
let output = Object.assign({}, target); | ||
if (isObject(target) && isObject(source)) { | ||
Object.keys(source).forEach(key => { | ||
if (isObject(source[key])) { | ||
if (!(key in target)) { | ||
Object.assign(output, { [key]: source[key] }); | ||
} else { | ||
output[key] = mergeDeep(target[key], source[key]); | ||
function mergeHelper( | ||
target: Record<string, any>, | ||
source: Record<string, any>, | ||
pastCopies: any[], | ||
) { | ||
if (source !== null && typeof source === 'object') { | ||
// In case the target has been frozen, make an extensible copy so that | ||
// we can merge properties into the copy. | ||
if (Object.isExtensible && !Object.isExtensible(target)) { | ||
target = shallowCopyForMerge(target, pastCopies); | ||
} | ||
|
||
Object.keys(source).forEach(sourceKey => { | ||
const sourceValue = source[sourceKey]; | ||
if (hasOwnProperty.call(target, sourceKey)) { | ||
const targetValue = target[sourceKey]; | ||
if (sourceValue !== targetValue) { | ||
// When there is a key collision, we need to make a shallow copy of | ||
// target[sourceKey] so the merge does not modify any source objects. | ||
// To avoid making unnecessary copies, we use a simple array to track | ||
// past copies, since it's safe to modify copies created earlier in | ||
// the merge. We use an array for pastCopies instead of a Map or Set, | ||
// since the number of copies should be relatively small, and some | ||
// Map/Set polyfills modify their keys. | ||
target[sourceKey] = mergeHelper( | ||
shallowCopyForMerge(targetValue, pastCopies), | ||
sourceValue, | ||
pastCopies, | ||
); | ||
} | ||
} else { | ||
Object.assign(output, { [key]: source[key] }); | ||
// If there is no collision, the target can safely share memory with | ||
// the source, and the recursion can terminate here. | ||
target[sourceKey] = sourceValue; | ||
} | ||
}); | ||
} | ||
return output; | ||
|
||
return target; | ||
} | ||
|
||
function shallowCopyForMerge<T>(value: T, pastCopies: any[]): T { | ||
if ( | ||
value !== null && | ||
typeof value === 'object' && | ||
pastCopies.indexOf(value) < 0 | ||
) { | ||
if (Array.isArray(value)) { | ||
value = (value as any).slice(0); | ||
} else { | ||
value = { ...(value as any) }; | ||
} | ||
pastCopies.push(value); | ||
} | ||
return value; | ||
} |