/** * Computes the partial difference between two values. * Returns `undefined` if the values are equal, or `{ diff }` containing only the changed parts. * For arrays, the diff contains only items in `next` that have no deep-equal counterpart in `prev`. * For objects, the diff contains only keys whose values changed. * * @param prev - The original value * @param next - The updated value * @returns An object containing the diff, or `undefined` if the values are equal */ export function partialDiff( prev: T, next: T, ): { diff: Partial } | undefined { if (prev === next) { return } else if (Array.isArray(prev) && Array.isArray(next)) { const res = { diff: [] as any[] } for (let newItem of next) { let anyEq = false for (let oldItem of prev) { if (!partialDiff(oldItem, newItem)) { anyEq = true break } } if (!anyEq) { res.diff.push(newItem) } } if (res.diff.length) { return res as any } else { return } } else if (typeof prev === 'object' && typeof next === 'object') { if (prev === null || next === null) return { diff: next } const res = { diff: {} as Record } const keys = Object.keys(next) as (keyof T)[] for (let key in prev) { if (!keys.includes(key)) keys.push(key) } for (let key of keys) { const diff = partialDiff(prev[key], next[key]) if (diff) { res.diff[key] = diff.diff } } if (Object.keys(res.diff).length) { return res } else { return } } else { return { diff: next } } } /** * Deeply merges multiple values together. Objects are merged key-by-key recursively. * Arrays are merged by appending items that are not already present (by deep equality). * Primitives are resolved by taking the last argument. * * @param args - The values to merge, applied left to right * @returns The merged result */ export function deepMerge(...args: unknown[]): unknown { const lastItem = (args as any)[args.length - 1] if (typeof lastItem !== 'object' || !lastItem) return lastItem if (Array.isArray(lastItem)) return deepMergeList( ...(args.filter((x) => Array.isArray(x)) as unknown[][]), ) return deepMergeObject( ...(args.filter( (x) => typeof x === 'object' && x && !Array.isArray(x), ) as object[]), ) } function deepMergeList(...args: unknown[][]): unknown[] { const res: unknown[] = [] for (let arg of args) { for (let item of arg) { if (!res.some((x) => !partialDiff(x, item))) { res.push(item) } } } return res } function deepMergeObject(...args: object[]): object { const lastItem = (args as any)[args.length - 1] if (args.length === 0) return lastItem as any if (args.length === 1) args.unshift({}) const allKeys = new Set(args.flatMap((x) => Object.keys(x))) for (const key of allKeys) { const filteredValues = args.flatMap((x) => key in x ? [(x as any)[key]] : [], ) ;(args as any)[0][key] = deepMerge(...filteredValues) } return args[0] as any }