array-comparisoncomparisoncomparison-tooldeep-diffdiffjson-diffnodejsobject-comparisonobject-diffobjectdiffobjectdifferencereactstreamingstreaming-datatypescript
1081 lines
29 KiB
1081 lines
29 KiB
/** |
|
* @jest-environment jsdom |
|
*/ |
|
import "blob-polyfill"; |
|
import { ReadableStream } from "web-streams-polyfill"; |
|
import prevListFile from "@mocks/prevList.json"; |
|
import nextListFile from "@mocks/nextList.json"; |
|
import { ListStatus } from "@models/list"; |
|
import { StreamListDiff } from "@models/stream"; |
|
import { streamListDiff } from "."; |
|
|
|
// @ts-expect-error - the ReadableStream polyfill is necessary to test ReadableStream in a Node environment. |
|
global.ReadableStream = ReadableStream; |
|
|
|
describe("data emission", () => { |
|
it("emits 'data' event and consider the all the nextList added if no prevList is provided", (done) => { |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const diff = streamListDiff([], nextList, "id", { |
|
chunksSize: 2, |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 1, name: "Item 1" }, |
|
prevIndex: null, |
|
newIndex: 0, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 2, name: "Item 2" }, |
|
prevIndex: null, |
|
newIndex: 1, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
]; |
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event and consider the all the prevList deleted if no nextList is provided", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const diff = streamListDiff(prevList, [], "id", { |
|
chunksSize: 2, |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
{ |
|
previousValue: { id: 1, name: "Item 1" }, |
|
currentValue: null, |
|
prevIndex: 0, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: null, |
|
prevIndex: 1, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
]; |
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("error", (err) => console.error(err)); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event with one object diff by chunk if chunkSize is 0 or undefined", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const nextList = [ |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
]; |
|
const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); |
|
|
|
const expectedChunks = [ |
|
[ |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: { id: 2, name: "Item 2" }, |
|
prevIndex: 1, |
|
newIndex: 0, |
|
indexDiff: -1, |
|
status: ListStatus.MOVED, |
|
}, |
|
], |
|
[ |
|
{ |
|
previousValue: { id: 1, name: "Item 1" }, |
|
currentValue: null, |
|
prevIndex: 0, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
], |
|
[ |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 3, name: "Item 3" }, |
|
prevIndex: null, |
|
newIndex: 1, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
], |
|
]; |
|
|
|
let chunkCount = 0; |
|
|
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks[chunkCount]); |
|
chunkCount++; |
|
}); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(3); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event with 5 object diff by chunk and return the last object diff in a one entry chunk at the end", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 4, name: "Item 4" }, |
|
{ id: 5, name: "Item 5" }, |
|
{ id: 6, name: "Item 6" }, |
|
{ id: 7, name: "Item 7" }, |
|
{ id: 8, name: "Item 8" }, |
|
{ id: 9, name: "Item 9" }, |
|
{ id: 10, name: "Item 10" }, |
|
]; |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item Two" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 5, name: "Item 5" }, |
|
{ id: 6, name: "Item Six" }, |
|
{ id: 7, name: "Item 7" }, |
|
{ id: 10, name: "Item 10" }, |
|
{ id: 11, name: "Item 11" }, |
|
{ id: 9, name: "Item 9" }, |
|
{ id: 8, name: "Item 8" }, |
|
]; |
|
const diff = streamListDiff(prevList, nextList, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
[ |
|
{ |
|
previousValue: { id: 1, name: "Item 1" }, |
|
currentValue: { id: 1, name: "Item 1" }, |
|
prevIndex: 0, |
|
newIndex: 0, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: { id: 2, name: "Item Two" }, |
|
prevIndex: 1, |
|
newIndex: 1, |
|
indexDiff: 0, |
|
status: ListStatus.UPDATED, |
|
}, |
|
{ |
|
previousValue: { id: 3, name: "Item 3" }, |
|
currentValue: { id: 3, name: "Item 3" }, |
|
prevIndex: 2, |
|
newIndex: 2, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 5, name: "Item 5" }, |
|
currentValue: { id: 5, name: "Item 5" }, |
|
prevIndex: 4, |
|
newIndex: 3, |
|
indexDiff: -1, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { id: 6, name: "Item 6" }, |
|
currentValue: { id: 6, name: "Item Six" }, |
|
prevIndex: 5, |
|
newIndex: 4, |
|
indexDiff: -1, |
|
status: ListStatus.UPDATED, |
|
}, |
|
], |
|
[ |
|
{ |
|
previousValue: { id: 7, name: "Item 7" }, |
|
currentValue: { id: 7, name: "Item 7" }, |
|
prevIndex: 6, |
|
newIndex: 5, |
|
indexDiff: -1, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { id: 9, name: "Item 9" }, |
|
currentValue: { id: 9, name: "Item 9" }, |
|
prevIndex: 8, |
|
newIndex: 8, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 10, name: "Item 10" }, |
|
currentValue: { id: 10, name: "Item 10" }, |
|
prevIndex: 9, |
|
newIndex: 6, |
|
indexDiff: -3, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { id: 8, name: "Item 8" }, |
|
currentValue: { id: 8, name: "Item 8" }, |
|
prevIndex: 7, |
|
newIndex: 9, |
|
indexDiff: 2, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { id: 4, name: "Item 4" }, |
|
currentValue: null, |
|
prevIndex: 3, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
], |
|
[ |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 11, name: "Item 11" }, |
|
prevIndex: null, |
|
newIndex: 7, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
], |
|
]; |
|
|
|
let chunkCount = 0; |
|
|
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks[chunkCount]); |
|
chunkCount++; |
|
}); |
|
|
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(3); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event with all the objects diff in a single chunk if the chunkSize is bigger than the provided lists ", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 4, name: "Item 4" }, |
|
]; |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item Two" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 5, name: "Item 5" }, |
|
]; |
|
|
|
const diff = streamListDiff(prevList, nextList, "id", { |
|
chunksSize: 150, |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
{ |
|
previousValue: { id: 1, name: "Item 1" }, |
|
currentValue: { id: 1, name: "Item 1" }, |
|
prevIndex: 0, |
|
newIndex: 0, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: { id: 2, name: "Item Two" }, |
|
prevIndex: 1, |
|
newIndex: 1, |
|
indexDiff: 0, |
|
status: ListStatus.UPDATED, |
|
}, |
|
{ |
|
previousValue: { id: 3, name: "Item 3" }, |
|
currentValue: { id: 3, name: "Item 3" }, |
|
prevIndex: 2, |
|
newIndex: 2, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 4, name: "Item 4" }, |
|
currentValue: null, |
|
prevIndex: 3, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 5, name: "Item 5" }, |
|
prevIndex: null, |
|
newIndex: 3, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
]; |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
|
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event with moved objects considered as updated if considerMoveAsUpdate is true", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 4, name: "Item 4" }, |
|
]; |
|
const nextList = [ |
|
{ id: 2, name: "Item Two" }, |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 5, name: "Item 5" }, |
|
]; |
|
const diff = streamListDiff(prevList, nextList, "id", { |
|
chunksSize: 5, |
|
considerMoveAsUpdate: true, |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: { id: 2, name: "Item Two" }, |
|
prevIndex: 1, |
|
newIndex: 0, |
|
indexDiff: -1, |
|
status: ListStatus.UPDATED, |
|
}, |
|
{ |
|
previousValue: { id: 1, name: "Item 1" }, |
|
currentValue: { id: 1, name: "Item 1" }, |
|
prevIndex: 0, |
|
newIndex: 1, |
|
indexDiff: 1, |
|
status: ListStatus.UPDATED, |
|
}, |
|
{ |
|
previousValue: { id: 3, name: "Item 3" }, |
|
currentValue: { id: 3, name: "Item 3" }, |
|
prevIndex: 2, |
|
newIndex: 2, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 4, name: "Item 4" }, |
|
currentValue: null, |
|
prevIndex: 3, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 5, name: "Item 5" }, |
|
prevIndex: null, |
|
newIndex: 3, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
]; |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
|
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event only with objects diff whose status match with showOnly's", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 4, name: "Item 4" }, |
|
]; |
|
const nextList = [ |
|
{ id: 2, name: "Item Two" }, |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 5, name: "Item 5" }, |
|
]; |
|
const diff = streamListDiff(prevList, nextList, "id", { |
|
chunksSize: 5, |
|
showOnly: ["added", "deleted"], |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
{ |
|
previousValue: { id: 4, name: "Item 4" }, |
|
currentValue: null, |
|
prevIndex: 3, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 5, name: "Item 5" }, |
|
prevIndex: null, |
|
newIndex: 3, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
]; |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
|
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("emits 'data' event with deep nested objects diff", (done) => { |
|
const prevList = [ |
|
{ |
|
id: 1, |
|
name: "Item 1", |
|
user: { role: "admin", hobbies: ["golf", "football"] }, |
|
}, |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3", user: { role: "admin", hobbies: ["rugby"] } }, |
|
{ |
|
id: 4, |
|
name: "Item 4", |
|
user: { role: "reader", hobbies: ["video games", "fishing"] }, |
|
}, |
|
{ id: 5, name: "Item 5" }, |
|
{ id: 6, name: "Item 6", user: { role: "root", hobbies: ["coding"] } }, |
|
{ id: 7, name: "Item 7" }, |
|
{ id: 8, name: "Item 8" }, |
|
{ id: 9, name: "Item 9" }, |
|
{ |
|
id: 10, |
|
name: "Item 10", |
|
user: { |
|
role: "root", |
|
hobbies: ["coding"], |
|
skills: { driving: true, diving: false }, |
|
}, |
|
}, |
|
]; |
|
const nextList = [ |
|
{ |
|
id: 1, |
|
name: "Item 1", |
|
user: { role: "admin", hobbies: ["golf", "football"] }, |
|
}, |
|
{ id: 2, name: "Item Two" }, |
|
{ id: 3, name: "Item 3", user: { role: "admin", hobbies: ["rugby"] } }, |
|
{ id: 5, name: "Item 5" }, |
|
{ id: 6, name: "Item 6", user: { role: "root", hobbies: ["farming"] } }, |
|
{ id: 7, name: "Item 7" }, |
|
{ |
|
id: 10, |
|
name: "Item 10", |
|
user: { |
|
role: "root", |
|
hobbies: ["coding"], |
|
skills: { driving: true, diving: false }, |
|
}, |
|
}, |
|
{ id: 11, name: "Item 11" }, |
|
{ id: 9, name: "Item 9" }, |
|
{ id: 8, name: "Item 8" }, |
|
]; |
|
const diff = streamListDiff(prevList, nextList, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
const expectedChunks = [ |
|
[ |
|
{ |
|
previousValue: { |
|
id: 1, |
|
name: "Item 1", |
|
user: { role: "admin", hobbies: ["golf", "football"] }, |
|
}, |
|
currentValue: { |
|
id: 1, |
|
name: "Item 1", |
|
user: { role: "admin", hobbies: ["golf", "football"] }, |
|
}, |
|
prevIndex: 0, |
|
newIndex: 0, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: { id: 2, name: "Item Two" }, |
|
prevIndex: 1, |
|
newIndex: 1, |
|
indexDiff: 0, |
|
status: ListStatus.UPDATED, |
|
}, |
|
{ |
|
previousValue: { |
|
id: 3, |
|
name: "Item 3", |
|
user: { role: "admin", hobbies: ["rugby"] }, |
|
}, |
|
currentValue: { |
|
id: 3, |
|
name: "Item 3", |
|
user: { role: "admin", hobbies: ["rugby"] }, |
|
}, |
|
prevIndex: 2, |
|
newIndex: 2, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 5, name: "Item 5" }, |
|
currentValue: { id: 5, name: "Item 5" }, |
|
prevIndex: 4, |
|
newIndex: 3, |
|
indexDiff: -1, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { |
|
id: 6, |
|
name: "Item 6", |
|
user: { role: "root", hobbies: ["coding"] }, |
|
}, |
|
currentValue: { |
|
id: 6, |
|
name: "Item 6", |
|
user: { role: "root", hobbies: ["farming"] }, |
|
}, |
|
prevIndex: 5, |
|
newIndex: 4, |
|
indexDiff: -1, |
|
status: ListStatus.UPDATED, |
|
}, |
|
], |
|
[ |
|
{ |
|
previousValue: { id: 7, name: "Item 7" }, |
|
currentValue: { id: 7, name: "Item 7" }, |
|
prevIndex: 6, |
|
newIndex: 5, |
|
indexDiff: -1, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { id: 9, name: "Item 9" }, |
|
currentValue: { id: 9, name: "Item 9" }, |
|
prevIndex: 8, |
|
newIndex: 8, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { |
|
id: 10, |
|
name: "Item 10", |
|
user: { |
|
role: "root", |
|
hobbies: ["coding"], |
|
skills: { driving: true, diving: false }, |
|
}, |
|
}, |
|
currentValue: { |
|
id: 10, |
|
name: "Item 10", |
|
user: { |
|
role: "root", |
|
hobbies: ["coding"], |
|
skills: { driving: true, diving: false }, |
|
}, |
|
}, |
|
prevIndex: 9, |
|
newIndex: 6, |
|
indexDiff: -3, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { id: 8, name: "Item 8" }, |
|
currentValue: { id: 8, name: "Item 8" }, |
|
prevIndex: 7, |
|
newIndex: 9, |
|
indexDiff: 2, |
|
status: ListStatus.MOVED, |
|
}, |
|
{ |
|
previousValue: { |
|
id: 4, |
|
name: "Item 4", |
|
user: { role: "reader", hobbies: ["video games", "fishing"] }, |
|
}, |
|
currentValue: null, |
|
prevIndex: 3, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
], |
|
[ |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 11, name: "Item 11" }, |
|
prevIndex: null, |
|
newIndex: 7, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
], |
|
]; |
|
|
|
let chunkCount = 0; |
|
|
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks[chunkCount]); |
|
chunkCount++; |
|
}); |
|
|
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(3); |
|
done(); |
|
}); |
|
}); |
|
}); |
|
|
|
describe("input handling", () => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 4, name: "Item 4" }, |
|
]; |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item Two" }, |
|
{ id: 3, name: "Item 3" }, |
|
{ id: 5, name: "Item 5" }, |
|
]; |
|
const expectedChunks = [ |
|
{ |
|
previousValue: { id: 1, name: "Item 1" }, |
|
currentValue: { id: 1, name: "Item 1" }, |
|
prevIndex: 0, |
|
newIndex: 0, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 2, name: "Item 2" }, |
|
currentValue: { id: 2, name: "Item Two" }, |
|
prevIndex: 1, |
|
newIndex: 1, |
|
indexDiff: 0, |
|
status: ListStatus.UPDATED, |
|
}, |
|
{ |
|
previousValue: { id: 3, name: "Item 3" }, |
|
currentValue: { id: 3, name: "Item 3" }, |
|
prevIndex: 2, |
|
newIndex: 2, |
|
indexDiff: 0, |
|
status: ListStatus.EQUAL, |
|
}, |
|
{ |
|
previousValue: { id: 4, name: "Item 4" }, |
|
currentValue: null, |
|
prevIndex: 3, |
|
newIndex: null, |
|
indexDiff: null, |
|
status: ListStatus.DELETED, |
|
}, |
|
{ |
|
previousValue: null, |
|
currentValue: { id: 5, name: "Item 5" }, |
|
prevIndex: null, |
|
newIndex: 3, |
|
indexDiff: null, |
|
status: ListStatus.ADDED, |
|
}, |
|
]; |
|
|
|
it("handles two readable streams", (done) => { |
|
const prevStream = new ReadableStream({ |
|
start(controller) { |
|
prevList.forEach((item) => controller.enqueue(item)); |
|
controller.close(); |
|
}, |
|
}); |
|
const nextStream = new ReadableStream({ |
|
start(controller) { |
|
nextList.forEach((item) => controller.enqueue(item)); |
|
controller.close(); |
|
}, |
|
}); |
|
|
|
const diff = streamListDiff(prevStream, nextStream, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("error", (err) => console.error(err)); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("handles two local files", (done) => { |
|
const prevFile = new File([JSON.stringify(prevListFile)], "prevList.json", { |
|
type: "application/json", |
|
}); |
|
|
|
const nextFile = new File([JSON.stringify(nextListFile)], "nextList.json", { |
|
type: "application/json", |
|
}); |
|
|
|
const diff = streamListDiff(prevFile, nextFile, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("error", (err) => console.error(err)); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("handles a readable stream against a local file", (done) => { |
|
const prevStream = new ReadableStream({ |
|
start(controller) { |
|
prevList.forEach((item) => controller.enqueue(item)); |
|
controller.close(); |
|
}, |
|
}); |
|
const nextFile = new File([JSON.stringify(nextListFile)], "nextList.json", { |
|
type: "application/json", |
|
}); |
|
|
|
const diff = streamListDiff(prevStream, nextFile, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("error", (err) => console.error(err)); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("handles a readable stream against an array", (done) => { |
|
const prevStream = new ReadableStream({ |
|
start(controller) { |
|
prevList.forEach((item) => controller.enqueue(item)); |
|
controller.close(); |
|
}, |
|
}); |
|
|
|
const diff = streamListDiff(prevStream, nextList, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("error", (err) => console.error(err)); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
it("handles a local file against an array", (done) => { |
|
const prevFile = new File([JSON.stringify(prevListFile)], "prevList.json", { |
|
type: "application/json", |
|
}); |
|
|
|
const diff = streamListDiff(prevFile, nextList, "id", { |
|
chunksSize: 5, |
|
useWorker: false, |
|
}); |
|
|
|
let chunkCount = 0; |
|
diff.on("data", (chunk) => { |
|
expect(chunk).toStrictEqual(expectedChunks); |
|
chunkCount++; |
|
}); |
|
diff.on("error", (err) => console.error(err)); |
|
diff.on("finish", () => { |
|
expect(chunkCount).toBe(1); |
|
done(); |
|
}); |
|
}); |
|
}); |
|
|
|
describe("finish event", () => { |
|
it("emits 'finish' event if no prevList nor nextList is provided", (done) => { |
|
const diff = streamListDiff([], [], "id", { useWorker: false }); |
|
diff.on("finish", () => done()); |
|
}); |
|
it("emits 'finish' event when all the chunks have been processed", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const nextList = [ |
|
{ id: 2, name: "Item 2" }, |
|
{ id: 3, name: "Item 3" }, |
|
]; |
|
const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); |
|
diff.on("finish", () => done()); |
|
}); |
|
}); |
|
|
|
describe("error event", () => { |
|
test("emits 'error' event when prevList has invalid data", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
"hello", |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
|
|
// @ts-expect-error prevList is invalid by design for the test |
|
const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
`Your prevList must only contain valid objects. Found 'hello'`, |
|
); |
|
done(); |
|
}); |
|
}); |
|
|
|
test("emits 'error' event when nextList has invalid data", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
"hello", |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
|
|
// @ts-expect-error nextList is invalid by design for the test |
|
const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
`Your nextList must only contain valid objects. Found 'hello'`, |
|
); |
|
done(); |
|
}); |
|
}); |
|
|
|
test("emits 'error' event when all prevList ojects don't have the requested reference property", (done) => { |
|
const prevList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; |
|
const nextList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
|
|
const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
`The reference property 'id' is not available in all the objects of your prevList.`, |
|
); |
|
done(); |
|
}); |
|
}); |
|
|
|
test("emits 'error' event when all nextList ojects don't have the requested reference property", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; |
|
|
|
const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
`The reference property 'id' is not available in all the objects of your nextList.`, |
|
); |
|
done(); |
|
}); |
|
}); |
|
|
|
test("emits 'error' event when the chunkSize option is negative", (done) => { |
|
const prevList = [ |
|
{ id: 1, name: "Item 1" }, |
|
{ id: 2, name: "Item 2" }, |
|
]; |
|
const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; |
|
|
|
const diff = streamListDiff(prevList, nextList, "id", { |
|
chunksSize: -3, |
|
useWorker: false, |
|
}); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
"The chunk size can't be negative. You entered the value '-3'", |
|
); |
|
done(); |
|
}); |
|
}); |
|
|
|
test("emits 'error' event when the prevList is not a valid type", (done) => { |
|
const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; |
|
|
|
// @ts-expect-error - prevList is invalid by design for the test |
|
const diff = streamListDiff({ name: "hello" }, nextList, "id", { |
|
useWorker: false, |
|
}); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
"Invalid prevList. Expected ReadableStream, Array, or File.", |
|
); |
|
done(); |
|
}); |
|
}); |
|
test("emits 'error' event when the nextList is not a valid type", (done) => { |
|
const prevList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; |
|
|
|
// @ts-expect-error - nextList is invalid by design for the test |
|
const diff = streamListDiff(prevList, null, "id", { useWorker: false }); |
|
|
|
diff.on("error", (err) => { |
|
expect(err["message"]).toEqual( |
|
"Invalid nextList. Expected ReadableStream, Array, or File.", |
|
); |
|
done(); |
|
}); |
|
}); |
|
}); |
|
|
|
const generateLargeDataset = (count: number) => { |
|
const data: Array<{ id: number; value: string }> = []; |
|
for (let i = 0; i < count; i++) { |
|
data.push({ id: i, value: `value-${i}` }); |
|
} |
|
return data; |
|
}; |
|
|
|
describe("performance", () => { |
|
it("process 100.000 in each stream", (done) => { |
|
const numEntries = 100_000; |
|
|
|
const prevList = generateLargeDataset(numEntries); |
|
const nextList = generateLargeDataset(numEntries); |
|
|
|
nextList[100].value = "updated-value-100"; // 1 updated entry |
|
nextList[20_000].value = "updated-value-20000"; // Another updated entry |
|
nextList.push({ id: numEntries, value: `new-value-${numEntries}` }); // 1 added entry |
|
|
|
const diffListener = streamListDiff<{ id: number; value: string }>( |
|
prevList, |
|
nextList, |
|
"id", |
|
{ |
|
chunksSize: 10_000, |
|
}, |
|
); |
|
|
|
const diffs: StreamListDiff<{ id: number; value: string }>[] = []; |
|
|
|
diffListener.on("data", (chunk) => { |
|
diffs.push(...chunk); |
|
}); |
|
|
|
diffListener.on("finish", () => { |
|
try { |
|
const updatedEntries = diffs.filter((d) => d.status === "updated"); |
|
const addedEntries = diffs.filter((d) => d.status === "added"); |
|
const deletedEntries = diffs.filter((d) => d.status === "deleted"); |
|
const equalEntries = diffs.filter((d) => d.status === "equal"); |
|
|
|
expect(updatedEntries.length).toBe(2); |
|
expect(addedEntries.length).toBe(1); |
|
expect(deletedEntries.length).toBe(0); |
|
expect(equalEntries.length).toBe(99998); |
|
done(); |
|
} catch (err) { |
|
done(err); |
|
} |
|
}); |
|
|
|
diffListener.on("error", (err) => done(err)); |
|
}); |
|
});
|
|
|