improve handling of non-object values in clever merge

improve caching
This commit is contained in:
Tobias Koppers 2021-01-06 10:51:44 +01:00
parent b067bad545
commit 6107ab8cca
2 changed files with 58 additions and 21 deletions

View File

@ -25,9 +25,14 @@ const DYNAMIC_INFO = Symbol("cleverMerge dynamic info");
* {a: 2}
* @param {T} first first object
* @param {O} second second object
* @returns {T & O} merged object of first and second object
* @returns {T & O | T | O} merged object of first and second object
*/
const cachedCleverMerge = (first, second) => {
if (second === undefined) return first;
if (first === undefined) return second;
if (typeof second !== "object" || second === null) return second;
if (typeof first !== "object" || first === null) return first;
let innerCache = mergeCache.get(first);
if (innerCache === undefined) {
innerCache = new WeakMap();
@ -35,7 +40,7 @@ const cachedCleverMerge = (first, second) => {
}
const prevMerge = innerCache.get(second);
if (prevMerge !== undefined) return prevMerge;
const newMerge = cleverMerge(first, second, true);
const newMerge = _cleverMerge(first, second, true);
innerCache.set(second, newMerge);
return newMerge;
};
@ -238,15 +243,31 @@ const getValueType = value => {
/**
* Merges two objects. Objects are deeply clever merged.
* Arrays might reference the old value with "..."
* Arrays might reference the old value with "...".
* Non-object values take preference over object values.
* @template T
* @template O
* @param {T} first first object
* @param {O} second second object
* @returns {T & O | T | O} merged object of first and second object
*/
const cleverMerge = (first, second) => {
if (second === undefined) return first;
if (first === undefined) return second;
if (typeof second !== "object" || second === null) return second;
if (typeof first !== "object" || first === null) return first;
return _cleverMerge(first, second, false);
};
/**
* Merges two objects. Objects are deeply clever merged.
* @param {object} first first object
* @param {object} second second object
* @param {boolean} internalCaching should parsing of objects and nested merges be cached
* @returns {object} merged object of first and second object
*/
const cleverMerge = (first, second, internalCaching = false) => {
if (second === undefined) return first;
if (first === undefined) return second;
const _cleverMerge = (first, second, internalCaching = false) => {
const firstObject = internalCaching
? cachedParseObject(first)
: parseObject(first);
@ -259,15 +280,14 @@ const cleverMerge = (first, second, internalCaching = false) => {
if (fnInfo) {
second = internalCaching
? cachedCleverMerge(fnInfo[1], second)
: cleverMerge(fnInfo[1], second, false);
: cleverMerge(fnInfo[1], second);
fn = fnInfo[0];
}
const newFn = (...args) => {
const fnResult = fn(...args);
if (typeof fnResult !== "object" || fnResult === null) return fnResult;
return internalCaching
? cachedCleverMerge(fnResult, second)
: cleverMerge(fnResult, second, false);
: cleverMerge(fnResult, second);
};
newFn[DYNAMIC_INFO] = [fn, second];
return serializeObject(firstObject.static, { byProperty, fn: newFn });
@ -505,29 +525,33 @@ const removeOperations = obj => {
};
/**
* @param {object} obj the object
* @param {string} byProperty the by description
* @template T
* @param {T} obj the object
* @param {keyof T} byProperty the by description
* @param {...any} values values
* @returns {object} object with merged byProperty
* @returns {T} object with merged byProperty
*/
const resolveByProperty = (obj, byProperty, ...values) => {
if (!(byProperty in obj)) {
if (typeof obj !== "object" || obj === null || !(byProperty in obj)) {
return obj;
}
const { [byProperty]: byValue, ...remaining } = obj;
const { [byProperty]: _byValue, ..._remaining } = obj;
const remaining = /** @type {T} */ (_remaining);
const byValue = /** @type {Record<string, T> | function(...any[]): T} */ (
/** @type {unknown} */ (_byValue)
);
if (typeof byValue === "object") {
const key = values[0];
if (key in byValue) {
return cleverMerge(remaining, byValue[key]);
return cachedCleverMerge(remaining, byValue[key]);
} else if ("default" in byValue) {
return cleverMerge(remaining, byValue.default);
return cachedCleverMerge(remaining, byValue.default);
} else {
return remaining;
return /** @type {T} */ (remaining);
}
} else if (typeof byValue === "function") {
const result = byValue.apply(null, values);
if (typeof result !== "object" || result === null) return result;
return cleverMerge(
return cachedCleverMerge(
remaining,
resolveByProperty(result, byProperty, ...values)
);

View File

@ -4,7 +4,8 @@ const {
cleverMerge,
DELETE,
removeOperations,
resolveByProperty
resolveByProperty,
cachedCleverMerge
} = require("../lib/util/cleverMerge");
describe("cleverMerge", () => {
@ -648,14 +649,26 @@ describe("cleverMerge", () => {
byArguments: () => false
},
false
]
],
nonObject1: [1, 2, 2],
nonObject2: [1, { a: 1 }, 1],
nonObject3: [{ a: 1 }, 1, 1],
nonObject4: [{ a: 1 }, undefined, { a: 1 }],
nonObject5: [undefined, { a: 1 }, { a: 1 }]
};
for (const key of Object.keys(cases)) {
const testCase = cases[key];
it(`should merge ${key} correctly`, () => {
let merged = cleverMerge(testCase[0], testCase[1]);
let merged1 = cachedCleverMerge(testCase[0], testCase[1]);
let merged2 = cachedCleverMerge(testCase[0], testCase[1]);
expect(merged2).toBe(merged1);
merged = resolveByProperty(merged, "byArguments", 1, 2, 3, 4, 5);
merged1 = resolveByProperty(merged1, "byArguments", 1, 2, 3, 4, 5);
merged2 = resolveByProperty(merged2, "byArguments", 1, 2, 3, 4, 5);
expect(merged).toEqual(testCase[2]);
expect(merged1).toEqual(testCase[2]);
expect(merged2).toEqual(testCase[2]);
});
}