Browse Source
* chore: stream large lists diff * chore: remove useless dev dependencies + folder structure * chore: handle edgecases streamlistsdiff * chore: handle errors * chore: test stream-list-diff * chore: improve memory usage + add tests * chore: update readme * chore: update package.jsonpull/27/head
23 changed files with 2517 additions and 2193 deletions
@ -1,107 +0,0 @@
@@ -1,107 +0,0 @@
|
||||
declare const STATUS: Record<string, ObjectDiffStatus>; |
||||
declare const LIST_STATUS: Record<string, ListDiffStatus>; |
||||
declare const GRANULARITY: Record<string, "basic" | "deep">; |
||||
type ListDiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; |
||||
type ObjectDiffStatus = "added" | "equal" | "deleted" | "updated"; |
||||
type ObjectData = Record<string, any> | undefined | null; |
||||
type ListData = any; |
||||
type ObjectStatusTuple = readonly [ |
||||
"added", |
||||
"equal", |
||||
"deleted", |
||||
"updated" |
||||
]; |
||||
type ListStatusTuple = readonly [ |
||||
"added", |
||||
"equal", |
||||
"deleted", |
||||
"moved", |
||||
"updated" |
||||
]; |
||||
type isEqualOptions = { |
||||
ignoreArrayOrder?: boolean; |
||||
}; |
||||
type ObjectOptions = { |
||||
ignoreArrayOrder?: boolean; |
||||
showOnly?: { |
||||
statuses: Array<ObjectStatusTuple[number]>; |
||||
granularity?: (typeof GRANULARITY)[keyof typeof GRANULARITY]; |
||||
}; |
||||
}; |
||||
type ListOptions = { |
||||
showOnly?: Array<ListStatusTuple[number]>; |
||||
referenceProperty?: string; |
||||
considerMoveAsUpdate?: boolean; |
||||
ignoreArrayOrder?: boolean; |
||||
}; |
||||
type ListDiff = { |
||||
type: "list"; |
||||
status: ListDiffStatus; |
||||
diff: { |
||||
value: ListData; |
||||
prevIndex: number | null; |
||||
newIndex: number | null; |
||||
indexDiff: number | null; |
||||
status: ListDiffStatus; |
||||
}[]; |
||||
}; |
||||
type SubProperties = { |
||||
property: string; |
||||
previousValue: any; |
||||
currentValue: any; |
||||
status: ObjectDiffStatus; |
||||
subPropertiesDiff?: SubProperties[]; |
||||
}; |
||||
type ObjectDiff = { |
||||
type: "object"; |
||||
status: ObjectDiffStatus; |
||||
diff: { |
||||
property: string; |
||||
previousValue: any; |
||||
currentValue: any; |
||||
status: ObjectDiffStatus; |
||||
subPropertiesDiff?: SubProperties[]; |
||||
}[]; |
||||
}; |
||||
type DataDiff = ListDiff | ObjectDiff; |
||||
|
||||
/** |
||||
* Returns the diff between two objects |
||||
* @param {Record<string, any>} prevData - The original object. |
||||
* @param {Record<string, any>} nextData - The new object. |
||||
* * @param {ListOptions} options - Options to refine your output. |
||||
- `showOnly`: returns only the values whose status you are interested in. It takes two parameters: `statuses` and `granularity` |
||||
`statuses` are the status you want to see in the output (e.g. `["added", "equal"]`) |
||||
`granularity` can be either `basic` (to return only the main properties whose status matches your query) or `deep` (to return the main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly). |
||||
- `ignoreArrayOrder` if set to `true`, `["hello", "world"]` and `["world", "hello"]` will be treated as `equal`, because the two arrays have the same value, just not in the same order. |
||||
* @returns ObjectDiff |
||||
*/ |
||||
declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: ObjectOptions): ObjectDiff; |
||||
|
||||
/** |
||||
* Returns the diff between two arrays |
||||
* @param {Array<T>} prevList - The original array. |
||||
* @param {Array<T>} nextList - The new array. |
||||
* @param {ListOptions} options - Options to refine your output. |
||||
- `showOnly` gives you the option to return only the values whose status you are interested in (e.g. `["added", "equal"]`). |
||||
- `referenceProperty` will consider an object to be updated instead of added or deleted if one of its properties remains stable, such as its `id`. This option has no effect on other datatypes. |
||||
* @returns ListDiff |
||||
*/ |
||||
declare const getListDiff: <T>(prevList: T[] | undefined | null, nextList: T[] | undefined | null, options?: ListOptions) => ListDiff; |
||||
|
||||
/** |
||||
* Returns true if two data are equal |
||||
* @param {any} a - The original data. |
||||
* @param {any} b - The data to compare. |
||||
* @param {isEqualOptions} options - The options to compare the data. |
||||
* @returns boolean |
||||
*/ |
||||
declare function isEqual(a: any, b: any, options?: isEqualOptions): boolean; |
||||
/** |
||||
* Returns true if the provided value is an object |
||||
* @param {any} value - The data to check. |
||||
* @returns value is Record<string, any> |
||||
*/ |
||||
declare function isObject(value: any): value is Record<string, any>; |
||||
|
||||
export { type DataDiff, GRANULARITY, LIST_STATUS, type ListData, type ListDiff, type ListDiffStatus, type ListOptions, type ListStatusTuple, type ObjectData, type ObjectDiff, type ObjectDiffStatus, type ObjectOptions, type ObjectStatusTuple, STATUS, type SubProperties, getListDiff, getObjectDiff, isEqual, type isEqualOptions, isObject }; |
@ -1,107 +0,0 @@
@@ -1,107 +0,0 @@
|
||||
declare const STATUS: Record<string, ObjectDiffStatus>; |
||||
declare const LIST_STATUS: Record<string, ListDiffStatus>; |
||||
declare const GRANULARITY: Record<string, "basic" | "deep">; |
||||
type ListDiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; |
||||
type ObjectDiffStatus = "added" | "equal" | "deleted" | "updated"; |
||||
type ObjectData = Record<string, any> | undefined | null; |
||||
type ListData = any; |
||||
type ObjectStatusTuple = readonly [ |
||||
"added", |
||||
"equal", |
||||
"deleted", |
||||
"updated" |
||||
]; |
||||
type ListStatusTuple = readonly [ |
||||
"added", |
||||
"equal", |
||||
"deleted", |
||||
"moved", |
||||
"updated" |
||||
]; |
||||
type isEqualOptions = { |
||||
ignoreArrayOrder?: boolean; |
||||
}; |
||||
type ObjectOptions = { |
||||
ignoreArrayOrder?: boolean; |
||||
showOnly?: { |
||||
statuses: Array<ObjectStatusTuple[number]>; |
||||
granularity?: (typeof GRANULARITY)[keyof typeof GRANULARITY]; |
||||
}; |
||||
}; |
||||
type ListOptions = { |
||||
showOnly?: Array<ListStatusTuple[number]>; |
||||
referenceProperty?: string; |
||||
considerMoveAsUpdate?: boolean; |
||||
ignoreArrayOrder?: boolean; |
||||
}; |
||||
type ListDiff = { |
||||
type: "list"; |
||||
status: ListDiffStatus; |
||||
diff: { |
||||
value: ListData; |
||||
prevIndex: number | null; |
||||
newIndex: number | null; |
||||
indexDiff: number | null; |
||||
status: ListDiffStatus; |
||||
}[]; |
||||
}; |
||||
type SubProperties = { |
||||
property: string; |
||||
previousValue: any; |
||||
currentValue: any; |
||||
status: ObjectDiffStatus; |
||||
subPropertiesDiff?: SubProperties[]; |
||||
}; |
||||
type ObjectDiff = { |
||||
type: "object"; |
||||
status: ObjectDiffStatus; |
||||
diff: { |
||||
property: string; |
||||
previousValue: any; |
||||
currentValue: any; |
||||
status: ObjectDiffStatus; |
||||
subPropertiesDiff?: SubProperties[]; |
||||
}[]; |
||||
}; |
||||
type DataDiff = ListDiff | ObjectDiff; |
||||
|
||||
/** |
||||
* Returns the diff between two objects |
||||
* @param {Record<string, any>} prevData - The original object. |
||||
* @param {Record<string, any>} nextData - The new object. |
||||
* * @param {ListOptions} options - Options to refine your output. |
||||
- `showOnly`: returns only the values whose status you are interested in. It takes two parameters: `statuses` and `granularity` |
||||
`statuses` are the status you want to see in the output (e.g. `["added", "equal"]`) |
||||
`granularity` can be either `basic` (to return only the main properties whose status matches your query) or `deep` (to return the main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly). |
||||
- `ignoreArrayOrder` if set to `true`, `["hello", "world"]` and `["world", "hello"]` will be treated as `equal`, because the two arrays have the same value, just not in the same order. |
||||
* @returns ObjectDiff |
||||
*/ |
||||
declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: ObjectOptions): ObjectDiff; |
||||
|
||||
/** |
||||
* Returns the diff between two arrays |
||||
* @param {Array<T>} prevList - The original array. |
||||
* @param {Array<T>} nextList - The new array. |
||||
* @param {ListOptions} options - Options to refine your output. |
||||
- `showOnly` gives you the option to return only the values whose status you are interested in (e.g. `["added", "equal"]`). |
||||
- `referenceProperty` will consider an object to be updated instead of added or deleted if one of its properties remains stable, such as its `id`. This option has no effect on other datatypes. |
||||
* @returns ListDiff |
||||
*/ |
||||
declare const getListDiff: <T>(prevList: T[] | undefined | null, nextList: T[] | undefined | null, options?: ListOptions) => ListDiff; |
||||
|
||||
/** |
||||
* Returns true if two data are equal |
||||
* @param {any} a - The original data. |
||||
* @param {any} b - The data to compare. |
||||
* @param {isEqualOptions} options - The options to compare the data. |
||||
* @returns boolean |
||||
*/ |
||||
declare function isEqual(a: any, b: any, options?: isEqualOptions): boolean; |
||||
/** |
||||
* Returns true if the provided value is an object |
||||
* @param {any} value - The data to check. |
||||
* @returns value is Record<string, any> |
||||
*/ |
||||
declare function isObject(value: any): value is Record<string, any>; |
||||
|
||||
export { type DataDiff, GRANULARITY, LIST_STATUS, type ListData, type ListDiff, type ListDiffStatus, type ListOptions, type ListStatusTuple, type ObjectData, type ObjectDiff, type ObjectDiffStatus, type ObjectOptions, type ObjectStatusTuple, STATUS, type SubProperties, getListDiff, getObjectDiff, isEqual, type isEqualOptions, isObject }; |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
'use strict'; |
||||
|
||||
var u={ADDED:"added",EQUAL:"equal",DELETED:"deleted",UPDATED:"updated"},c={...u,MOVED:"moved"},y={BASIC:"basic",DEEP:"deep"};function D(e,t,i={ignoreArrayOrder:!1}){return typeof e!=typeof t?!1:Array.isArray(e)&&Array.isArray(t)?e.length!==t.length?!1:i.ignoreArrayOrder?e.every(f=>t.some(n=>JSON.stringify(n)===JSON.stringify(f))):e.every((f,n)=>JSON.stringify(f)===JSON.stringify(t[n])):typeof e=="object"?JSON.stringify(e)===JSON.stringify(t):e===t}function p(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function O(e,t={statuses:[],granularity:y.BASIC}){let{statuses:i,granularity:f}=t;return e.reduce((n,r)=>{if(f===y.DEEP&&r.subPropertiesDiff){let s=O(r.subPropertiesDiff,t);if(s.length>0)return [...n,{...r,subPropertiesDiff:s}]}if(f===y.DEEP&&r.subDiff){let s=O(r.subDiff,t);if(s.length>0)return [...n,{...r,subDiff:s}]}return i.includes(r.status)?[...n,r]:n},[])}function E(e){return e.some(t=>t.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function A(e,t,i={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:y.BASIC}}){if(!e)return {type:"object",status:u.EQUAL,diff:[]};let f=[];return Object.entries(e).forEach(([n,r])=>{if(p(r)){let s=[];return Object.entries(r).forEach(([o,d])=>{s.push({property:o,previousValue:t===u.ADDED?void 0:d,currentValue:t===u.ADDED?d:void 0,status:t});}),f.push({property:n,previousValue:t===u.ADDED?void 0:e[n],currentValue:t===u.ADDED?r:void 0,status:t,subPropertiesDiff:s})}return f.push({property:n,previousValue:t===u.ADDED?void 0:e[n],currentValue:t===u.ADDED?r:void 0,status:t})}),i.showOnly&&i.showOnly.statuses.length>0?{type:"object",status:t,diff:O(f,i.showOnly)}:{type:"object",status:t,diff:f}}function P(e,t,i){if(!e)return;let f=Object.entries(e).find(([n])=>D(n,t,i));return f?f[1]:void 0}function j(e,t,i){return D(e,t,i)?u.EQUAL:u.UPDATED}function U(e){return e.some(t=>t.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function g(e,t){if(!e)return;let i=Object.keys(e),f=Object.keys(t),n=i.filter(r=>!f.includes(r));if(n.length>0)return n.map(r=>({property:r,value:e[r]}))}function S(e,t,i){let f=[],n,r=g(e,t);return r&&r.forEach(s=>{f.push({property:s.property,previousValue:s.value,currentValue:void 0,status:u.DELETED});}),Object.entries(t).forEach(([s,o])=>{let d=P(e,s,i);if(!d)return f.push({property:s,previousValue:d,currentValue:o,status:!e||!(s in e)?u.ADDED:d===o?u.EQUAL:u.UPDATED});if(p(o)){let a=S(d,o,i);a&&a.length>0&&(n=a);}d&&f.push({property:s,previousValue:d,currentValue:o,status:j(d,o,i),...!!n&&{subDiff:n}});}),f}function m(e,t,i={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:y.BASIC}}){if(!e&&!t)return {type:"object",status:u.EQUAL,diff:[]};if(!e)return A(t,u.ADDED,i);if(!t)return A(e,u.DELETED,i);let f=[];Object.entries(t).forEach(([r,s])=>{let o=e[r];if(!o)return f.push({property:r,previousValue:o,currentValue:s,status:r in e?o===s?u.EQUAL:u.UPDATED:u.ADDED});if(p(s)){let d=S(o,s,i),a=U(d);return f.push({property:r,previousValue:o,currentValue:s,status:a,...a!==u.EQUAL&&{subPropertiesDiff:d}})}return f.push({property:r,previousValue:o,currentValue:s,status:j(o,s,i)})});let n=g(e,t);return n&&n.forEach(r=>{f.push({property:r.property,previousValue:r.value,currentValue:void 0,status:u.DELETED});}),i.showOnly&&i.showOnly.statuses.length>0?{type:"object",status:E(f),diff:O(f,i.showOnly)}:{type:"object",status:E(f),diff:f}}function w(e,t=[]){return e.filter(i=>t?.includes(i.status))}function h(e,t,i={showOnly:[]}){let f=e.map((n,r)=>({value:n,prevIndex:t===c.ADDED?null:r,newIndex:t===c.ADDED?r:null,indexDiff:null,status:t}));return i.showOnly&&i.showOnly.length>0?{type:"list",status:t,diff:f.filter(n=>i.showOnly?.includes(n.status))}:{type:"list",status:t,diff:f}}function L(e){return e.some(t=>t.status!==c.EQUAL)?c.UPDATED:c.EQUAL}function T(e,t){return p(e)&&t?Object.hasOwn(e,t):!1}var I=(e,t,i={showOnly:[],referenceProperty:void 0,considerMoveAsUpdate:!1,ignoreArrayOrder:!1})=>{if(!e&&!t)return {type:"list",status:c.EQUAL,diff:[]};if(!e)return h(t,c.ADDED,i);if(!t)return h(e,c.DELETED,i);let f=[],n=[];return t.forEach((r,s)=>{let o=e.findIndex((a,b)=>T(a,i.referenceProperty)?p(r)?D(a[i.referenceProperty],r[i.referenceProperty])&&!n.includes(b):!1:D(a,r)&&!n.includes(b));o>-1&&n.push(o);let d=o===-1?null:s-o;if(d===0||i.ignoreArrayOrder){let a=c.EQUAL;return T(r,i.referenceProperty)&&(D(e[o],r)||(a=c.UPDATED)),f.push({value:r,prevIndex:o,newIndex:s,indexDiff:d,status:a})}return o===-1?f.push({value:r,prevIndex:null,newIndex:s,indexDiff:d,status:c.ADDED}):f.push({value:r,prevIndex:o,newIndex:s,indexDiff:d,status:i.considerMoveAsUpdate?c.UPDATED:c.MOVED})}),e.forEach((r,s)=>{if(!n.includes(s))return f.splice(s,0,{value:r,prevIndex:s,newIndex:null,indexDiff:null,status:c.DELETED})}),i.showOnly&&i?.showOnly?.length>0?{type:"list",status:L(f),diff:w(f,i.showOnly)}:{type:"list",status:L(f),diff:f}}; |
||||
|
||||
exports.GRANULARITY = y; |
||||
exports.LIST_STATUS = c; |
||||
exports.STATUS = u; |
||||
exports.getListDiff = I; |
||||
exports.getObjectDiff = m; |
||||
exports.isEqual = D; |
||||
exports.isObject = p; |
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
var u={ADDED:"added",EQUAL:"equal",DELETED:"deleted",UPDATED:"updated"},c={...u,MOVED:"moved"},y={BASIC:"basic",DEEP:"deep"};function D(e,t,i={ignoreArrayOrder:!1}){return typeof e!=typeof t?!1:Array.isArray(e)&&Array.isArray(t)?e.length!==t.length?!1:i.ignoreArrayOrder?e.every(f=>t.some(n=>JSON.stringify(n)===JSON.stringify(f))):e.every((f,n)=>JSON.stringify(f)===JSON.stringify(t[n])):typeof e=="object"?JSON.stringify(e)===JSON.stringify(t):e===t}function p(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function b(e,t={statuses:[],granularity:y.BASIC}){let{statuses:i,granularity:f}=t;return e.reduce((n,r)=>{if(f===y.DEEP&&r.subPropertiesDiff){let s=b(r.subPropertiesDiff,t);if(s.length>0)return [...n,{...r,subPropertiesDiff:s}]}if(f===y.DEEP&&r.subDiff){let s=b(r.subDiff,t);if(s.length>0)return [...n,{...r,subDiff:s}]}return i.includes(r.status)?[...n,r]:n},[])}function A(e){return e.some(t=>t.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function j(e,t,i={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:y.BASIC}}){if(!e)return {type:"object",status:u.EQUAL,diff:[]};let f=[];return Object.entries(e).forEach(([n,r])=>{if(p(r)){let s=[];return Object.entries(r).forEach(([o,d])=>{s.push({property:o,previousValue:t===u.ADDED?void 0:d,currentValue:t===u.ADDED?d:void 0,status:t});}),f.push({property:n,previousValue:t===u.ADDED?void 0:e[n],currentValue:t===u.ADDED?r:void 0,status:t,subPropertiesDiff:s})}return f.push({property:n,previousValue:t===u.ADDED?void 0:e[n],currentValue:t===u.ADDED?r:void 0,status:t})}),i.showOnly&&i.showOnly.statuses.length>0?{type:"object",status:t,diff:b(f,i.showOnly)}:{type:"object",status:t,diff:f}}function U(e,t,i){if(!e)return;let f=Object.entries(e).find(([n])=>D(n,t,i));return f?f[1]:void 0}function g(e,t,i){return D(e,t,i)?u.EQUAL:u.UPDATED}function m(e){return e.some(t=>t.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function S(e,t){if(!e)return;let i=Object.keys(e),f=Object.keys(t),n=i.filter(r=>!f.includes(r));if(n.length>0)return n.map(r=>({property:r,value:e[r]}))}function h(e,t,i){let f=[],n,r=S(e,t);return r&&r.forEach(s=>{f.push({property:s.property,previousValue:s.value,currentValue:void 0,status:u.DELETED});}),Object.entries(t).forEach(([s,o])=>{let d=U(e,s,i);if(!d)return f.push({property:s,previousValue:d,currentValue:o,status:!e||!(s in e)?u.ADDED:d===o?u.EQUAL:u.UPDATED});if(p(o)){let a=h(d,o,i);a&&a.length>0&&(n=a);}d&&f.push({property:s,previousValue:d,currentValue:o,status:g(d,o,i),...!!n&&{subDiff:n}});}),f}function w(e,t,i={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:y.BASIC}}){if(!e&&!t)return {type:"object",status:u.EQUAL,diff:[]};if(!e)return j(t,u.ADDED,i);if(!t)return j(e,u.DELETED,i);let f=[];Object.entries(t).forEach(([r,s])=>{let o=e[r];if(!o)return f.push({property:r,previousValue:o,currentValue:s,status:r in e?o===s?u.EQUAL:u.UPDATED:u.ADDED});if(p(s)){let d=h(o,s,i),a=m(d);return f.push({property:r,previousValue:o,currentValue:s,status:a,...a!==u.EQUAL&&{subPropertiesDiff:d}})}return f.push({property:r,previousValue:o,currentValue:s,status:g(o,s,i)})});let n=S(e,t);return n&&n.forEach(r=>{f.push({property:r.property,previousValue:r.value,currentValue:void 0,status:u.DELETED});}),i.showOnly&&i.showOnly.statuses.length>0?{type:"object",status:A(f),diff:b(f,i.showOnly)}:{type:"object",status:A(f),diff:f}}function I(e,t=[]){return e.filter(i=>t?.includes(i.status))}function L(e,t,i={showOnly:[]}){let f=e.map((n,r)=>({value:n,prevIndex:t===c.ADDED?null:r,newIndex:t===c.ADDED?r:null,indexDiff:null,status:t}));return i.showOnly&&i.showOnly.length>0?{type:"list",status:t,diff:f.filter(n=>i.showOnly?.includes(n.status))}:{type:"list",status:t,diff:f}}function T(e){return e.some(t=>t.status!==c.EQUAL)?c.UPDATED:c.EQUAL}function P(e,t){return p(e)&&t?Object.hasOwn(e,t):!1}var R=(e,t,i={showOnly:[],referenceProperty:void 0,considerMoveAsUpdate:!1,ignoreArrayOrder:!1})=>{if(!e&&!t)return {type:"list",status:c.EQUAL,diff:[]};if(!e)return L(t,c.ADDED,i);if(!t)return L(e,c.DELETED,i);let f=[],n=[];return t.forEach((r,s)=>{let o=e.findIndex((a,E)=>P(a,i.referenceProperty)?p(r)?D(a[i.referenceProperty],r[i.referenceProperty])&&!n.includes(E):!1:D(a,r)&&!n.includes(E));o>-1&&n.push(o);let d=o===-1?null:s-o;if(d===0||i.ignoreArrayOrder){let a=c.EQUAL;return P(r,i.referenceProperty)&&(D(e[o],r)||(a=c.UPDATED)),f.push({value:r,prevIndex:o,newIndex:s,indexDiff:d,status:a})}return o===-1?f.push({value:r,prevIndex:null,newIndex:s,indexDiff:d,status:c.ADDED}):f.push({value:r,prevIndex:o,newIndex:s,indexDiff:d,status:i.considerMoveAsUpdate?c.UPDATED:c.MOVED})}),e.forEach((r,s)=>{if(!n.includes(s))return f.splice(s,0,{value:r,prevIndex:s,newIndex:null,indexDiff:null,status:c.DELETED})}),i.showOnly&&i?.showOnly?.length>0?{type:"list",status:T(f),diff:I(f,i.showOnly)}:{type:"list",status:T(f),diff:f}}; |
||||
|
||||
export { y as GRANULARITY, c as LIST_STATUS, u as STATUS, R as getListDiff, w as getObjectDiff, D as isEqual, p as isObject }; |
@ -1,5 +1,23 @@
@@ -1,5 +1,23 @@
|
||||
module.exports = { |
||||
transform: { |
||||
"^.+\\.(ts|js)$": "ts-jest", |
||||
"^.+\\.(ts|js)$": [ |
||||
"@swc/jest", |
||||
{ |
||||
jsc: { |
||||
baseUrl: ".", |
||||
parser: { |
||||
syntax: "typescript", |
||||
tsx: true, |
||||
dynamicImport: true, |
||||
}, |
||||
paths: { |
||||
"@models/*": ["./src/models/*"], |
||||
"@lib/*": ["./src/lib/*"], |
||||
|
||||
}, |
||||
target: "esnext", |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
@ -1,5 +1,7 @@
@@ -1,5 +1,7 @@
|
||||
export { getObjectDiff } from "./object-diff"; |
||||
export { getListDiff } from "./list-diff"; |
||||
export { isEqual, isObject } from "./utils"; |
||||
export { getObjectDiff } from "./lib/object-diff"; |
||||
export { getListDiff } from "./lib/list-diff"; |
||||
export { isEqual, isObject } from "./lib/utils"; |
||||
export { streamListDiff } from "./lib/stream-list-diff"; |
||||
export * from "./models/list"; |
||||
export * from "./models/object"; |
||||
export * from "./models/stream"; |
||||
|
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
import { getListDiff } from "../src/list-diff"; |
||||
import { LIST_STATUS } from "../src/models/list"; |
||||
import { getListDiff } from "."; |
||||
import { LIST_STATUS } from "@models/list"; |
||||
|
||||
describe("getListDiff", () => { |
||||
it("returns an empty diff if no lists are provided", () => { |
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
import { GRANULARITY, OBJECT_STATUS } from "../src/models/object"; |
||||
import { getObjectDiff } from "../src/object-diff"; |
||||
import { GRANULARITY, OBJECT_STATUS } from "@models/object"; |
||||
import { getObjectDiff } from "."; |
||||
|
||||
describe("getObjectDiff", () => { |
||||
it("returns an empty diff if no objects are provided", () => { |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
import { StreamListDiff } from "@models/stream"; |
||||
|
||||
type Listener<T extends unknown[]> = (...args: T) => void; |
||||
|
||||
export enum StreamEvent { |
||||
Data = "data", |
||||
Finish = "finish", |
||||
Error = "error", |
||||
} |
||||
|
||||
export type Emitter<T extends Record<string, unknown>> = EventEmitter<{ |
||||
data: [StreamListDiff<T>[]]; |
||||
error: [Error]; |
||||
finish: []; |
||||
}>; |
||||
|
||||
export class EventEmitter<Events extends Record<string, unknown[]>> { |
||||
private events: Record<string, Listener<unknown[]>[]> = {}; |
||||
|
||||
on<E extends keyof Events>(event: E, listener: Listener<Events[E]>): this { |
||||
if (!this.events[event as string]) { |
||||
this.events[event as string] = []; |
||||
} |
||||
this.events[event as string].push(listener as Listener<unknown[]>); |
||||
return this; |
||||
} |
||||
|
||||
emit<E extends keyof Events>(event: E, ...args: Events[E]): void { |
||||
if (this.events[event as string]) { |
||||
this.events[event as string].forEach((listener) => listener(...args)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export type EmitterEvents<T extends Record<string, unknown>> = { |
||||
data: [StreamListDiff<T>[]]; |
||||
error: [Error]; |
||||
finish: []; |
||||
}; |
||||
|
||||
export interface StreamListener<T extends Record<string, unknown>> { |
||||
on<E extends keyof EmitterEvents<T>>( |
||||
event: E, |
||||
listener: Listener<EmitterEvents<T>[E]>, |
||||
): this; |
||||
} |
@ -0,0 +1,327 @@
@@ -0,0 +1,327 @@
|
||||
import { |
||||
DEFAULT_LIST_STREAM_OPTIONS, |
||||
ListStreamOptions, |
||||
ReferenceProperty, |
||||
StreamListDiff, |
||||
StreamReferences, |
||||
} from "@models/stream"; |
||||
import { LIST_STATUS } from "@models/list"; |
||||
import { isObject } from "@lib/utils"; |
||||
import { |
||||
Emitter, |
||||
EmitterEvents, |
||||
EventEmitter, |
||||
StreamListener, |
||||
StreamEvent, |
||||
} from "./emitter"; |
||||
|
||||
function outputDiffChunk<T extends Record<string, unknown>>( |
||||
emitter: Emitter<T>, |
||||
) { |
||||
let chunks: StreamListDiff<T>[] = []; |
||||
|
||||
return function handleDiffChunk( |
||||
chunk: StreamListDiff<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); |
||||
} else { |
||||
return; |
||||
} |
||||
} |
||||
return emitter.emit(StreamEvent.Data, [chunk]); |
||||
}; |
||||
} |
||||
|
||||
function formatSingleListStreamDiff<T extends Record<string, unknown>>( |
||||
list: T[], |
||||
isPrevious: boolean, |
||||
status: LIST_STATUS, |
||||
options: ListStreamOptions, |
||||
): StreamListDiff<T>[] | null { |
||||
let isValid = true; |
||||
const diff: StreamListDiff<T>[] = []; |
||||
for (let i = 0; i < list.length; i++) { |
||||
const data = list[i]; |
||||
if (!isObject(data)) { |
||||
isValid = false; |
||||
break; |
||||
} |
||||
diff.push({ |
||||
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 (!isValid) { |
||||
return null; |
||||
} |
||||
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 sign = String(Math.sign(chunksSize)); |
||||
return sign !== "-1" && sign !== "NaN"; |
||||
} |
||||
|
||||
function isDataValid<T extends Record<string, unknown>>( |
||||
data: T, |
||||
referenceProperty: ReferenceProperty<T>, |
||||
listType: "prevList" | "nextList", |
||||
): { isValid: boolean; message?: string } { |
||||
if (!isObject(data)) { |
||||
return { |
||||
isValid: false, |
||||
message: `Your ${listType} must only contain valid objects. Found '${data}'`, |
||||
}; |
||||
} |
||||
if (!Object.hasOwn(data, referenceProperty)) { |
||||
return { |
||||
isValid: false, |
||||
message: `The reference property '${String(referenceProperty)}' is not available in all the objects of your ${listType}.`, |
||||
}; |
||||
} |
||||
return { |
||||
isValid: true, |
||||
message: "", |
||||
}; |
||||
} |
||||
|
||||
function getDiffChunks<T extends Record<string, unknown>>( |
||||
prevList: T[] = [], |
||||
nextList: T[] = [], |
||||
referenceProperty: ReferenceProperty<T>, |
||||
emitter: Emitter<T>, |
||||
options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, |
||||
): void { |
||||
if (!isValidChunkSize(options?.chunksSize)) { |
||||
return emitter.emit( |
||||
StreamEvent.Error, |
||||
new Error( |
||||
`The chunk size can't be negative. You entered the value '${options.chunksSize}'`, |
||||
), |
||||
); |
||||
} |
||||
if (prevList.length === 0 && nextList.length === 0) { |
||||
return emitter.emit(StreamEvent.Finish); |
||||
} |
||||
const handleDiffChunk = outputDiffChunk<T>(emitter); |
||||
if (prevList.length === 0) { |
||||
const nextDiff = formatSingleListStreamDiff( |
||||
nextList as T[], |
||||
false, |
||||
LIST_STATUS.ADDED, |
||||
options, |
||||
); |
||||
if (!nextDiff) { |
||||
emitter.emit( |
||||
StreamEvent.Error, |
||||
new Error("Your nextList must only contain valid objects."), |
||||
); |
||||
return emitter.emit(StreamEvent.Finish); |
||||
} |
||||
nextDiff?.forEach((data, i) => |
||||
handleDiffChunk(data, i === nextDiff.length - 1, options), |
||||
); |
||||
return emitter.emit(StreamEvent.Finish); |
||||
} |
||||
if (nextList.length === 0) { |
||||
const prevDiff = formatSingleListStreamDiff( |
||||
prevList as T[], |
||||
true, |
||||
LIST_STATUS.DELETED, |
||||
options, |
||||
); |
||||
if (!prevDiff) { |
||||
emitter.emit( |
||||
StreamEvent.Error, |
||||
new Error("Your prevList must only contain valid objects."), |
||||
); |
||||
return emitter.emit(StreamEvent.Finish); |
||||
} |
||||
prevDiff?.forEach((data, i) => |
||||
handleDiffChunk(data, i === prevDiff.length - 1, options), |
||||
); |
||||
return emitter.emit(StreamEvent.Finish); |
||||
} |
||||
const listsReferences: StreamReferences<T> = new Map(); |
||||
for (let i = 0; i < prevList.length; i++) { |
||||
const data = prevList[i]; |
||||
if (data) { |
||||
const { isValid, message } = isDataValid( |
||||
data, |
||||
referenceProperty, |
||||
"prevList", |
||||
); |
||||
if (!isValid) { |
||||
emitter.emit(StreamEvent.Error, new Error(message)); |
||||
emitter.emit(StreamEvent.Finish); |
||||
break; |
||||
} |
||||
listsReferences.set(String(data[referenceProperty]), { |
||||
prevIndex: i, |
||||
nextIndex: undefined, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
const totalChunks = listsReferences.size; |
||||
|
||||
for (let i = 0; i < nextList.length; i++) { |
||||
const data = nextList[i]; |
||||
if (data) { |
||||
const { isValid, message } = isDataValid( |
||||
data, |
||||
referenceProperty, |
||||
"nextList", |
||||
); |
||||
if (!isValid) { |
||||
emitter.emit(StreamEvent.Error, new Error(message)); |
||||
emitter.emit(StreamEvent.Finish); |
||||
break; |
||||
} |
||||
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, |
||||
}, |
||||
totalChunks > 0 ? false : i === nextList.length - 1, |
||||
options, |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
let streamedChunks = 0; |
||||
|
||||
for (const [key, data] of listsReferences.entries()) { |
||||
streamedChunks++; |
||||
const isLastChunk = totalChunks === streamedChunks; |
||||
|
||||
if (typeof data.nextIndex === "undefined") { |
||||
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 = JSON.stringify(prevData) === JSON.stringify(nextData); |
||||
const indexDiff = data.nextIndex - data.prevIndex; |
||||
if (isDataEqual) { |
||||
if (indexDiff === 0) { |
||||
handleDiffChunk( |
||||
{ |
||||
previousValue: prevList[data.prevIndex], |
||||
currentValue: nextList[data.nextIndex], |
||||
prevIndex: data.prevIndex, |
||||
newIndex: data.nextIndex, |
||||
indexDiff: 0, |
||||
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, |
||||
); |
||||
} |
||||
} |
||||
listsReferences.delete(key); |
||||
} |
||||
|
||||
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 streamed chunk. (e.g. `0` = 1 object diff by chunk, `10` = 10 object diffs by chunk). |
||||
- `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 streamListDiff<T extends Record<string, unknown>>( |
||||
prevList: T[], |
||||
nextList: T[], |
||||
referenceProperty: ReferenceProperty<T>, |
||||
options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, |
||||
): StreamListener<T> { |
||||
const emitter = new EventEmitter<EmitterEvents<T>>(); |
||||
setTimeout(() => { |
||||
try { |
||||
getDiffChunks(prevList, nextList, referenceProperty, emitter, options); |
||||
} catch (err) { |
||||
return emitter.emit(StreamEvent.Error, err as Error); |
||||
} |
||||
}, 0); |
||||
return emitter as StreamListener<T>; |
||||
} |
@ -0,0 +1,867 @@
@@ -0,0 +1,867 @@
|
||||
import { LIST_STATUS } from "@models/list"; |
||||
import { streamListDiff } from "."; |
||||
import { StreamListDiff } from "@models/stream"; |
||||
|
||||
describe("streamListDiff 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 = streamListDiff([], 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, |
||||
}, |
||||
]; |
||||
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 }); |
||||
|
||||
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, |
||||
}, |
||||
]; |
||||
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 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"); |
||||
|
||||
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", () => { |
||||
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 }); |
||||
|
||||
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, |
||||
}, |
||||
], |
||||
]; |
||||
|
||||
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 }); |
||||
|
||||
const expectedChunks = [ |
||||
{ |
||||
previousValue: null, |
||||
currentValue: { id: 5, name: "Item 5" }, |
||||
prevIndex: null, |
||||
newIndex: 3, |
||||
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, |
||||
}, |
||||
]; |
||||
|
||||
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, |
||||
}); |
||||
|
||||
const expectedChunks = [ |
||||
{ |
||||
previousValue: null, |
||||
currentValue: { id: 5, name: "Item 5" }, |
||||
prevIndex: null, |
||||
newIndex: 3, |
||||
indexDiff: null, |
||||
status: LIST_STATUS.ADDED, |
||||
}, |
||||
{ |
||||
previousValue: { id: 1, name: "Item 1" }, |
||||
currentValue: { id: 1, name: "Item 1" }, |
||||
prevIndex: 0, |
||||
newIndex: 1, |
||||
indexDiff: 1, |
||||
status: LIST_STATUS.UPDATED, |
||||
}, |
||||
{ |
||||
previousValue: { id: 2, name: "Item 2" }, |
||||
currentValue: { id: 2, name: "Item Two" }, |
||||
prevIndex: 1, |
||||
newIndex: 0, |
||||
indexDiff: -1, |
||||
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, |
||||
}, |
||||
]; |
||||
|
||||
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"], |
||||
}); |
||||
|
||||
const expectedChunks = [ |
||||
{ |
||||
previousValue: null, |
||||
currentValue: { id: 5, name: "Item 5" }, |
||||
prevIndex: null, |
||||
newIndex: 3, |
||||
indexDiff: null, |
||||
status: LIST_STATUS.ADDED, |
||||
}, |
||||
{ |
||||
previousValue: { id: 4, name: "Item 4" }, |
||||
currentValue: null, |
||||
prevIndex: 3, |
||||
newIndex: null, |
||||
indexDiff: null, |
||||
status: LIST_STATUS.DELETED, |
||||
}, |
||||
]; |
||||
|
||||
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 }); |
||||
|
||||
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", |
||||
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: 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", |
||||
user: { role: "admin", hobbies: ["rugby"] }, |
||||
}, |
||||
currentValue: { |
||||
id: 3, |
||||
name: "Item 3", |
||||
user: { role: "admin", hobbies: ["rugby"] }, |
||||
}, |
||||
prevIndex: 2, |
||||
newIndex: 2, |
||||
indexDiff: 0, |
||||
status: LIST_STATUS.EQUAL, |
||||
}, |
||||
{ |
||||
previousValue: { |
||||
id: 4, |
||||
name: "Item 4", |
||||
user: { role: "reader", hobbies: ["video games", "fishing"] }, |
||||
}, |
||||
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", |
||||
user: { role: "root", hobbies: ["coding"] }, |
||||
}, |
||||
currentValue: { |
||||
id: 6, |
||||
name: "Item 6", |
||||
user: { role: "root", hobbies: ["farming"] }, |
||||
}, |
||||
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", |
||||
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: LIST_STATUS.MOVED, |
||||
}, |
||||
], |
||||
]; |
||||
|
||||
let chunkCount = 0; |
||||
|
||||
diff.on("data", (chunk) => { |
||||
expect(chunk).toStrictEqual(expectedChunks[chunkCount]); |
||||
chunkCount++; |
||||
}); |
||||
|
||||
diff.on("finish", () => { |
||||
expect(chunkCount).toBe(3); |
||||
done(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe("streamListDiff finish", () => { |
||||
it("emits 'finish' event if no prevList nor nextList is provided", (done) => { |
||||
const diff = streamListDiff([], [], "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 = streamListDiff(prevList, nextList, "id"); |
||||
diff.on("finish", () => done()); |
||||
}); |
||||
}); |
||||
|
||||
describe("streamListDiff 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 = streamListDiff(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 = streamListDiff(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 = streamListDiff(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 = streamListDiff(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 = streamListDiff(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(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe("Performance", () => { |
||||
it("should correctly stream diff for 10.000 entries", (done) => { |
||||
const generateLargeList = (size: number, idPrefix: string) => { |
||||
return Array.from({ length: size }, (_, i) => ({ |
||||
id: `${idPrefix}-${i}`, |
||||
value: i, |
||||
})); |
||||
}; |
||||
const prevList = generateLargeList(10_000, "prev"); |
||||
const nextList = [ |
||||
...generateLargeList(5000, "prev"), |
||||
...generateLargeList(5000, "next"), |
||||
]; |
||||
|
||||
const receivedChunks: StreamListDiff<{ id: string; value: number }>[] = []; |
||||
let chunkCount = 0; |
||||
const diffStream = streamListDiff(prevList, nextList, "id", { |
||||
chunksSize: 1000, |
||||
}); |
||||
|
||||
diffStream.on("data", (chunk) => { |
||||
receivedChunks.push(...chunk); |
||||
chunkCount++; |
||||
}); |
||||
|
||||
diffStream.on("finish", () => { |
||||
const deletions = receivedChunks.filter( |
||||
(diff) => diff.status === LIST_STATUS.DELETED, |
||||
); |
||||
const additions = receivedChunks.filter( |
||||
(diff) => diff.status === LIST_STATUS.ADDED, |
||||
); |
||||
const updates = receivedChunks.filter( |
||||
(diff) => diff.status === LIST_STATUS.EQUAL, |
||||
); |
||||
expect(receivedChunks.length).toBe(15_000); // 5000 deletions + 5000 equal + 5000 additions
|
||||
expect(chunkCount).toBe(15); |
||||
expect(deletions.length).toBe(5000); |
||||
expect(additions.length).toBe(5000); |
||||
expect(updates.length).toBe(5000); |
||||
done(); |
||||
}); |
||||
}); |
||||
it("should correctly stream diff for 100.000 entries", (done) => { |
||||
const generateLargeList = (size: number, idPrefix: string) => { |
||||
return Array.from({ length: size }, (_, i) => ({ |
||||
id: `${idPrefix}-${i}`, |
||||
value: i, |
||||
})); |
||||
}; |
||||
const prevList = generateLargeList(100_000, "prev"); |
||||
const nextList = [ |
||||
...generateLargeList(50000, "prev"), |
||||
...generateLargeList(50000, "next"), |
||||
]; |
||||
|
||||
const receivedChunks: StreamListDiff<{ id: string; value: number }>[] = []; |
||||
let chunkCount = 0; |
||||
const diffStream = streamListDiff(prevList, nextList, "id", { |
||||
chunksSize: 10_000, |
||||
}); |
||||
|
||||
diffStream.on("data", (chunk) => { |
||||
receivedChunks.push(...chunk); |
||||
chunkCount++; |
||||
}); |
||||
|
||||
diffStream.on("finish", () => { |
||||
const deletions = receivedChunks.filter( |
||||
(diff) => diff.status === LIST_STATUS.DELETED, |
||||
); |
||||
const additions = receivedChunks.filter( |
||||
(diff) => diff.status === LIST_STATUS.ADDED, |
||||
); |
||||
const updates = receivedChunks.filter( |
||||
(diff) => diff.status === LIST_STATUS.EQUAL, |
||||
); |
||||
expect(receivedChunks.length).toBe(150_000); // 50.000 deletions + 50.000 equal + 50.000 additions
|
||||
expect(chunkCount).toBe(15); |
||||
expect(deletions.length).toBe(50000); |
||||
expect(additions.length).toBe(50000); |
||||
expect(updates.length).toBe(50000); |
||||
done(); |
||||
}); |
||||
}); |
||||
}); |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
import { isEqualOptions } from "./models/utils"; |
||||
import { isEqualOptions } from "@models/utils"; |
||||
|
||||
/** |
||||
* Returns true if two data are equal |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
import { isEqual, isObject } from "../src/utils"; |
||||
import { isEqual, isObject } from "."; |
||||
|
||||
describe("isEqual", () => { |
||||
it("return true if data are the same", () => { |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
import { LIST_STATUS } from "@models/list"; |
||||
|
||||
export type StreamListDiff<T extends Record<string, unknown>> = { |
||||
currentValue: T | null; |
||||
previousValue: T | null; |
||||
prevIndex: number | null; |
||||
newIndex: number | null; |
||||
indexDiff: number | null; |
||||
status: LIST_STATUS; |
||||
}; |
||||
|
||||
export type ReferenceProperty<T extends Record<string, unknown>> = keyof T; |
||||
|
||||
export type StreamReferences<T extends Record<string, unknown>> = Map< |
||||
ReferenceProperty<T>, |
||||
{ prevIndex: number; nextIndex?: number } |
||||
>; |
||||
|
||||
export type ListStreamOptions = { |
||||
chunksSize?: number; // 0 by default.
|
||||
showOnly?: `${LIST_STATUS}`[]; |
||||
considerMoveAsUpdate?: boolean; |
||||
}; |
||||
|
||||
export const DEFAULT_LIST_STREAM_OPTIONS: ListStreamOptions = { |
||||
chunksSize: 0, |
||||
}; |
Loading…
Reference in new issue