Browse Source

chore: test stream-list-diff

pull/26/head
Antoine Lanoe 7 months ago
parent
commit
7fe7a84082
  1. 90
      src/lib/stream-list-diff/index.ts
  2. 374
      src/lib/stream-list-diff/stream-list-diff.test.ts

90
src/lib/stream-list-diff/index.ts

@ -6,7 +6,7 @@ import {
StreamReferences, StreamReferences,
} from "@models/stream"; } from "@models/stream";
import { LIST_STATUS } from "@models/list"; import { LIST_STATUS } from "@models/list";
import { isEqual, isObject } from "@lib/utils"; import { isObject } from "@lib/utils";
import { Emitter, EventEmitter, StreamEvent } from "./emitter"; import { Emitter, EventEmitter, StreamEvent } from "./emitter";
function outputDiffChunk<T extends Record<string, unknown>>( function outputDiffChunk<T extends Record<string, unknown>>(
@ -31,6 +31,8 @@ function outputDiffChunk<T extends Record<string, unknown>>(
const output = chunks; const output = chunks;
chunks = []; chunks = [];
return emitter.emit(StreamEvent.Data, output); return emitter.emit(StreamEvent.Data, output);
} else {
return;
} }
} }
return emitter.emit(StreamEvent.Data, [chunk]); return emitter.emit(StreamEvent.Data, [chunk]);
@ -73,56 +75,53 @@ function isValidChunkSize(
chunksSize: ListStreamOptions["chunksSize"], chunksSize: ListStreamOptions["chunksSize"],
): boolean { ): boolean {
if (!chunksSize) return true; if (!chunksSize) return true;
const x = String(Math.sign(chunksSize)); const sign = String(Math.sign(chunksSize));
return x !== "-1" && x !== "NaN"; return sign !== "-1" && sign !== "NaN";
} }
function isDataValid<T extends Record<string, unknown>>( function isDataValid<T extends Record<string, unknown>>(
data: T, data: T,
referenceProperty: ReferenceProperty<T>, referenceProperty: ReferenceProperty<T>,
emitter: Emitter<T>,
listType: "prevList" | "nextList", listType: "prevList" | "nextList",
): boolean { ): { isValid: boolean; message?: string } {
if (!isObject(data)) { if (!isObject(data)) {
emitter.emit( return {
StreamEvent.Error, isValid: false,
new Error( message: `Your ${listType} must only contain valid objects. Found '${data}'`,
`Your ${listType} must only contain valid objects. Found ${data}`, };
),
);
return false;
} }
if (!Object.hasOwn(data, referenceProperty)) { if (!Object.hasOwn(data, referenceProperty)) {
emitter.emit( return {
StreamEvent.Error, isValid: false,
new Error( message: `The reference property '${String(referenceProperty)}' is not available in all the objects of your ${listType}.`,
`The reference property ${String(referenceProperty)} is not available in all the objects of your ${listType}.`, };
),
);
return false;
} }
return true; return {
isValid: true,
message: "",
};
} }
function getDiffChunks<T extends Record<string, unknown>>( function getDiffChunks<T extends Record<string, unknown>>(
prevList: T[], prevList: T[] = [],
nextList: T[], nextList: T[] = [],
referenceProperty: ReferenceProperty<T>, referenceProperty: ReferenceProperty<T>,
emitter: Emitter<T>, emitter: Emitter<T>,
options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS,
) { ): void {
if (!isValidChunkSize(options?.chunksSize)) { if (!isValidChunkSize(options?.chunksSize)) {
return emitter.emit( return emitter.emit(
StreamEvent.Error, StreamEvent.Error,
new Error( new Error(
`The chunk size can't be negative. You entered the value ${options.chunksSize}`, `The chunk size can't be negative. You entered the value '${options.chunksSize}'`,
), ),
); );
} }
if (!prevList && !nextList) { if (prevList.length === 0 && nextList.length === 0) {
return []; return emitter.emit(StreamEvent.Finish);
} }
if (!prevList) { const handleDiffChunk = outputDiffChunk<T>(emitter);
if (prevList.length === 0) {
const nextDiff = formatSingleListStreamDiff( const nextDiff = formatSingleListStreamDiff(
nextList as T[], nextList as T[],
false, false,
@ -136,11 +135,12 @@ function getDiffChunks<T extends Record<string, unknown>>(
); );
emitter.emit(StreamEvent.Finish); emitter.emit(StreamEvent.Finish);
} }
return nextDiff?.forEach((data, i) => nextDiff?.forEach((data, i) =>
handleDiffChunk(data, i === nextDiff.length - 1, options), handleDiffChunk(data, i === nextDiff.length - 1, options),
); );
return emitter.emit(StreamEvent.Finish);
} }
if (!nextList) { if (nextList.length === 0) {
const prevDiff = formatSingleListStreamDiff( const prevDiff = formatSingleListStreamDiff(
prevList as T[], prevList as T[],
true, true,
@ -154,17 +154,22 @@ function getDiffChunks<T extends Record<string, unknown>>(
); );
emitter.emit(StreamEvent.Finish); emitter.emit(StreamEvent.Finish);
} }
return prevDiff?.forEach((data, i) => prevDiff?.forEach((data, i) =>
handleDiffChunk(data, i === prevDiff.length - 1, options), handleDiffChunk(data, i === prevDiff.length - 1, options),
); );
return emitter.emit(StreamEvent.Finish);
} }
const listsReferences: StreamReferences<T> = new Map(); const listsReferences: StreamReferences<T> = new Map();
const handleDiffChunk = outputDiffChunk<T>(emitter);
for (let i = 0; i < prevList.length; i++) { for (let i = 0; i < prevList.length; i++) {
const data = prevList[i]; const data = prevList[i];
if (data) { if (data) {
const isValid = isDataValid(data, referenceProperty, emitter, "prevList"); const { isValid, message } = isDataValid(
data,
referenceProperty,
"prevList",
);
if (!isValid) { if (!isValid) {
emitter.emit(StreamEvent.Error, new Error(message));
emitter.emit(StreamEvent.Finish); emitter.emit(StreamEvent.Finish);
break; break;
} }
@ -176,10 +181,15 @@ function getDiffChunks<T extends Record<string, unknown>>(
} }
for (let i = 0; i < nextList.length; i++) { for (let i = 0; i < nextList.length; i++) {
const data = prevList[i]; const data = nextList[i];
if (data) { if (data) {
const isValid = isDataValid(data, referenceProperty, emitter, "nextList"); const { isValid, message } = isDataValid(
data,
referenceProperty,
"nextList",
);
if (!isValid) { if (!isValid) {
emitter.emit(StreamEvent.Error, new Error(message));
emitter.emit(StreamEvent.Finish); emitter.emit(StreamEvent.Finish);
break; break;
} }
@ -207,10 +217,11 @@ function getDiffChunks<T extends Record<string, unknown>>(
let streamedChunks = 0; let streamedChunks = 0;
const totalChunks = listsReferences.size; const totalChunks = listsReferences.size;
for (const data of listsReferences.values()) { for (const data of listsReferences.values()) {
streamedChunks++; streamedChunks++;
const isLastChunk = totalChunks === streamedChunks; const isLastChunk = totalChunks === streamedChunks;
if (!data.nextIndex) { if (typeof data.nextIndex === "undefined") {
handleDiffChunk( handleDiffChunk(
{ {
previousValue: prevList[data.prevIndex], previousValue: prevList[data.prevIndex],
@ -226,17 +237,17 @@ function getDiffChunks<T extends Record<string, unknown>>(
} else { } else {
const prevData = prevList[data.prevIndex]; const prevData = prevList[data.prevIndex];
const nextData = nextList[data.nextIndex]; const nextData = nextList[data.nextIndex];
const isDataEqual = isEqual(prevData, nextData); const isDataEqual = JSON.stringify(prevData) === JSON.stringify(nextData);
const indexDiff = data.prevIndex - data.nextIndex; const indexDiff = data.nextIndex - data.prevIndex;
if (isDataEqual) { if (isDataEqual) {
if (indexDiff === 0) { if (indexDiff === 0) {
handleDiffChunk( handleDiffChunk(
{ {
previousValue: prevList[data.prevIndex], previousValue: prevList[data.prevIndex],
currentValue: nextList[data.nextIndex], currentValue: nextList[data.nextIndex],
prevIndex: null, prevIndex: data.prevIndex,
newIndex: data.nextIndex, newIndex: data.nextIndex,
indexDiff: null, indexDiff: 0,
status: LIST_STATUS.EQUAL, status: LIST_STATUS.EQUAL,
}, },
isLastChunk, isLastChunk,
@ -274,6 +285,7 @@ function getDiffChunks<T extends Record<string, unknown>>(
} }
} }
} }
return emitter.emit(StreamEvent.Finish); return emitter.emit(StreamEvent.Finish);
} }

374
src/lib/stream-list-diff/stream-list-diff.test.ts

@ -0,0 +1,374 @@
import { LIST_STATUS } from "@models/list";
import { streamListsDiff } from ".";
describe("streamListsDiff data", () => {
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 = streamListsDiff([], nextList, "id", { chunksSize: 2 });
const expectedChunks = [
{
previousValue: null,
currentValue: { id: 1, name: "Item 1" },
prevIndex: null,
newIndex: 0,
indexDiff: null,
status: LIST_STATUS.ADDED,
},
{
previousValue: null,
currentValue: { id: 2, name: "Item 2" },
prevIndex: null,
newIndex: 1,
indexDiff: null,
status: LIST_STATUS.ADDED,
},
];
diff.on("data", (chunk) => expect(chunk).toStrictEqual(expectedChunks));
diff.on("finish", () => 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 = streamListsDiff(prevList, [], "id", { chunksSize: 2 });
const expectedChunks = [
{
previousValue: { id: 1, name: "Item 1" },
currentValue: null,
prevIndex: 0,
newIndex: null,
indexDiff: null,
status: LIST_STATUS.DELETED,
},
{
previousValue: { id: 2, name: "Item 2" },
currentValue: null,
prevIndex: 1,
newIndex: null,
indexDiff: null,
status: LIST_STATUS.DELETED,
},
];
diff.on("data", (chunk) => expect(chunk).toStrictEqual(expectedChunks));
diff.on("finish", () => 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 = streamListsDiff(prevList, nextList, "id");
const expectedChunks = [
[
{
previousValue: null,
currentValue: { id: 3, name: "Item 3" },
prevIndex: null,
newIndex: 1,
indexDiff: null,
status: LIST_STATUS.ADDED,
},
],
[
{
previousValue: { id: 1, name: "Item 1" },
currentValue: null,
prevIndex: 0,
newIndex: null,
indexDiff: null,
status: LIST_STATUS.DELETED,
},
],
[
{
previousValue: { id: 2, name: "Item 2" },
currentValue: { id: 2, name: "Item 2" },
prevIndex: 1,
newIndex: 0,
indexDiff: -1,
status: LIST_STATUS.MOVED,
},
],
];
let chunkCount = 0;
diff.on("data", (chunk) => {
expect(chunk).toStrictEqual(expectedChunks[chunkCount]);
chunkCount++;
});
diff.on("finish", () => done());
});
it("emits 'data' event with 5 object diff by chunk", (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 = streamListsDiff(prevList, nextList, "id", { chunksSize: 5 });
const expectedChunks = [
[
{
previousValue: null,
currentValue: { id: 11, name: "Item 11" },
prevIndex: null,
newIndex: 7,
indexDiff: null,
status: LIST_STATUS.ADDED,
},
{
previousValue: { id: 1, name: "Item 1" },
currentValue: { id: 1, name: "Item 1" },
prevIndex: 0,
newIndex: 0,
indexDiff: 0,
status: LIST_STATUS.EQUAL,
},
{
previousValue: { id: 2, name: "Item 2" },
currentValue: { id: 2, name: "Item Two" },
prevIndex: 1,
newIndex: 1,
indexDiff: 0,
status: LIST_STATUS.UPDATED,
},
{
previousValue: { id: 3, name: "Item 3" },
currentValue: { id: 3, name: "Item 3" },
prevIndex: 2,
newIndex: 2,
indexDiff: 0,
status: LIST_STATUS.EQUAL,
},
{
previousValue: { id: 4, name: "Item 4" },
currentValue: null,
prevIndex: 3,
newIndex: null,
indexDiff: null,
status: LIST_STATUS.DELETED,
},
],
[
{
previousValue: { id: 5, name: "Item 5" },
currentValue: { id: 5, name: "Item 5" },
prevIndex: 4,
newIndex: 3,
indexDiff: -1,
status: LIST_STATUS.MOVED,
},
{
previousValue: { id: 6, name: "Item 6" },
currentValue: { id: 6, name: "Item Six" },
prevIndex: 5,
newIndex: 4,
indexDiff: -1,
status: LIST_STATUS.UPDATED,
},
{
previousValue: { id: 7, name: "Item 7" },
currentValue: { id: 7, name: "Item 7" },
prevIndex: 6,
newIndex: 5,
indexDiff: -1,
status: LIST_STATUS.MOVED,
},
{
previousValue: { id: 8, name: "Item 8" },
currentValue: { id: 8, name: "Item 8" },
prevIndex: 7,
newIndex: 9,
indexDiff: 2,
status: LIST_STATUS.MOVED,
},
{
previousValue: { id: 9, name: "Item 9" },
currentValue: { id: 9, name: "Item 9" },
prevIndex: 8,
newIndex: 8,
indexDiff: 0,
status: LIST_STATUS.EQUAL,
},
{
previousValue: { id: 10, name: "Item 10" },
currentValue: { id: 10, name: "Item 10" },
prevIndex: 9,
newIndex: 6,
indexDiff: -3,
status: LIST_STATUS.MOVED,
},
],
[
{
previousValue: { id: 10, name: "Item 10" },
currentValue: { id: 10, name: "Item 10" },
prevIndex: 9,
newIndex: 6,
indexDiff: -3,
status: LIST_STATUS.MOVED,
},
],
];
let chunkCount = 0;
diff.on("data", (chunk) => {
// console.log("chunks received", chunk);
// console.log("expected chunk", expectedChunks[chunkCount]);
//expect(chunk).toStrictEqual(expectedChunks[chunkCount]);
chunkCount++;
});
diff.on("finish", () => {
expect(chunkCount).toBe(3);
done();
});
});
});
describe("streamListsDiff finish", () => {
it("emits 'finish' event if no prevList nor nextList is provided", (done) => {
const diff = streamListsDiff([], [], "id");
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 = streamListsDiff(prevList, nextList, "id");
diff.on("finish", () => done());
});
});
describe("streamListsDiff error", () => {
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 = streamListsDiff(prevList, nextList, "id");
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 = streamListsDiff(prevList, nextList, "id");
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 = streamListsDiff(prevList, nextList, "id");
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 = streamListsDiff(prevList, nextList, "id");
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 = streamListsDiff(prevList, nextList, "id", { chunksSize: -3 });
diff.on("error", (err) => {
expect(err["message"]).toEqual(
"The chunk size can't be negative. You entered the value '-3'",
);
done();
});
});
});
Loading…
Cancel
Save