array-comparisoncomparisoncomparison-tooldeep-diffdiffjson-diffnodejsobject-comparisonobject-diffobjectdiffobjectdifferencereactstreamingstreaming-datatypescript
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
6.9 KiB
238 lines
6.9 KiB
import { |
|
DEFAULT_LIST_STREAM_OPTIONS, |
|
ListStreamOptions, |
|
ReferenceProperty, |
|
StreamListsDiff, |
|
StreamReferences, |
|
} from "@models/stream"; |
|
import { LIST_STATUS } from "@models/list"; |
|
import { isEqual } from "@lib/utils"; |
|
import { EventEmitter, StreamEvent } from "./emitter"; |
|
|
|
function outputDiffChunk<T extends Record<string, unknown>>( |
|
emitter: EventEmitter, |
|
) { |
|
let chunks: StreamListsDiff<T>[] = []; |
|
|
|
return function handleDiffChunk( |
|
chunk: StreamListsDiff<T>, |
|
isLastChunk: boolean, |
|
options: ListStreamOptions, |
|
): void { |
|
const showChunk = options?.showOnly |
|
? options?.showOnly.includes(chunk.status) |
|
: true; |
|
if (!showChunk) { |
|
return; |
|
} |
|
if ((options.chunksSize as number) > 0) { |
|
chunks.push(chunk); |
|
if (chunks.length >= (options.chunksSize as number) || isLastChunk) { |
|
const output = chunks; |
|
chunks = []; |
|
return emitter.emit(StreamEvent.Data, output); |
|
} |
|
} |
|
return emitter.emit(StreamEvent.Data, [chunk]); |
|
}; |
|
} |
|
|
|
function formatSingleListStreamDiff<T extends Record<string, unknown>>( |
|
list: T[], |
|
isPrevious: boolean, |
|
status: LIST_STATUS, |
|
options: ListStreamOptions, |
|
): StreamListsDiff<T>[] { |
|
const diff: StreamListsDiff<T>[] = list.map((data, i) => ({ |
|
previousValue: isPrevious ? data : null, |
|
currentValue: isPrevious ? null : data, |
|
prevIndex: status === LIST_STATUS.ADDED ? null : i, |
|
newIndex: status === LIST_STATUS.ADDED ? i : null, |
|
indexDiff: null, |
|
status, |
|
})); |
|
if (options.showOnly && options.showOnly.length > 0) { |
|
return diff.filter((value) => options.showOnly?.includes(value.status)); |
|
} |
|
return diff; |
|
} |
|
|
|
function isValidChunkSize( |
|
chunksSize: ListStreamOptions["chunksSize"], |
|
): boolean { |
|
if (!chunksSize) return true; |
|
const x = String(Math.sign(chunksSize)); |
|
return x !== "-1" && x !== "NaN"; |
|
} |
|
|
|
function getDiffChunks<T extends Record<string, unknown>>( |
|
prevList: T[], |
|
nextList: T[], |
|
referenceProperty: ReferenceProperty<T>, |
|
emitter: EventEmitter, |
|
options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, |
|
) { |
|
if (!isValidChunkSize(options?.chunksSize)) { |
|
return emitter.emit( |
|
StreamEvent.Error, |
|
`The chunk size can't be negative. You entered the value ${options.chunksSize}`, |
|
); |
|
} |
|
if (!prevList && !nextList) { |
|
return []; |
|
} |
|
if (!prevList) { |
|
const nextDiff = formatSingleListStreamDiff( |
|
nextList as T[], |
|
false, |
|
LIST_STATUS.ADDED, |
|
options, |
|
); |
|
return nextDiff.forEach((data, i) => |
|
handleDiffChunk(data, i === nextDiff.length - 1, options), |
|
); |
|
} |
|
if (!nextList) { |
|
const prevDiff = formatSingleListStreamDiff( |
|
prevList as T[], |
|
true, |
|
LIST_STATUS.DELETED, |
|
options, |
|
); |
|
return prevDiff.forEach((data, i) => |
|
handleDiffChunk(data, i === prevDiff.length - 1, options), |
|
); |
|
} |
|
const listsReferences: StreamReferences<T> = new Map(); |
|
const handleDiffChunk = outputDiffChunk<T>(emitter); |
|
prevList.forEach((data, i) => { |
|
if (data) { |
|
listsReferences.set(String(data[referenceProperty]), { |
|
prevIndex: i, |
|
nextIndex: undefined, |
|
}); |
|
} |
|
}); |
|
|
|
nextList.forEach((data, i) => { |
|
if (data) { |
|
const listReference = listsReferences.get( |
|
String(data[referenceProperty]), |
|
); |
|
if (listReference) { |
|
listReference.nextIndex = i; |
|
} else { |
|
handleDiffChunk( |
|
{ |
|
previousValue: null, |
|
currentValue: data, |
|
prevIndex: null, |
|
newIndex: i, |
|
indexDiff: null, |
|
status: LIST_STATUS.ADDED, |
|
}, |
|
i === nextList.length - 1, |
|
options, |
|
); |
|
} |
|
} |
|
}); |
|
let streamedChunks = 0; |
|
const totalChunks = listsReferences.size; |
|
for (const data of listsReferences.values()) { |
|
streamedChunks++; |
|
const isLastChunk = totalChunks === streamedChunks; |
|
if (!data.nextIndex) { |
|
handleDiffChunk( |
|
{ |
|
previousValue: prevList[data.prevIndex], |
|
currentValue: null, |
|
prevIndex: data.prevIndex, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: LIST_STATUS.DELETED, |
|
}, |
|
isLastChunk, |
|
options, |
|
); |
|
} else { |
|
const prevData = prevList[data.prevIndex]; |
|
const nextData = nextList[data.nextIndex]; |
|
const isDataEqual = isEqual(prevData, nextData); |
|
const indexDiff = data.prevIndex - data.nextIndex; |
|
if (isDataEqual) { |
|
if (indexDiff === 0) { |
|
handleDiffChunk( |
|
{ |
|
previousValue: prevList[data.prevIndex], |
|
currentValue: nextList[data.nextIndex], |
|
prevIndex: null, |
|
newIndex: data.nextIndex, |
|
indexDiff: null, |
|
status: LIST_STATUS.EQUAL, |
|
}, |
|
isLastChunk, |
|
options, |
|
); |
|
} else { |
|
handleDiffChunk( |
|
{ |
|
previousValue: prevList[data.prevIndex], |
|
currentValue: nextList[data.nextIndex], |
|
prevIndex: data.prevIndex, |
|
newIndex: data.nextIndex, |
|
indexDiff, |
|
status: options.considerMoveAsUpdate |
|
? LIST_STATUS.UPDATED |
|
: LIST_STATUS.MOVED, |
|
}, |
|
isLastChunk, |
|
options, |
|
); |
|
} |
|
} else { |
|
handleDiffChunk( |
|
{ |
|
previousValue: prevList[data.prevIndex], |
|
currentValue: nextList[data.nextIndex], |
|
prevIndex: data.prevIndex, |
|
newIndex: data.nextIndex, |
|
indexDiff, |
|
status: LIST_STATUS.UPDATED, |
|
}, |
|
isLastChunk, |
|
options, |
|
); |
|
} |
|
} |
|
} |
|
return emitter.emit(StreamEvent.Finish); |
|
} |
|
|
|
/** |
|
* Streams the diff of two object lists |
|
* @param {Record<string, unknown>[]} prevList - The original object list. |
|
* @param {Record<string, unknown>[]} nextList - The new object list. |
|
* @param {ReferenceProperty<T>} referenceProperty - A common property in all the objects of your lists (e.g. `id`) |
|
* @param {ListStreamOptions} options - Options to refine your output. |
|
- `chunksSize`: the number of object diffs returned by each stream chunk. If set to `0`, each stream will return a single object diff. If set to `10` each stream will return 10 object diffs. (default is `0`) |
|
- `showOnly`: returns only the values whose status you are interested in. (e.g. `["added", "equal"]`) |
|
- `considerMoveAsUpdate`: if set to `true` a `moved` object will be considered as `updated` |
|
* @returns EventEmitter |
|
*/ |
|
export function streamListsDiff<T extends Record<string, unknown>>( |
|
prevList: T[], |
|
nextList: T[], |
|
referenceProperty: ReferenceProperty<T>, |
|
options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, |
|
): EventEmitter { |
|
const emitter = new EventEmitter(); |
|
setTimeout(() => { |
|
try { |
|
getDiffChunks(prevList, nextList, referenceProperty, emitter, options); |
|
} catch (err) { |
|
return emitter.emit(StreamEvent.Error, err); |
|
} |
|
}, 0); |
|
return emitter; |
|
}
|
|
|