From 2f204b373b1cae09556fdb2c9b08bd061db6a380 Mon Sep 17 00:00:00 2001 From: Antoine Lanoe Date: Mon, 28 Oct 2024 17:23:55 +0100 Subject: [PATCH] feat: use workers for streamListDiff --- .gitignore | 4 +- README.md | 31 +- eslint.config.mjs | 2 +- index.ts | 0 jest.config.js => jest.config.ts | 12 +- jest.setup.ts | 5 + package-lock.json | 1564 ++++++++++------- package.json | 41 +- scripts/transpile-node-worker.js | 17 + src/lib/list-diff/index.ts | 36 +- src/lib/list-diff/list-diff.test.ts | 8 +- src/lib/object-diff/index.ts | 52 +- src/lib/object-diff/object-diff.test.ts | 26 +- src/lib/stream-list-diff/client/index.ts | 93 +- .../client/stream-list-diff-client.test.ts | 153 +- .../stream-list-diff-client.worker.test.ts | 1116 ++++++++++++ .../stream-list-diff/client/worker/utils.ts | 78 + .../client/worker/web-worker.ts | 30 + src/lib/stream-list-diff/server/index.ts | 93 +- .../server/stream-list-diff.test.ts | 149 +- .../server/stream-list-diff.worker.test.ts | 1048 +++++++++++ .../server/worker/node-worker.ts | 38 + .../stream-list-diff/server/worker/utils.ts | 78 + src/lib/stream-list-diff/utils.ts | 8 +- .../emitter.ts => models/emitter/index.ts} | 27 +- src/models/list/index.ts | 27 +- src/models/object/index.ts | 14 +- src/models/stream/index.ts | 36 +- src/models/worker/index.ts | 22 + tsconfig.json | 5 +- tsup.config.ts | 28 +- 31 files changed, 3875 insertions(+), 966 deletions(-) delete mode 100644 index.ts rename jest.config.js => jest.config.ts (62%) create mode 100644 jest.setup.ts create mode 100644 scripts/transpile-node-worker.js create mode 100644 src/lib/stream-list-diff/client/stream-list-diff-client.worker.test.ts create mode 100644 src/lib/stream-list-diff/client/worker/utils.ts create mode 100644 src/lib/stream-list-diff/client/worker/web-worker.ts create mode 100644 src/lib/stream-list-diff/server/stream-list-diff.worker.test.ts create mode 100644 src/lib/stream-list-diff/server/worker/node-worker.ts create mode 100644 src/lib/stream-list-diff/server/worker/utils.ts rename src/{lib/stream-list-diff/emitter.ts => models/emitter/index.ts} (67%) create mode 100644 src/models/worker/index.ts diff --git a/.gitignore b/.gitignore index 19e6239..640d792 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /node_modules dist -.eslintcache \ No newline at end of file +.eslintcache +# Ignore generated worker files +src/lib/stream-list-diff/server/worker/node-worker.cjs \ No newline at end of file diff --git a/README.md b/README.md index 5198e2f..0c871ef 100644 --- a/README.md +++ b/README.md @@ -104,12 +104,12 @@ type ObjectDiff = { diff: Diff[]; }; -/** recursive diff in case of subproperties */ type Diff = { property: string; previousValue: unknown; currentValue: unknown; status: "added" | "deleted" | "equal" | "updated"; + // recursive diff in case of subproperties diff?: Diff[]; }; ``` @@ -307,13 +307,15 @@ getListDiff( ```js // If you are in a server environment -import { streamListDiff } from "@donedeal0/superdiff/server"; +import { streamListDiff } from "@donedeal0/superdiff/server.cjs"; // If you are in a browser environment import { streamListDiff } from "@donedeal0/superdiff/client"; ``` Streams the diff of two object lists, ideal for large lists and maximum performance. +ℹ️ `streamListDiff` requires ESM support for browser usage. It will work out of the box if you use a modern bundler (Webpack, Rollup) or JavaScript framework (Next.js, Vue.js). + #### FORMAT **Input** @@ -330,6 +332,8 @@ Streams the diff of two object lists, ideal for large lists and maximum performa showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default chunksSize?: number, // 0 by default considerMoveAsUpdate?: boolean; // false by default + useWorker?: boolean; // true by default + showWarnings?: boolean; // true by default } ``` @@ -345,6 +349,9 @@ Streams the diff of two object lists, ideal for large lists and maximum performa showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default chunksSize?: number, // 0 by default considerMoveAsUpdate?: boolean; // false by default + useWorker?: boolean; // true by default + showWarnings?: boolean; // true by default + } ``` @@ -355,6 +362,10 @@ Streams the diff of two object lists, ideal for large lists and maximum performa - `chunksSize` the number of object diffs returned by each streamed chunk. (e.g. `0` = 1 object diff per chunk, `10` = 10 object diffs per chunk). - `showOnly` gives you the option to return only the values whose status you are interested in (e.g. `["added", "equal"]`). - `considerMoveAsUpdate`: if set to `true` a `moved` value will be considered as `updated`. + - `useWorker`: if set to `true`, the diff will be run in a worker for maximum performance. Only recommended for large lists (e.g. +100,000 items). + - `showWarnings`: if set to `true`, potential warnings will be displayed in the console. + +> ⚠️ Warning: using Readable streams may impact workers' performance since they need to be converted to arrays. Consider using arrays or files for optimal performance. Alternatively, you can turn the `useWorker` option off. **Output** @@ -364,20 +375,12 @@ The objects diff are grouped into arrays - called `chunks` - and are consumed th - `error`: to be notified if an error occurs during the stream. ```ts -interface StreamListener> { - on>( - event: E, - listener: Listener[E]>, - ): this; +interface StreamListener { + on(event: "data", listener: (chunk: StreamListDiff[]) => void); + on(event: "finish", listener: () => void); + on(event: "error", listener: (error: Error) => void); } -type EmitterEvents> = { - data: [StreamListDiff[]]; - error: [Error]; - finish: []; -}; - - type StreamListDiff> = { currentValue: T | null; previousValue: T | null; diff --git a/eslint.config.mjs b/eslint.config.mjs index 769dd55..6c09409 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,7 @@ import tseslint from "typescript-eslint"; export default [ { files: ["**/*.{js,mjs,cjs,ts}"] }, - { ignores: ["dist", "jest.config.js"] }, + { ignores: ["dist", "jest.config.js", "src/lib/stream-list-diff/server/worker/node-worker.cjs"] }, { settings: { react: { version: "detect" } } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, diff --git a/index.ts b/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/jest.config.js b/jest.config.ts similarity index 62% rename from jest.config.js rename to jest.config.ts index 0dbaf11..9e801ee 100644 --- a/jest.config.js +++ b/jest.config.ts @@ -1,6 +1,8 @@ -module.exports = { +import type { Config } from "jest"; + +const config: Config = { transform: { - "^.+\\.(ts|js)$": [ + "^.+\\.(ts|js)$": [ "@swc/jest", { jsc: { @@ -11,13 +13,17 @@ module.exports = { dynamicImport: true, }, paths: { + "@mocks/*": ["./src/mocks/*"], "@models/*": ["./src/models/*"], "@lib/*": ["./src/lib/*"], - }, target: "esnext", }, }, ], }, + testEnvironment: "node", + setupFilesAfterEnv: ["/jest.setup.ts"], }; + +export default config; diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 0000000..c8f8d7b --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1,5 @@ +import { TextEncoder, TextDecoder } from "util"; + +global.TextEncoder = TextEncoder; +//@ts-expect-error - the TextDecoder is valid +global.TextDecoder = TextDecoder; diff --git a/package-lock.json b/package-lock.json index ef1fda1..472f9ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,34 @@ { "name": "@donedeal0/superdiff", - "version": "2.1.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@donedeal0/superdiff", - "version": "2.1.0", + "version": "3.0.0", "license": "ISC", "devDependencies": { - "@eslint/js": "^9.11.1", + "@eslint/js": "^9.14.0", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.0", "@semantic-release/npm": "^12.0.1", - "@swc/core": "^1.7.26", - "@swc/jest": "^0.2.36", - "@types/jest": "^29.5.13", + "@swc/core": "^1.7.42", + "@swc/jest": "^0.2.37", + "@types/jest": "^29.5.14", "blob-polyfill": "^9.0.20240710", - "eslint": "^9.11.1", + "eslint": "^9.14.0", "husky": "^9.1.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jsdom": "^25.0.1", "prettier": "^3.3.3", "swc-loader": "^0.2.6", - "tsup": "^8.3.0", - "typescript": "^5.6.2", - "typescript-eslint": "^8.7.0", + "ts-node": "^10.9.2", + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "typescript-eslint": "^8.12.2", "web-streams-polyfill": "^4.0.0" }, "funding": { @@ -494,10 +496,32 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], @@ -511,9 +535,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], @@ -527,9 +551,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], @@ -543,9 +567,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], @@ -559,9 +583,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], @@ -575,9 +599,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], @@ -591,9 +615,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], @@ -607,9 +631,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], @@ -623,9 +647,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], @@ -639,9 +663,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], @@ -655,9 +679,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], @@ -671,9 +695,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], @@ -687,9 +711,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], @@ -703,9 +727,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], @@ -719,9 +743,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], @@ -735,9 +759,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], @@ -751,9 +775,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], @@ -767,9 +791,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], @@ -783,9 +807,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ "arm64" ], @@ -799,9 +823,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], @@ -815,9 +839,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], @@ -831,9 +855,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], @@ -847,9 +871,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], @@ -863,9 +887,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], @@ -906,9 +930,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -929,9 +953,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", - "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -991,9 +1015,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", - "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1020,6 +1044,41 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1034,9 +1093,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", + "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", "dev": true, "engines": { "node": ">=18.18" @@ -2140,9 +2199,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", + "integrity": "sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==", "cpu": [ "arm" ], @@ -2153,9 +2212,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz", + "integrity": "sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==", "cpu": [ "arm64" ], @@ -2166,9 +2225,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz", + "integrity": "sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==", "cpu": [ "arm64" ], @@ -2179,9 +2238,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz", + "integrity": "sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==", "cpu": [ "x64" ], @@ -2191,10 +2250,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz", + "integrity": "sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz", + "integrity": "sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz", + "integrity": "sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==", "cpu": [ "arm" ], @@ -2205,9 +2290,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz", + "integrity": "sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==", "cpu": [ "arm" ], @@ -2218,9 +2303,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz", + "integrity": "sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==", "cpu": [ "arm64" ], @@ -2231,9 +2316,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz", + "integrity": "sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==", "cpu": [ "arm64" ], @@ -2244,9 +2329,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz", + "integrity": "sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==", "cpu": [ "ppc64" ], @@ -2257,9 +2342,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz", + "integrity": "sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==", "cpu": [ "riscv64" ], @@ -2270,9 +2355,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz", + "integrity": "sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==", "cpu": [ "s390x" ], @@ -2283,9 +2368,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz", + "integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==", "cpu": [ "x64" ], @@ -2296,9 +2381,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz", + "integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==", "cpu": [ "x64" ], @@ -2309,9 +2394,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz", + "integrity": "sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==", "cpu": [ "arm64" ], @@ -2322,9 +2407,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz", + "integrity": "sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==", "cpu": [ "ia32" ], @@ -2335,9 +2420,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz", + "integrity": "sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==", "cpu": [ "x64" ], @@ -2851,14 +2936,14 @@ } }, "node_modules/@swc/core": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", - "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.42.tgz", + "integrity": "sha512-iQrRk3SKndQZ4ptJv1rzeQSiCYQIhMjiO97QXOlCcCoaazOLKPnLnXzU4Kv0FuBFyYfG2FE94BoR0XI2BN02qw==", "dev": true, "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.12" + "@swc/types": "^0.1.13" }, "engines": { "node": ">=10" @@ -2868,16 +2953,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.26", - "@swc/core-darwin-x64": "1.7.26", - "@swc/core-linux-arm-gnueabihf": "1.7.26", - "@swc/core-linux-arm64-gnu": "1.7.26", - "@swc/core-linux-arm64-musl": "1.7.26", - "@swc/core-linux-x64-gnu": "1.7.26", - "@swc/core-linux-x64-musl": "1.7.26", - "@swc/core-win32-arm64-msvc": "1.7.26", - "@swc/core-win32-ia32-msvc": "1.7.26", - "@swc/core-win32-x64-msvc": "1.7.26" + "@swc/core-darwin-arm64": "1.7.42", + "@swc/core-darwin-x64": "1.7.42", + "@swc/core-linux-arm-gnueabihf": "1.7.42", + "@swc/core-linux-arm64-gnu": "1.7.42", + "@swc/core-linux-arm64-musl": "1.7.42", + "@swc/core-linux-x64-gnu": "1.7.42", + "@swc/core-linux-x64-musl": "1.7.42", + "@swc/core-win32-arm64-msvc": "1.7.42", + "@swc/core-win32-ia32-msvc": "1.7.42", + "@swc/core-win32-x64-msvc": "1.7.42" }, "peerDependencies": { "@swc/helpers": "*" @@ -2889,9 +2974,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", - "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.42.tgz", + "integrity": "sha512-fWhaCs2+8GDRIcjExVDEIfbptVrxDqG8oHkESnXgymmvqTWzWei5SOnPNMS8Q+MYsn/b++Y2bDxkcwmq35Bvxg==", "cpu": [ "arm64" ], @@ -2905,9 +2990,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", - "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.42.tgz", + "integrity": "sha512-ZaVHD2bijrlkCyD7NDzLmSK849Jgcx+6DdL4x1dScoz1slJ8GTvLtEu0JOUaaScQwA+cVlhmrmlmi9ssjbRLGQ==", "cpu": [ "x64" ], @@ -2921,9 +3006,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", - "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.42.tgz", + "integrity": "sha512-iF0BJj7hVTbY/vmbvyzVTh/0W80+Q4fbOYschdUM3Bsud39TA+lSaPOefOHywkNH58EQ1z3EAxYcJOWNES7GFQ==", "cpu": [ "arm" ], @@ -2937,9 +3022,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", - "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.42.tgz", + "integrity": "sha512-xGu8j+DOLYTLkVmsfZPJbNPW1EkiWgSucT0nOlz77bLxImukt/0+HVm2hOwHSKuArQ8C3cjahAMY3b/s4VH2ww==", "cpu": [ "arm64" ], @@ -2953,9 +3038,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", - "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.42.tgz", + "integrity": "sha512-qtW3JNO7i1yHEko59xxz+jY38+tYmB96JGzj6XzygMbYJYZDYbrOpXQvKbMGNG3YeTDan7Fp2jD0dlKf7NgDPA==", "cpu": [ "arm64" ], @@ -2969,9 +3054,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", - "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.42.tgz", + "integrity": "sha512-F9WY1TN+hhhtiEzZjRQziNLt36M5YprMeOBHjsLVNqwgflzleSI7ulgnlQECS8c8zESaXj3ksGduAoJYtPC1cA==", "cpu": [ "x64" ], @@ -2985,9 +3070,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", - "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.42.tgz", + "integrity": "sha512-7YMdOaYKLMQ8JGfnmRDwidpLFs/6ka+80zekeM0iCVO48yLrJR36G0QGXzMjKsXI0BPhq+mboZRRENK4JfQnEA==", "cpu": [ "x64" ], @@ -3001,9 +3086,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", - "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.42.tgz", + "integrity": "sha512-C5CYWaIZEyqPl5W/EwcJ/mLBJFHVoUEa/IwWi0b4q2fCXcSCktQGwKXOQ+d67GneiZoiq0HasgcdMmMpGS9YRQ==", "cpu": [ "arm64" ], @@ -3017,9 +3102,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", - "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.42.tgz", + "integrity": "sha512-3j47seZ5pO62mbrqvPe1iwhe2BXnM5q7iB+n2xgA38PCGYt0mnaJafqmpCXm/uYZOCMqSNynaoOWCMMZm4sqtA==", "cpu": [ "ia32" ], @@ -3033,9 +3118,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", - "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", + "version": "1.7.42", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.42.tgz", + "integrity": "sha512-FXl9MdeUogZLGDcLr6QIRdDVkpG0dkN4MLM4dwQ5kcAk+XfKPrQibX6M2kcfhsCx+jtBqtK7hRFReRXPWJZGbA==", "cpu": [ "x64" ], @@ -3055,9 +3140,9 @@ "dev": true }, "node_modules/@swc/jest": { - "version": "0.2.36", - "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.36.tgz", - "integrity": "sha512-8X80dp81ugxs4a11z1ka43FPhP+/e+mJNXJSxiNYk8gIX/jPBtY4gQTrKu/KIoco8bzKuPI5lUxjfLiGsfvnlw==", + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.37.tgz", + "integrity": "sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==", "dev": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", @@ -3072,9 +3157,9 @@ } }, "node_modules/@swc/types": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", - "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", + "integrity": "sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==", "dev": true, "dependencies": { "@swc/counter": "^0.1.3" @@ -3089,6 +3174,30 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3170,9 +3279,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", - "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3255,16 +3364,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz", - "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", + "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/type-utils": "8.7.0", - "@typescript-eslint/utils": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3288,15 +3397,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz", - "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", + "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/typescript-estree": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4" }, "engines": { @@ -3316,13 +3425,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", - "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", + "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0" + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3333,13 +3442,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz", - "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", + "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.7.0", - "@typescript-eslint/utils": "8.7.0", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/utils": "8.12.2", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3357,9 +3466,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", - "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", + "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3370,13 +3479,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", - "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", + "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3434,15 +3543,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", - "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", + "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.7.0", - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/typescript-estree": "8.7.0" + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3456,12 +3565,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", - "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", + "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/types": "8.12.2", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3667,9 +3776,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3825,6 +3934,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -4027,18 +4142,6 @@ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", "dev": true }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/blob-polyfill": { "version": "9.0.20240710", "resolved": "https://registry.npmjs.org/blob-polyfill/-/blob-polyfill-9.0.20240710.tgz", @@ -4206,39 +4309,18 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dev": true, "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/chrome-trace-event": { @@ -4768,6 +4850,12 @@ "node": ">=8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4816,47 +4904,40 @@ "dev": true }, "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", "dev": true, "dependencies": { - "cssom": "~0.3.6" + "rrweb-cssom": "^0.7.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/data-urls/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/data-urls/node_modules/webidl-conversions": { @@ -4869,16 +4950,16 @@ } }, "node_modules/data-urls/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dev": true, "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/debug": { @@ -4960,6 +5041,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5287,9 +5377,9 @@ "peer": true }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, "hasInstallScript": true, "bin": { @@ -5299,30 +5389,30 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/escalade": { @@ -5365,21 +5455,21 @@ } }, "node_modules/eslint": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", - "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", + "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.6.0", + "@eslint/core": "^0.7.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.11.1", + "@eslint/js": "9.14.0", "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -5387,9 +5477,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5399,13 +5489,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { @@ -5427,9 +5515,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -5443,9 +5531,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5583,14 +5671,14 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6229,15 +6317,15 @@ "dev": true }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "dependencies": { - "whatwg-encoding": "^2.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-escaper": { @@ -6461,18 +6549,6 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -6543,15 +6619,6 @@ "node": ">=8" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -7228,80 +7295,320 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=14" } }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-environment-jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "iconv-lite": "0.6.3" }, "engines": { - "node": ">=7.0.0" + "node": ">=12" } }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "node": ">=12" } }, "node_modules/jest-environment-node": { @@ -8368,43 +8675,38 @@ } }, "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^2.11.2" }, "peerDependenciesMeta": { "canvas": { @@ -8412,45 +8714,6 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/jsdom/node_modules/parse5": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", @@ -8464,15 +8727,15 @@ } }, "node_modules/jsdom/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/jsdom/node_modules/webidl-conversions": { @@ -8485,16 +8748,16 @@ } }, "node_modules/jsdom/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dev": true, "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/jsesc": { @@ -8810,6 +9073,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -11958,9 +12227,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -11986,9 +12255,9 @@ } }, "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, "engines": { "node": ">= 6" @@ -12233,9 +12502,9 @@ "dev": true }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -12419,15 +12688,16 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/registry-auth-token": { @@ -12515,9 +12785,9 @@ } }, "node_modules/rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", + "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", "dev": true, "dependencies": { "@types/estree": "1.0.6" @@ -12530,25 +12800,33 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", + "@rollup/rollup-android-arm-eabi": "4.24.3", + "@rollup/rollup-android-arm64": "4.24.3", + "@rollup/rollup-darwin-arm64": "4.24.3", + "@rollup/rollup-darwin-x64": "4.24.3", + "@rollup/rollup-freebsd-arm64": "4.24.3", + "@rollup/rollup-freebsd-x64": "4.24.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.3", + "@rollup/rollup-linux-arm-musleabihf": "4.24.3", + "@rollup/rollup-linux-arm64-gnu": "4.24.3", + "@rollup/rollup-linux-arm64-musl": "4.24.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.3", + "@rollup/rollup-linux-riscv64-gnu": "4.24.3", + "@rollup/rollup-linux-s390x-gnu": "4.24.3", + "@rollup/rollup-linux-x64-gnu": "4.24.3", + "@rollup/rollup-linux-x64-musl": "4.24.3", + "@rollup/rollup-win32-arm64-msvc": "4.24.3", + "@rollup/rollup-win32-ia32-msvc": "4.24.3", + "@rollup/rollup-win32-x64-msvc": "4.24.3", "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13689,13 +13967,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true + }, "node_modules/tinyglobby": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.6.tgz", - "integrity": "sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", "dev": true, "dependencies": { - "fdir": "^6.3.0", + "fdir": "^6.4.2", "picomatch": "^4.0.2" }, "engines": { @@ -13703,9 +13987,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", - "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" @@ -13728,6 +14012,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.56.tgz", + "integrity": "sha512-2PT1oRZCxtsbLi5R2SQjE/v4vvgRggAtVcYj+3Rrcnu2nPZvu7m64+gDa/EsVSWd3QzEc0U0xN+rbEKsJC47kA==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.56" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.56.tgz", + "integrity": "sha512-Ihxv/Bwiyj73icTYVgBUkQ3wstlCglLoegSgl64oSrGUBX1hc7Qmf/CnrnJLaQdZrCnTaLqMYOwKMKlkfkFrxQ==", + "dev": true + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -13756,27 +14058,15 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { @@ -13811,9 +14101,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", + "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", "dev": true, "engines": { "node": ">=16" @@ -13828,27 +14118,70 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsup": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz", - "integrity": "sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.5.tgz", + "integrity": "sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==", "dev": true, "dependencies": { "bundle-require": "^5.0.0", "cac": "^6.7.14", - "chokidar": "^3.6.0", + "chokidar": "^4.0.1", "consola": "^3.2.3", - "debug": "^4.3.5", - "esbuild": "^0.23.0", - "execa": "^5.1.1", + "debug": "^4.3.7", + "esbuild": "^0.24.0", "joycon": "^3.1.1", - "picocolors": "^1.0.1", + "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", - "rollup": "^4.19.0", + "rollup": "^4.24.0", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", - "tinyglobby": "^0.2.1", + "tinyexec": "^0.3.1", + "tinyglobby": "^0.2.9", "tree-kill": "^1.2.2" }, "bin": { @@ -13925,9 +14258,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -13938,14 +14271,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.7.0.tgz", - "integrity": "sha512-nEHbEYJyHwsuf7c3V3RS7Saq+1+la3i0ieR3qP0yjqWSzVmh8Drp47uOl9LjbPANac4S7EFSqvcYIKXUUwIfIQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.12.2.tgz", + "integrity": "sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.7.0", - "@typescript-eslint/parser": "8.7.0", - "@typescript-eslint/utils": "8.7.0" + "@typescript-eslint/eslint-plugin": "8.12.2", + "@typescript-eslint/parser": "8.12.2", + "@typescript-eslint/utils": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -14091,6 +14424,12 @@ "dev": true, "peer": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -14116,15 +14455,15 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/walker": { @@ -14254,24 +14593,24 @@ "peer": true }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { @@ -14458,12 +14797,12 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { @@ -14524,6 +14863,15 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 37e1a83..ac1dee5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@donedeal0/superdiff", - "version": "3.0.0", + "version": "3.1.0", + "type": "module", "description": "SuperDiff compares two arrays or objects and returns a full diff of their differences", "main": "dist/index.js", "module": "dist/index.mjs", @@ -9,6 +10,18 @@ "files": [ "dist" ], + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./client": { + "default": "./dist/client.mjs" + }, + "./server": { + "default": "./dist/server.cjs" + } + }, "author": "DoneDeal0", "license": "ISC", "repository": { @@ -60,6 +73,11 @@ "object", "diff", "deep-diff", + "json-diff", + "files diff", + "json", + "file", + "isobject", "comparison", "compare", "stream", @@ -73,28 +91,31 @@ "lint:dead-code": "npx -p typescript@latest -p knip knip", "lint": "eslint --cache --max-warnings=0 --fix", "prepare": "husky", - "test": "jest", + "transpile": "node scripts/transpile-node-worker.js", + "test": "npm run transpile && jest", "tsc": "tsc --noEmit --incremental" }, "devDependencies": { - "@eslint/js": "^9.11.1", + "@eslint/js": "^9.14.0", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.0", "@semantic-release/npm": "^12.0.1", - "@swc/core": "^1.7.26", - "@swc/jest": "^0.2.36", - "@types/jest": "^29.5.13", + "@swc/core": "^1.7.42", + "@swc/jest": "^0.2.37", + "@types/jest": "^29.5.14", "blob-polyfill": "^9.0.20240710", - "eslint": "^9.11.1", + "eslint": "^9.14.0", "husky": "^9.1.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jsdom": "^25.0.1", "prettier": "^3.3.3", "swc-loader": "^0.2.6", - "tsup": "^8.3.0", - "typescript": "^5.6.2", - "typescript-eslint": "^8.7.0", + "ts-node": "^10.9.2", + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "typescript-eslint": "^8.12.2", "web-streams-polyfill": "^4.0.0" } } diff --git a/scripts/transpile-node-worker.js b/scripts/transpile-node-worker.js new file mode 100644 index 0000000..fd2b12c --- /dev/null +++ b/scripts/transpile-node-worker.js @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-undef */ +import { execSync } from "child_process"; +import { existsSync } from "fs" + +// The src/lib/stream-list-diff/server/node-worker.ts file needs to be transpiled to a .cjs file to be used in the tests. +const workerFile = "src/lib/stream-list-diff/server/worker/node-worker" + +try { + if(!existsSync(`${workerFile}.cjs`)){ + execSync(`npx esbuild ${workerFile}.ts --bundle --platform=node --format=cjs --outfile=${workerFile}.cjs`, { + stdio: "inherit", + }); + } +} catch (_) { + process.exit(1); +} \ No newline at end of file diff --git a/src/lib/list-diff/index.ts b/src/lib/list-diff/index.ts index 065fde3..ae94065 100644 --- a/src/lib/list-diff/index.ts +++ b/src/lib/list-diff/index.ts @@ -1,10 +1,10 @@ +import { isEqual, isObject } from "@lib/utils"; import { DEFAULT_LIST_DIFF_OPTIONS, - LIST_STATUS, + ListStatus, ListDiff, ListDiffOptions, } from "@models/list"; -import { isEqual, isObject } from "@lib/utils"; function getLeanDiff( diff: ListDiff["diff"], @@ -15,13 +15,13 @@ function getLeanDiff( function formatSingleListDiff( listData: T[], - status: LIST_STATUS, + status: ListStatus, options: ListDiffOptions = { showOnly: [] }, ): ListDiff { const diff = listData.map((data, i) => ({ value: data, - prevIndex: status === LIST_STATUS.ADDED ? null : i, - newIndex: status === LIST_STATUS.ADDED ? i : null, + prevIndex: status === ListStatus.ADDED ? null : i, + newIndex: status === ListStatus.ADDED ? i : null, indexDiff: null, status, })); @@ -39,10 +39,10 @@ function formatSingleListDiff( }; } -function getListStatus(listDiff: ListDiff["diff"]): LIST_STATUS { - return listDiff.some((value) => value.status !== LIST_STATUS.EQUAL) - ? LIST_STATUS.UPDATED - : LIST_STATUS.EQUAL; +function getListStatus(listDiff: ListDiff["diff"]): ListStatus { + return listDiff.some((value) => value.status !== ListStatus.EQUAL) + ? ListStatus.UPDATED + : ListStatus.EQUAL; } function isReferencedObject( @@ -72,15 +72,15 @@ export const getListDiff = ( if (!prevList && !nextList) { return { type: "list", - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, diff: [], }; } if (!prevList) { - return formatSingleListDiff(nextList as T[], LIST_STATUS.ADDED, options); + return formatSingleListDiff(nextList as T[], ListStatus.ADDED, options); } if (!nextList) { - return formatSingleListDiff(prevList as T[], LIST_STATUS.DELETED, options); + return formatSingleListDiff(prevList as T[], ListStatus.DELETED, options); } const diff: ListDiff["diff"] = []; const prevIndexMatches = new Set(); @@ -106,10 +106,10 @@ export const getListDiff = ( } const indexDiff = prevIndex === -1 ? null : i - prevIndex; if (indexDiff === 0 || options.ignoreArrayOrder) { - let nextStatus = LIST_STATUS.EQUAL; + let nextStatus = ListStatus.EQUAL; if (isReferencedObject(nextValue, options.referenceProperty)) { if (!isEqual(prevList[prevIndex], nextValue)) { - nextStatus = LIST_STATUS.UPDATED; + nextStatus = ListStatus.UPDATED; } } return diff.push({ @@ -126,7 +126,7 @@ export const getListDiff = ( prevIndex: null, newIndex: i, indexDiff, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }); } return diff.push({ @@ -135,8 +135,8 @@ export const getListDiff = ( newIndex: i, indexDiff, status: options.considerMoveAsUpdate - ? LIST_STATUS.UPDATED - : LIST_STATUS.MOVED, + ? ListStatus.UPDATED + : ListStatus.MOVED, }); }); @@ -147,7 +147,7 @@ export const getListDiff = ( prevIndex: i, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }); } }); diff --git a/src/lib/list-diff/list-diff.test.ts b/src/lib/list-diff/list-diff.test.ts index 94ed606..c8f90e6 100644 --- a/src/lib/list-diff/list-diff.test.ts +++ b/src/lib/list-diff/list-diff.test.ts @@ -1,5 +1,5 @@ +import { ListStatus } from "@models/list"; import { getListDiff } from "."; -import { LIST_STATUS } from "@models/list"; describe("getListDiff", () => { it("returns an empty diff if no lists are provided", () => { @@ -418,7 +418,7 @@ describe("getListDiff", () => { false, { name: "joe", age: 88 }, ], - { showOnly: [LIST_STATUS.ADDED, LIST_STATUS.DELETED] }, + { showOnly: [ListStatus.ADDED, ListStatus.DELETED] }, ), ).toStrictEqual({ type: "list", @@ -463,7 +463,7 @@ describe("getListDiff", () => { }); expect( getListDiff(["mbappe", "mendes", "verratti", "ruiz"], null, { - showOnly: [LIST_STATUS.MOVED, LIST_STATUS.UPDATED], + showOnly: [ListStatus.MOVED, ListStatus.UPDATED], }), ).toStrictEqual({ type: "list", @@ -474,7 +474,7 @@ describe("getListDiff", () => { it("returns all values if their status match the required statuses", () => { expect( getListDiff(null, ["mbappe", "mendes", "verratti", "ruiz"], { - showOnly: [LIST_STATUS.ADDED], + showOnly: [ListStatus.ADDED], }), ).toStrictEqual({ type: "list", diff --git a/src/lib/object-diff/index.ts b/src/lib/object-diff/index.ts index af03b4b..b5acb23 100644 --- a/src/lib/object-diff/index.ts +++ b/src/lib/object-diff/index.ts @@ -1,13 +1,13 @@ +import { isEqual, isObject } from "@lib/utils"; import { - GRANULARITY, - OBJECT_STATUS, + Granularity, + ObjectStatus, ObjectData, ObjectDiff, ObjectDiffOptions, Diff, DEFAULT_OBJECT_DIFF_OPTIONS, } from "@models/object"; -import { isEqual, isObject } from "@lib/utils"; function getLeanDiff( diff: ObjectDiff["diff"], @@ -17,7 +17,7 @@ function getLeanDiff( const res: ObjectDiff["diff"] = []; for (let i = 0; i < diff.length; i++) { const value = diff[i]; - if (granularity === GRANULARITY.DEEP && value.diff) { + if (granularity === Granularity.DEEP && value.diff) { const leanDiff = getLeanDiff(value.diff, showOnly); if (leanDiff.length > 0) { res.push({ ...value, diff: leanDiff }); @@ -29,21 +29,21 @@ function getLeanDiff( return res; } -function getObjectStatus(diff: ObjectDiff["diff"]): OBJECT_STATUS { - return diff.some((property) => property.status !== OBJECT_STATUS.EQUAL) - ? OBJECT_STATUS.UPDATED - : OBJECT_STATUS.EQUAL; +function getObjectStatus(diff: ObjectDiff["diff"]): ObjectStatus { + return diff.some((property) => property.status !== ObjectStatus.EQUAL) + ? ObjectStatus.UPDATED + : ObjectStatus.EQUAL; } function formatSingleObjectDiff( data: ObjectData, - status: OBJECT_STATUS, + status: ObjectStatus, options: ObjectDiffOptions = DEFAULT_OBJECT_DIFF_OPTIONS, ): ObjectDiff { if (!data) { return { type: "object", - status: OBJECT_STATUS.EQUAL, + status: ObjectStatus.EQUAL, diff: [], }; } @@ -55,16 +55,16 @@ function formatSingleObjectDiff( for (const [subProperty, subValue] of Object.entries(value)) { subPropertiesDiff.push({ property: subProperty, - previousValue: status === OBJECT_STATUS.ADDED ? undefined : subValue, - currentValue: status === OBJECT_STATUS.ADDED ? subValue : undefined, + previousValue: status === ObjectStatus.ADDED ? undefined : subValue, + currentValue: status === ObjectStatus.ADDED ? subValue : undefined, status, }); } diff.push({ property, previousValue: - status === OBJECT_STATUS.ADDED ? undefined : data[property], - currentValue: status === OBJECT_STATUS.ADDED ? value : undefined, + status === ObjectStatus.ADDED ? undefined : data[property], + currentValue: status === ObjectStatus.ADDED ? value : undefined, status, diff: subPropertiesDiff, }); @@ -72,8 +72,8 @@ function formatSingleObjectDiff( diff.push({ property, previousValue: - status === OBJECT_STATUS.ADDED ? undefined : data[property], - currentValue: status === OBJECT_STATUS.ADDED ? value : undefined, + status === ObjectStatus.ADDED ? undefined : data[property], + currentValue: status === ObjectStatus.ADDED ? value : undefined, status, }); } @@ -97,11 +97,11 @@ function getValueStatus( previousValue: unknown, nextValue: unknown, options?: ObjectDiffOptions, -): OBJECT_STATUS { +): ObjectStatus { if (isEqual(previousValue, nextValue, options)) { - return OBJECT_STATUS.EQUAL; + return ObjectStatus.EQUAL; } - return OBJECT_STATUS.UPDATED; + return ObjectStatus.UPDATED; } function getDiff( @@ -123,7 +123,7 @@ function getDiff( property, previousValue: prevSubValue, currentValue: undefined, - status: OBJECT_STATUS.DELETED, + status: ObjectStatus.DELETED, }); continue; } @@ -132,20 +132,20 @@ function getDiff( property, previousValue: undefined, currentValue: nextSubValue, - status: OBJECT_STATUS.ADDED, + status: ObjectStatus.ADDED, }); continue; } if (isObject(nextSubValue) && isObject(prevSubValue)) { const subDiff = getDiff(prevSubValue, nextSubValue, options); const isUpdated = subDiff.some( - (entry) => entry.status !== OBJECT_STATUS.EQUAL, + (entry) => entry.status !== ObjectStatus.EQUAL, ); diff.push({ property, previousValue: prevSubValue, currentValue: nextSubValue, - status: isUpdated ? OBJECT_STATUS.UPDATED : OBJECT_STATUS.EQUAL, + status: isUpdated ? ObjectStatus.UPDATED : ObjectStatus.EQUAL, ...(isUpdated && { diff: subDiff }), }); } else { @@ -180,15 +180,15 @@ export function getObjectDiff( if (!prevData && !nextData) { return { type: "object", - status: OBJECT_STATUS.EQUAL, + status: ObjectStatus.EQUAL, diff: [], }; } if (!prevData) { - return formatSingleObjectDiff(nextData, OBJECT_STATUS.ADDED, options); + return formatSingleObjectDiff(nextData, ObjectStatus.ADDED, options); } if (!nextData) { - return formatSingleObjectDiff(prevData, OBJECT_STATUS.DELETED, options); + return formatSingleObjectDiff(prevData, ObjectStatus.DELETED, options); } const diff: ObjectDiff["diff"] = getDiff(prevData, nextData, options); const status = getObjectStatus(diff); diff --git a/src/lib/object-diff/object-diff.test.ts b/src/lib/object-diff/object-diff.test.ts index 78fea53..6efef72 100644 --- a/src/lib/object-diff/object-diff.test.ts +++ b/src/lib/object-diff/object-diff.test.ts @@ -1,4 +1,4 @@ -import { GRANULARITY, OBJECT_STATUS } from "@models/object"; +import { Granularity, ObjectStatus } from "@models/object"; import { getObjectDiff } from "."; describe("getObjectDiff", () => { @@ -51,8 +51,8 @@ describe("getObjectDiff", () => { null, { showOnly: { - statuses: [OBJECT_STATUS.ADDED], - granularity: GRANULARITY.DEEP, + statuses: [ObjectStatus.ADDED], + granularity: Granularity.DEEP, }, }, ), @@ -509,7 +509,7 @@ describe("getObjectDiff", () => { nickname: "super joe", }, }, - { showOnly: { statuses: [OBJECT_STATUS.ADDED] } }, + { showOnly: { statuses: [ObjectStatus.ADDED] } }, ), ).toStrictEqual({ type: "object", @@ -549,8 +549,8 @@ describe("getObjectDiff", () => { }, { showOnly: { - statuses: [OBJECT_STATUS.ADDED, OBJECT_STATUS.DELETED], - granularity: GRANULARITY.DEEP, + statuses: [ObjectStatus.ADDED, ObjectStatus.DELETED], + granularity: Granularity.DEEP, }, }, ), @@ -634,8 +634,8 @@ describe("getObjectDiff", () => { }, { showOnly: { - statuses: [OBJECT_STATUS.UPDATED], - granularity: GRANULARITY.DEEP, + statuses: [ObjectStatus.UPDATED], + granularity: Granularity.DEEP, }, }, ), @@ -742,8 +742,8 @@ describe("getObjectDiff", () => { }, { showOnly: { - statuses: [OBJECT_STATUS.ADDED], - granularity: GRANULARITY.DEEP, + statuses: [ObjectStatus.ADDED], + granularity: Granularity.DEEP, }, }, ), @@ -834,8 +834,8 @@ describe("getObjectDiff", () => { }, { showOnly: { - statuses: [OBJECT_STATUS.DELETED], - granularity: GRANULARITY.DEEP, + statuses: [ObjectStatus.DELETED], + granularity: Granularity.DEEP, }, }, ), @@ -850,7 +850,7 @@ describe("getObjectDiff", () => { getObjectDiff( { name: "joe", age: 54, hobbies: ["golf", "football"] }, null, - { showOnly: { statuses: [OBJECT_STATUS.DELETED] } }, + { showOnly: { statuses: [ObjectStatus.DELETED] } }, ), ).toStrictEqual({ type: "object", diff --git a/src/lib/stream-list-diff/client/index.ts b/src/lib/stream-list-diff/client/index.ts index d35ae98..7af43a2 100644 --- a/src/lib/stream-list-diff/client/index.ts +++ b/src/lib/stream-list-diff/client/index.ts @@ -1,24 +1,21 @@ +import { IEmitter, EmitterEvents, EventEmitter } from "@models/emitter"; import { DataBuffer, DEFAULT_LIST_STREAM_OPTIONS, ListStreamOptions, ReferenceProperty, -} from "@models/stream"; -import { LIST_STATUS } from "@models/list"; -import { - Emitter, - EmitterEvents, - EventEmitter, - StreamListener, StreamEvent, -} from "../emitter"; + StreamListener, +} from "@models/stream"; +import { ListStatus, ListType } from "@models/list"; import { isDataValid, isValidChunkSize, outputDiffChunk } from "../utils"; +import { generateWorker } from "./worker/utils"; async function getDiffChunks>( prevStream: ReadableStream, nextStream: ReadableStream, referenceProperty: ReferenceProperty, - emitter: Emitter, + emitter: IEmitter, options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, ): Promise { if (!isValidChunkSize(options?.chunksSize)) { @@ -42,7 +39,7 @@ async function getDiffChunks>( const { isValid, message } = isDataValid( chunk, referenceProperty, - "prevList", + ListType.PREV, ); if (!isValid) { emitter.emit(StreamEvent.Error, new Error(message)); @@ -67,10 +64,10 @@ async function getDiffChunks>( indexDiff, status: indexDiff === 0 - ? LIST_STATUS.EQUAL + ? ListStatus.EQUAL : options.considerMoveAsUpdate - ? LIST_STATUS.UPDATED - : LIST_STATUS.MOVED, + ? ListStatus.UPDATED + : ListStatus.MOVED, }, options, ); @@ -82,7 +79,7 @@ async function getDiffChunks>( prevIndex: currentPrevIndex, newIndex: relatedChunk.index, indexDiff, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, options, ); @@ -97,7 +94,7 @@ async function getDiffChunks>( const { isValid, message } = isDataValid( chunk, referenceProperty, - "nextList", + ListType.NEXT, ); if (!isValid) { emitter.emit(StreamEvent.Error, new Error(message)); @@ -122,10 +119,10 @@ async function getDiffChunks>( indexDiff, status: indexDiff === 0 - ? LIST_STATUS.EQUAL + ? ListStatus.EQUAL : options.considerMoveAsUpdate - ? LIST_STATUS.UPDATED - : LIST_STATUS.MOVED, + ? ListStatus.UPDATED + : ListStatus.MOVED, }, options, ); @@ -137,7 +134,7 @@ async function getDiffChunks>( prevIndex: relatedChunk.index, newIndex: currentNextIndex, indexDiff, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, options, ); @@ -175,7 +172,7 @@ async function getDiffChunks>( prevIndex: chunk.index, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, options, ); @@ -189,7 +186,7 @@ async function getDiffChunks>( prevIndex: null, newIndex: chunk.index, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, options, ); @@ -202,7 +199,7 @@ async function getDiffChunks>( async function getValidClientStream>( input: ReadableStream | T[] | File, - listType: "prevList" | "nextList", + listType: ListType, ): Promise> { if (Array.isArray(input)) { return new ReadableStream({ @@ -243,6 +240,25 @@ async function getValidClientStream>( ); } +export async function generateStream>( + prevList: ReadableStream | File | T[], + nextList: ReadableStream | File | T[], + referenceProperty: ReferenceProperty, + options: ListStreamOptions, + emitter: IEmitter, +): Promise { + try { + const [prevStream, nextStream] = await Promise.all([ + getValidClientStream(prevList, ListType.PREV), + getValidClientStream(nextList, ListType.NEXT), + ]); + + getDiffChunks(prevStream, nextStream, referenceProperty, emitter, options); + } catch (err) { + return emitter.emit(StreamEvent.Error, err as Error); + } +} + /** * Streams the diff of two object lists * @param {ReadableStream | File | Record[]} prevList - The original object list. @@ -250,8 +266,10 @@ async function getValidClientStream>( * @param {string} 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` + - `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`. + - `useWorker`: if set to `true`, the diff will be run in a worker. Recommended for maximum performance, `true` by default. + - `showWarnings`: if set to `true`, potential warnings will be displayed in the console. * @returns StreamListener */ export function streamListDiff>( @@ -261,23 +279,16 @@ export function streamListDiff>( options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, ): StreamListener { const emitter = new EventEmitter>(); - setTimeout(async () => { - try { - const [prevStream, nextStream] = await Promise.all([ - getValidClientStream(prevList, "prevList"), - getValidClientStream(nextList, "nextList"), - ]); - getDiffChunks( - prevStream, - nextStream, - referenceProperty, - emitter, - options, - ); - } catch (err) { - return emitter.emit(StreamEvent.Error, err as Error); - } - }, 0); + if (typeof Worker === "undefined" || !options.useWorker) { + setTimeout( + () => + generateStream(prevList, nextList, referenceProperty, options, emitter), + 0, + ); + } else { + generateWorker(prevList, nextList, referenceProperty, options, emitter); + } + return emitter as StreamListener; } diff --git a/src/lib/stream-list-diff/client/stream-list-diff-client.test.ts b/src/lib/stream-list-diff/client/stream-list-diff-client.test.ts index fe5f1dd..efea10d 100644 --- a/src/lib/stream-list-diff/client/stream-list-diff-client.test.ts +++ b/src/lib/stream-list-diff/client/stream-list-diff-client.test.ts @@ -3,13 +3,13 @@ */ import "blob-polyfill"; import { ReadableStream } from "web-streams-polyfill"; -import { LIST_STATUS } from "@models/list"; +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 "."; -import prevListFile from "../../../mocks/prevList.json"; -import nextListFile from "../../../mocks/nextList.json"; -//@ts-expect-error - the ReadableStream polyfill is necessary to test ReadableStream in a Node environment. +// @ts-expect-error - the ReadableStream polyfill is necessary to test ReadableStream in a Node environment. global.ReadableStream = ReadableStream; describe("data emission", () => { @@ -18,7 +18,10 @@ describe("data emission", () => { { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ]; - const diff = streamListDiff([], nextList, "id", { chunksSize: 2 }); + const diff = streamListDiff([], nextList, "id", { + chunksSize: 2, + useWorker: false, + }); const expectedChunks = [ { @@ -27,7 +30,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 0, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, { previousValue: null, @@ -35,7 +38,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 1, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; let chunkCount = 0; @@ -53,7 +56,10 @@ describe("data emission", () => { { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ]; - const diff = streamListDiff(prevList, [], "id", { chunksSize: 2 }); + const diff = streamListDiff(prevList, [], "id", { + chunksSize: 2, + useWorker: false, + }); const expectedChunks = [ { @@ -62,7 +68,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: { id: 2, name: "Item 2" }, @@ -70,7 +76,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ]; let chunkCount = 0; @@ -78,7 +84,7 @@ describe("data emission", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("shiiiite", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -93,7 +99,7 @@ describe("data emission", () => { { id: 2, name: "Item 2" }, { id: 3, name: "Item 3" }, ]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); const expectedChunks = [ [ @@ -103,7 +109,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 0, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, ], [ @@ -113,7 +119,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ], [ @@ -123,7 +129,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 1, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ], ]; @@ -166,6 +172,7 @@ describe("data emission", () => { ]; const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, + useWorker: false, }); const expectedChunks = [ @@ -176,7 +183,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -184,7 +191,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -192,7 +199,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 5, name: "Item 5" }, @@ -200,7 +207,7 @@ describe("data emission", () => { prevIndex: 4, newIndex: 3, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 6, name: "Item 6" }, @@ -208,7 +215,7 @@ describe("data emission", () => { prevIndex: 5, newIndex: 4, indexDiff: -1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, ], [ @@ -218,7 +225,7 @@ describe("data emission", () => { prevIndex: 6, newIndex: 5, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 9, name: "Item 9" }, @@ -226,7 +233,7 @@ describe("data emission", () => { prevIndex: 8, newIndex: 8, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 10, name: "Item 10" }, @@ -234,7 +241,7 @@ describe("data emission", () => { prevIndex: 9, newIndex: 6, indexDiff: -3, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 8, name: "Item 8" }, @@ -242,7 +249,7 @@ describe("data emission", () => { prevIndex: 7, newIndex: 9, indexDiff: 2, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 4, name: "Item 4" }, @@ -250,7 +257,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ], [ @@ -260,7 +267,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 7, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ], ]; @@ -293,6 +300,7 @@ describe("data emission", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 150, + useWorker: false, }); const expectedChunks = [ @@ -302,7 +310,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -310,7 +318,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -318,7 +326,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 4, name: "Item 4" }, @@ -326,7 +334,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -334,7 +342,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -365,6 +373,7 @@ describe("data emission", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, considerMoveAsUpdate: true, + useWorker: false, }); const expectedChunks = [ @@ -374,7 +383,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 0, indexDiff: -1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 1, name: "Item 1" }, @@ -382,7 +391,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 1, indexDiff: 1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -390,7 +399,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 4, name: "Item 4" }, @@ -398,7 +407,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -406,7 +415,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -437,6 +446,7 @@ describe("data emission", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, showOnly: ["added", "deleted"], + useWorker: false, }); const expectedChunks = [ @@ -446,7 +456,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -454,7 +464,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -524,6 +534,7 @@ describe("data emission", () => { ]; const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, + useWorker: false, }); const expectedChunks = [ @@ -542,7 +553,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -550,7 +561,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { @@ -566,7 +577,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 5, name: "Item 5" }, @@ -574,7 +585,7 @@ describe("data emission", () => { prevIndex: 4, newIndex: 3, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { @@ -590,7 +601,7 @@ describe("data emission", () => { prevIndex: 5, newIndex: 4, indexDiff: -1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, ], [ @@ -600,7 +611,7 @@ describe("data emission", () => { prevIndex: 6, newIndex: 5, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 9, name: "Item 9" }, @@ -608,7 +619,7 @@ describe("data emission", () => { prevIndex: 8, newIndex: 8, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { @@ -632,7 +643,7 @@ describe("data emission", () => { prevIndex: 9, newIndex: 6, indexDiff: -3, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 8, name: "Item 8" }, @@ -640,7 +651,7 @@ describe("data emission", () => { prevIndex: 7, newIndex: 9, indexDiff: 2, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { @@ -652,7 +663,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ], [ @@ -662,7 +673,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 7, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ], ]; @@ -701,7 +712,7 @@ describe("input handling", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -709,7 +720,7 @@ describe("input handling", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -717,7 +728,7 @@ describe("input handling", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 4, name: "Item 4" }, @@ -725,7 +736,7 @@ describe("input handling", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -733,7 +744,7 @@ describe("input handling", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -753,6 +764,7 @@ describe("input handling", () => { const diff = streamListDiff(prevStream, nextStream, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -760,7 +772,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -777,6 +789,7 @@ describe("input handling", () => { const diff = streamListDiff(prevFile, nextFile, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -784,7 +797,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -803,6 +816,7 @@ describe("input handling", () => { const diff = streamListDiff(prevStream, nextFile, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -810,7 +824,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -826,6 +840,7 @@ describe("input handling", () => { const diff = streamListDiff(prevStream, nextList, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -833,7 +848,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -846,6 +861,7 @@ describe("input handling", () => { const diff = streamListDiff(prevFile, nextList, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -853,7 +869,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -863,7 +879,7 @@ describe("input handling", () => { describe("finish event", () => { it("emits 'finish' event if no prevList nor nextList is provided", (done) => { - const diff = streamListDiff([], [], "id"); + const diff = streamListDiff([], [], "id", { useWorker: false }); diff.on("finish", () => done()); }); it("emits 'finish' event when all the chunks have been processed", (done) => { @@ -875,7 +891,7 @@ describe("finish event", () => { { id: 2, name: "Item 2" }, { id: 3, name: "Item 3" }, ]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("finish", () => done()); }); }); @@ -893,7 +909,7 @@ describe("error event", () => { ]; // @ts-expect-error prevList is invalid by design for the test - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -915,7 +931,7 @@ describe("error event", () => { ]; // @ts-expect-error nextList is invalid by design for the test - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -932,7 +948,7 @@ describe("error event", () => { { id: 2, name: "Item 2" }, ]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -949,7 +965,7 @@ describe("error event", () => { ]; const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -968,6 +984,7 @@ describe("error event", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: -3, + useWorker: false, }); diff.on("error", (err) => { @@ -982,7 +999,9 @@ describe("error event", () => { 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"); + const diff = streamListDiff({ name: "hello" }, nextList, "id", { + useWorker: false, + }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -995,7 +1014,7 @@ describe("error event", () => { 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"); + const diff = streamListDiff(prevList, null, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( diff --git a/src/lib/stream-list-diff/client/stream-list-diff-client.worker.test.ts b/src/lib/stream-list-diff/client/stream-list-diff-client.worker.test.ts new file mode 100644 index 0000000..57c70af --- /dev/null +++ b/src/lib/stream-list-diff/client/stream-list-diff-client.worker.test.ts @@ -0,0 +1,1116 @@ +/** + * @jest-environment jsdom + */ +import "blob-polyfill"; +import "jsdom"; +import { ReadableStream } from "web-streams-polyfill"; +import prevListFile from "@mocks/prevList.json"; +import nextListFile from "@mocks/nextList.json"; +import { ListStatus } from "@models/list"; +import { + ListStreamOptions, + ReferenceProperty, + StreamEvent, + StreamListDiff, +} from "@models/stream"; +import { workerDiff } from "./worker/utils"; +import { streamListDiff } from "."; + +class Worker { + onmessage: ((event: { data: unknown }) => void) | null = null; + + postMessage>(msg: { + prevList: File | T[]; + nextList: File | T[]; + referenceProperty: ReferenceProperty; + options: ListStreamOptions; + }) { + if (msg) { + const { prevList, nextList, referenceProperty, options } = msg; + const listener = workerDiff( + prevList, + nextList, + referenceProperty, + options, + ); + + listener.on(StreamEvent.Data, (chunk) => { + this.onmessage!({ + data: { event: StreamEvent.Data, chunk }, + }); + }); + + listener.on(StreamEvent.Finish, () => { + this.onmessage!({ + data: { event: StreamEvent.Finish }, + }); + }); + + listener.on(StreamEvent.Error, (error) => { + this.onmessage!({ + data: { event: StreamEvent.Error, error: error.message }, + }); + }); + } + } + + terminate() {} +} + +// @ts-expect-error - a Worker polyfill is necessary to test a Web Worker in a Node environment. +global.Worker = Worker; +// @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, + showWarnings: 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 }); + + 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"); + + 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 }); + + 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, + }); + + 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, + }); + + 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"], + }); + + 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, + }); + + 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, + showWarnings: 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, + }); + + 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, + showWarnings: 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, + showWarnings: 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, + }); + + 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"); + 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("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"); + + 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(); + }); + }); + + 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"); + + 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"); + + 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)); + }); +}); diff --git a/src/lib/stream-list-diff/client/worker/utils.ts b/src/lib/stream-list-diff/client/worker/utils.ts new file mode 100644 index 0000000..114151b --- /dev/null +++ b/src/lib/stream-list-diff/client/worker/utils.ts @@ -0,0 +1,78 @@ +import { IEmitter, EmitterEvents, EventEmitter } from "@models/emitter"; +import { + ListStreamOptions, + READABLE_STREAM_ALERT, + ReferenceProperty, + StreamEvent, + StreamListener, +} from "@models/stream"; +import { WebWorkerMessage } from "@models/worker"; +import { generateStream } from ".."; + +export function workerDiff>( + prevList: File | T[], + nextList: File | T[], + referenceProperty: ReferenceProperty, + options: ListStreamOptions, +): StreamListener { + const emitter = new EventEmitter>(); + setTimeout( + () => + generateStream(prevList, nextList, referenceProperty, options, emitter), + 0, + ); + return emitter as StreamListener; +} + +async function getArrayFromStream( + readableStream: ReadableStream, + showWarnings: boolean = true, +): Promise { + if (showWarnings) { + console.warn(READABLE_STREAM_ALERT); + } + const reader = readableStream.getReader(); + const chunks: T[] = []; + let result; + while (!(result = await reader.read()).done) { + chunks.push(result.value); + } + return chunks; +} + +export async function generateWorker>( + prevList: ReadableStream | File | T[], + nextList: ReadableStream | File | T[], + referenceProperty: ReferenceProperty, + options: ListStreamOptions, + emitter: IEmitter, +) { + try { + if (prevList instanceof ReadableStream) { + prevList = await getArrayFromStream(prevList, options?.showWarnings); + } + if (nextList instanceof ReadableStream) { + nextList = await getArrayFromStream(nextList, options?.showWarnings); + } + const worker = new Worker(new URL("./web-worker.js", import.meta.url), { + type: "module", + }); + worker.postMessage({ prevList, nextList, referenceProperty, options }); + worker.onmessage = (e: WebWorkerMessage) => { + const { event, chunk, error } = e.data; + if (event === StreamEvent.Data) { + emitter.emit(StreamEvent.Data, chunk); + } else if (event === StreamEvent.Finish) { + emitter.emit(StreamEvent.Finish); + worker.terminate(); + } else if (event === StreamEvent.Error) { + emitter.emit(StreamEvent.Error, new Error(error)); + worker.terminate(); + } + }; + worker.onerror = (err: ErrorEvent) => + emitter.emit(StreamEvent.Error, new Error(err.message)); + } catch (err) { + return emitter.emit(StreamEvent.Error, err as Error); + } +} diff --git a/src/lib/stream-list-diff/client/worker/web-worker.ts b/src/lib/stream-list-diff/client/worker/web-worker.ts new file mode 100644 index 0000000..273cd94 --- /dev/null +++ b/src/lib/stream-list-diff/client/worker/web-worker.ts @@ -0,0 +1,30 @@ +import { + ListStreamOptions, + ReferenceProperty, + StreamEvent, +} from "@models/stream"; +import { workerDiff } from "./utils"; + +self.onmessage = async >( + event: MessageEvent<{ + prevList: File | T[]; + nextList: File | T[]; + referenceProperty: ReferenceProperty; + options: ListStreamOptions; + }>, +) => { + const { prevList, nextList, referenceProperty, options } = event.data; + const listener = workerDiff(prevList, nextList, referenceProperty, options); + + listener.on(StreamEvent.Data, (chunk) => { + self.postMessage({ event: StreamEvent.Data, chunk }); + }); + + listener.on(StreamEvent.Finish, () => { + self.postMessage({ event: StreamEvent.Finish }); + }); + + listener.on(StreamEvent.Error, (error) => { + self.postMessage({ event: StreamEvent.Error, error: error.message }); + }); +}; diff --git a/src/lib/stream-list-diff/server/index.ts b/src/lib/stream-list-diff/server/index.ts index d4fbb3e..a7cce68 100644 --- a/src/lib/stream-list-diff/server/index.ts +++ b/src/lib/stream-list-diff/server/index.ts @@ -1,27 +1,25 @@ import { createReadStream } from "fs"; import { Readable, Transform } from "stream"; -import { LIST_STATUS } from "@models/list"; +import { Worker } from "worker_threads"; +import { IEmitter, EmitterEvents, EventEmitter } from "@models/emitter"; +import { ListStatus, ListType } from "@models/list"; import { DataBuffer, DEFAULT_LIST_STREAM_OPTIONS, FilePath, ListStreamOptions, ReferenceProperty, -} from "@models/stream"; -import { - Emitter, - EmitterEvents, - EventEmitter, - StreamListener, StreamEvent, -} from "../emitter"; + StreamListener, +} from "@models/stream"; import { isDataValid, isValidChunkSize, outputDiffChunk } from "../utils"; +import { generateWorker } from "./worker/utils"; async function getDiffChunks>( prevStream: Readable, nextStream: Readable, referenceProperty: ReferenceProperty, - emitter: Emitter, + emitter: IEmitter, options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, ): Promise { if (!isValidChunkSize(options?.chunksSize)) { @@ -42,7 +40,7 @@ async function getDiffChunks>( const { isValid, message } = isDataValid( chunk, referenceProperty, - "prevList", + ListType.PREV, ); if (!isValid) { emitter.emit(StreamEvent.Error, new Error(message)); @@ -67,10 +65,10 @@ async function getDiffChunks>( indexDiff, status: indexDiff === 0 - ? LIST_STATUS.EQUAL + ? ListStatus.EQUAL : options.considerMoveAsUpdate - ? LIST_STATUS.UPDATED - : LIST_STATUS.MOVED, + ? ListStatus.UPDATED + : ListStatus.MOVED, }, options, ); @@ -82,7 +80,7 @@ async function getDiffChunks>( prevIndex: currentPrevIndex, newIndex: relatedChunk.index, indexDiff, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, options, ); @@ -97,7 +95,7 @@ async function getDiffChunks>( const { isValid, message } = isDataValid( chunk, referenceProperty, - "nextList", + ListType.NEXT, ); if (!isValid) { emitter.emit(StreamEvent.Error, new Error(message)); @@ -122,10 +120,10 @@ async function getDiffChunks>( indexDiff, status: indexDiff === 0 - ? LIST_STATUS.EQUAL + ? ListStatus.EQUAL : options.considerMoveAsUpdate - ? LIST_STATUS.UPDATED - : LIST_STATUS.MOVED, + ? ListStatus.UPDATED + : ListStatus.MOVED, }, options, ); @@ -137,7 +135,7 @@ async function getDiffChunks>( prevIndex: relatedChunk.index, newIndex: currentNextIndex, indexDiff, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, options, ); @@ -169,7 +167,7 @@ async function getDiffChunks>( prevIndex: chunk.index, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, options, ); @@ -183,7 +181,7 @@ async function getDiffChunks>( prevIndex: null, newIndex: chunk.index, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, options, ); @@ -195,7 +193,7 @@ async function getDiffChunks>( function getValidStream( input: Readable | FilePath | T[], - listType: "prevList" | "nextList", + listType: ListType, ): Readable { if (input instanceof Readable) { return input; @@ -227,10 +225,27 @@ function getValidStream( }), ); } - throw new Error(`Invalid ${listType}. Expected Readable, Array, or File.`); } +export async function generateStream>( + prevList: Readable | FilePath | T[], + nextList: Readable | FilePath | T[], + referenceProperty: ReferenceProperty, + options: ListStreamOptions, + emitter: IEmitter, +): Promise { + try { + const [prevStream, nextStream] = await Promise.all([ + getValidStream(prevList, ListType.PREV), + getValidStream(nextList, ListType.NEXT), + ]); + getDiffChunks(prevStream, nextStream, referenceProperty, emitter, options); + } catch (err) { + return emitter.emit(StreamEvent.Error, err as Error); + } +} + /** * Streams the diff of two object lists * @param {Readable | FilePath | Record[]} prevList - The original object list. @@ -238,29 +253,27 @@ function getValidStream( * @param {string} 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` + - `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`. + - `useWorker`: if set to `true`, the diff will be run in a worker. Recommended for maximum performance, `true` by default. + - `showWarnings`: if set to `true`, potential warnings will be displayed in the console. * @returns StreamListener */ export function streamListDiff>( - prevStream: Readable | FilePath | T[], - nextStream: Readable | FilePath | T[], + prevList: Readable | FilePath | T[], + nextList: Readable | FilePath | T[], referenceProperty: ReferenceProperty, options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, ): StreamListener { const emitter = new EventEmitter>(); - setTimeout(async () => { - try { - await getDiffChunks( - getValidStream(prevStream, "prevList"), - getValidStream(nextStream, "nextList"), - referenceProperty, - emitter, - options, - ); - } catch (err) { - return emitter.emit(StreamEvent.Error, err as Error); - } - }, 0); + if (typeof Worker === "undefined" || !options.useWorker) { + setTimeout( + () => + generateStream(prevList, nextList, referenceProperty, options, emitter), + 0, + ); + } else { + generateWorker(prevList, nextList, referenceProperty, options, emitter); + } return emitter as StreamListener; } diff --git a/src/lib/stream-list-diff/server/stream-list-diff.test.ts b/src/lib/stream-list-diff/server/stream-list-diff.test.ts index f1751d0..3ae87dc 100644 --- a/src/lib/stream-list-diff/server/stream-list-diff.test.ts +++ b/src/lib/stream-list-diff/server/stream-list-diff.test.ts @@ -1,6 +1,6 @@ import path from "path"; import { Readable } from "stream"; -import { LIST_STATUS } from "@models/list"; +import { ListStatus } from "@models/list"; import { StreamListDiff } from "@models/stream"; import { streamListDiff } from "."; @@ -10,7 +10,10 @@ describe("data emission", () => { { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ]; - const diff = streamListDiff([], nextList, "id", { chunksSize: 2 }); + const diff = streamListDiff([], nextList, "id", { + chunksSize: 2, + useWorker: false, + }); const expectedChunks = [ { @@ -19,7 +22,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 0, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, { previousValue: null, @@ -27,7 +30,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 1, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; let chunkCount = 0; @@ -45,7 +48,10 @@ describe("data emission", () => { { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ]; - const diff = streamListDiff(prevList, [], "id", { chunksSize: 2 }); + const diff = streamListDiff(prevList, [], "id", { + chunksSize: 2, + useWorker: false, + }); const expectedChunks = [ { @@ -54,7 +60,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: { id: 2, name: "Item 2" }, @@ -62,7 +68,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ]; let chunkCount = 0; @@ -70,7 +76,7 @@ describe("data emission", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("shiiiite", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -85,7 +91,7 @@ describe("data emission", () => { { id: 2, name: "Item 2" }, { id: 3, name: "Item 3" }, ]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); const expectedChunks = [ [ @@ -95,7 +101,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 0, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, ], [ @@ -105,7 +111,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ], [ @@ -115,7 +121,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 1, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ], ]; @@ -158,6 +164,7 @@ describe("data emission", () => { ]; const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, + useWorker: false, }); const expectedChunks = [ @@ -168,7 +175,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -176,7 +183,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -184,7 +191,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 5, name: "Item 5" }, @@ -192,7 +199,7 @@ describe("data emission", () => { prevIndex: 4, newIndex: 3, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 6, name: "Item 6" }, @@ -200,7 +207,7 @@ describe("data emission", () => { prevIndex: 5, newIndex: 4, indexDiff: -1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, ], [ @@ -210,7 +217,7 @@ describe("data emission", () => { prevIndex: 6, newIndex: 5, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 9, name: "Item 9" }, @@ -218,7 +225,7 @@ describe("data emission", () => { prevIndex: 8, newIndex: 8, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 10, name: "Item 10" }, @@ -226,7 +233,7 @@ describe("data emission", () => { prevIndex: 9, newIndex: 6, indexDiff: -3, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 8, name: "Item 8" }, @@ -234,7 +241,7 @@ describe("data emission", () => { prevIndex: 7, newIndex: 9, indexDiff: 2, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 4, name: "Item 4" }, @@ -242,7 +249,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ], [ @@ -252,7 +259,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 7, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ], ]; @@ -286,6 +293,7 @@ describe("data emission", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, + useWorker: false, }); const expectedChunks = [ @@ -295,7 +303,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -303,7 +311,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -311,7 +319,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 4, name: "Item 4" }, @@ -319,7 +327,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -327,7 +335,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -336,7 +344,7 @@ describe("data emission", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -358,6 +366,7 @@ describe("data emission", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, considerMoveAsUpdate: true, + useWorker: false, }); const expectedChunks = [ @@ -367,7 +376,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 0, indexDiff: -1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 1, name: "Item 1" }, @@ -375,7 +384,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 1, indexDiff: 1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -383,7 +392,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 4, name: "Item 4" }, @@ -391,7 +400,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -399,7 +408,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -430,6 +439,7 @@ describe("data emission", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, showOnly: ["added", "deleted"], + useWorker: false, }); const expectedChunks = [ @@ -439,7 +449,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -447,7 +457,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -517,6 +527,7 @@ describe("data emission", () => { ]; const diff = streamListDiff(prevList, nextList, "id", { chunksSize: 5, + useWorker: false, }); const expectedChunks = [ @@ -535,7 +546,7 @@ describe("data emission", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -543,7 +554,7 @@ describe("data emission", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { @@ -559,7 +570,7 @@ describe("data emission", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 5, name: "Item 5" }, @@ -567,7 +578,7 @@ describe("data emission", () => { prevIndex: 4, newIndex: 3, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { @@ -583,7 +594,7 @@ describe("data emission", () => { prevIndex: 5, newIndex: 4, indexDiff: -1, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, ], [ @@ -593,7 +604,7 @@ describe("data emission", () => { prevIndex: 6, newIndex: 5, indexDiff: -1, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 9, name: "Item 9" }, @@ -601,7 +612,7 @@ describe("data emission", () => { prevIndex: 8, newIndex: 8, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { @@ -625,7 +636,7 @@ describe("data emission", () => { prevIndex: 9, newIndex: 6, indexDiff: -3, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { id: 8, name: "Item 8" }, @@ -633,7 +644,7 @@ describe("data emission", () => { prevIndex: 7, newIndex: 9, indexDiff: 2, - status: LIST_STATUS.MOVED, + status: ListStatus.MOVED, }, { previousValue: { @@ -645,7 +656,7 @@ describe("data emission", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, ], [ @@ -655,7 +666,7 @@ describe("data emission", () => { prevIndex: null, newIndex: 7, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ], ]; @@ -694,7 +705,7 @@ describe("input handling", () => { prevIndex: 0, newIndex: 0, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 2, name: "Item 2" }, @@ -702,7 +713,7 @@ describe("input handling", () => { prevIndex: 1, newIndex: 1, indexDiff: 0, - status: LIST_STATUS.UPDATED, + status: ListStatus.UPDATED, }, { previousValue: { id: 3, name: "Item 3" }, @@ -710,7 +721,7 @@ describe("input handling", () => { prevIndex: 2, newIndex: 2, indexDiff: 0, - status: LIST_STATUS.EQUAL, + status: ListStatus.EQUAL, }, { previousValue: { id: 4, name: "Item 4" }, @@ -718,7 +729,7 @@ describe("input handling", () => { prevIndex: 3, newIndex: null, indexDiff: null, - status: LIST_STATUS.DELETED, + status: ListStatus.DELETED, }, { previousValue: null, @@ -726,7 +737,7 @@ describe("input handling", () => { prevIndex: null, newIndex: 3, indexDiff: null, - status: LIST_STATUS.ADDED, + status: ListStatus.ADDED, }, ]; @@ -736,6 +747,7 @@ describe("input handling", () => { const diff = streamListDiff(prevStream, nextStream, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -743,7 +755,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -755,6 +767,7 @@ describe("input handling", () => { const diff = streamListDiff(prevFile, nextFile, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -762,7 +775,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -774,6 +787,7 @@ describe("input handling", () => { const diff = streamListDiff(prevStream, nextFile, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -781,7 +795,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -792,6 +806,7 @@ describe("input handling", () => { const diff = streamListDiff(prevStream, nextList, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -799,7 +814,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -810,6 +825,7 @@ describe("input handling", () => { const diff = streamListDiff(prevFile, nextList, "id", { chunksSize: 5, + useWorker: false, }); let chunkCount = 0; @@ -817,7 +833,7 @@ describe("input handling", () => { expect(chunk).toStrictEqual(expectedChunks); chunkCount++; }); - diff.on("error", (err) => console.error("sheeeet", err)); + diff.on("error", (err) => console.error(err)); diff.on("finish", () => { expect(chunkCount).toBe(1); done(); @@ -827,7 +843,7 @@ describe("input handling", () => { describe("finish event", () => { it("emits 'finish' event if no prevList nor nextList is provided", (done) => { - const diff = streamListDiff([], [], "id"); + const diff = streamListDiff([], [], "id", { useWorker: false }); diff.on("finish", () => done()); }); it("emits 'finish' event when all the chunks have been processed", (done) => { @@ -839,7 +855,7 @@ describe("finish event", () => { { id: 2, name: "Item 2" }, { id: 3, name: "Item 3" }, ]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("finish", () => done()); }); }); @@ -857,7 +873,7 @@ describe("error event", () => { ]; // @ts-expect-error prevList is invalid by design for the test - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -879,7 +895,7 @@ describe("error event", () => { ]; // @ts-expect-error nextList is invalid by design for the test - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -896,7 +912,7 @@ describe("error event", () => { { id: 2, name: "Item 2" }, ]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -913,7 +929,7 @@ describe("error event", () => { ]; const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; - const diff = streamListDiff(prevList, nextList, "id"); + const diff = streamListDiff(prevList, nextList, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -932,6 +948,7 @@ describe("error event", () => { const diff = streamListDiff(prevList, nextList, "id", { chunksSize: -3, + useWorker: false, }); diff.on("error", (err) => { @@ -946,7 +963,9 @@ describe("error event", () => { 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"); + const diff = streamListDiff({ name: "hello" }, nextList, "id", { + useWorker: false, + }); diff.on("error", (err) => { expect(err["message"]).toEqual( @@ -959,7 +978,7 @@ describe("error event", () => { 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"); + const diff = streamListDiff(prevList, null, "id", { useWorker: false }); diff.on("error", (err) => { expect(err["message"]).toEqual( diff --git a/src/lib/stream-list-diff/server/stream-list-diff.worker.test.ts b/src/lib/stream-list-diff/server/stream-list-diff.worker.test.ts new file mode 100644 index 0000000..690e1ff --- /dev/null +++ b/src/lib/stream-list-diff/server/stream-list-diff.worker.test.ts @@ -0,0 +1,1048 @@ +import path from "path"; +import { Readable } from "stream"; +import { ListStatus } from "@models/list"; +import { StreamListDiff } from "@models/stream"; +import { streamListDiff } from "."; + +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: true, + }); + + 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).toEqual(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: true, + }); + + 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).toEqual(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: true }); + + 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).toEqual(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: true, + }); + + 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).toEqual(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: 5, + useWorker: true, + }); + + 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).toEqual(expectedChunks); + chunkCount++; + }); + diff.on("error", (err) => console.error(err)); + 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: true, + }); + + 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).toEqual(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: true, + }); + + 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).toEqual(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: true, + }); + + 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).toEqual(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 = Readable.from(prevList, { objectMode: true }); + const nextStream = Readable.from(nextList, { objectMode: true }); + + const diff = streamListDiff(prevStream, nextStream, "id", { + chunksSize: 5, + useWorker: true, + showWarnings: false, + }); + + let chunkCount = 0; + diff.on("data", (chunk) => { + expect(chunk).toEqual(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 = path.resolve(__dirname, "../../../mocks/prevList.json"); + const nextFile = path.resolve(__dirname, "../../../mocks/nextList.json"); + + const diff = streamListDiff(prevFile, nextFile, "id", { + chunksSize: 5, + useWorker: true, + }); + + let chunkCount = 0; + diff.on("data", (chunk) => { + expect(chunk).toEqual(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 = Readable.from(prevList, { objectMode: true }); + const nextFile = path.resolve(__dirname, "../../../mocks/nextList.json"); + + const diff = streamListDiff(prevStream, nextFile, "id", { + chunksSize: 5, + useWorker: true, + showWarnings: false, + }); + + let chunkCount = 0; + diff.on("data", (chunk) => { + expect(chunk).toEqual(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 = Readable.from(prevList, { objectMode: true }); + + const diff = streamListDiff(prevStream, nextList, "id", { + chunksSize: 5, + useWorker: true, + showWarnings: false, + }); + + let chunkCount = 0; + diff.on("data", (chunk) => { + expect(chunk).toEqual(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 = path.resolve(__dirname, "../../../mocks/prevList.json"); + + const diff = streamListDiff(prevFile, nextList, "id", { + chunksSize: 5, + useWorker: true, + }); + + let chunkCount = 0; + diff.on("data", (chunk) => { + expect(chunk).toEqual(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: true }); + 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: true }); + 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: true }); + + 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: true }); + + 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: true }); + + 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: true }); + + 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: true, + }); + + 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: true, + }); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + "Invalid prevList. Expected Readable, 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: true }); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + "Invalid nextList. Expected Readable, 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)); + }); +}); diff --git a/src/lib/stream-list-diff/server/worker/node-worker.ts b/src/lib/stream-list-diff/server/worker/node-worker.ts new file mode 100644 index 0000000..0004a0a --- /dev/null +++ b/src/lib/stream-list-diff/server/worker/node-worker.ts @@ -0,0 +1,38 @@ +import { parentPort } from "worker_threads"; +import { + FilePath, + ListStreamOptions, + ReferenceProperty, + StreamEvent, +} from "@models/stream"; +import { WorkerEvent } from "@models/worker"; +import { workerDiff } from "./utils"; + +parentPort?.on( + WorkerEvent.Message, + async >(event: { + prevList: FilePath | T[]; + nextList: FilePath | T[]; + referenceProperty: ReferenceProperty; + options: ListStreamOptions; + }) => { + const { prevList, nextList, referenceProperty, options } = event; + + const listener = workerDiff(prevList, nextList, referenceProperty, options); + + listener.on(StreamEvent.Data, (chunk) => { + parentPort?.postMessage({ event: StreamEvent.Data, chunk }); + }); + + listener.on(StreamEvent.Finish, () => { + parentPort?.postMessage({ event: StreamEvent.Finish }); + }); + + listener.on(StreamEvent.Error, (error) => { + parentPort?.postMessage({ + event: StreamEvent.Error, + error: error.message, + }); + }); + }, +); diff --git a/src/lib/stream-list-diff/server/worker/utils.ts b/src/lib/stream-list-diff/server/worker/utils.ts new file mode 100644 index 0000000..92d6726 --- /dev/null +++ b/src/lib/stream-list-diff/server/worker/utils.ts @@ -0,0 +1,78 @@ +import path from "path"; +import { once, Readable } from "stream"; +import { Worker } from "worker_threads"; +import { IEmitter, EmitterEvents, EventEmitter } from "@models/emitter"; +import { + FilePath, + ListStreamOptions, + READABLE_STREAM_ALERT, + ReferenceProperty, + StreamEvent, + StreamListener, +} from "@models/stream"; +import { NodeWorkerMessage, WorkerEvent } from "@models/worker"; +import { generateStream } from ".."; + +async function getArrayFromStream( + stream: Readable, + showWarnings: boolean = true, +): Promise { + if (showWarnings) { + console.warn(READABLE_STREAM_ALERT); + } + const data: T[] = []; + stream.on(StreamEvent.Data, (chunk) => data.push(chunk)); + await once(stream, "end"); + return data; +} + +export async function generateWorker>( + prevList: Readable | FilePath | T[], + nextList: Readable | FilePath | T[], + referenceProperty: ReferenceProperty, + options: ListStreamOptions, + emitter: IEmitter, +) { + try { + if (prevList instanceof Readable) { + prevList = await getArrayFromStream(prevList, options?.showWarnings); + } + if (nextList instanceof Readable) { + nextList = await getArrayFromStream(nextList, options?.showWarnings); + } + const worker = new Worker(path.resolve(__dirname, "./node-worker.cjs")); + worker.postMessage({ prevList, nextList, referenceProperty, options }); + worker.on(WorkerEvent.Message, (e: NodeWorkerMessage) => { + const { event, chunk, error } = e; + if (event === StreamEvent.Data) { + emitter.emit(StreamEvent.Data, chunk); + } else if (event === StreamEvent.Finish) { + emitter.emit(StreamEvent.Finish); + worker.terminate(); + } else if (event === StreamEvent.Error) { + emitter.emit(StreamEvent.Error, new Error(error)); + worker.terminate(); + } + }); + worker.on(WorkerEvent.Error, (err) => + emitter.emit(StreamEvent.Error, new Error(err.message)), + ); + } catch (err) { + return emitter.emit(StreamEvent.Error, err as Error); + } +} + +export function workerDiff>( + prevList: FilePath | T[], + nextList: FilePath | T[], + referenceProperty: ReferenceProperty, + options: ListStreamOptions, +): StreamListener { + const emitter = new EventEmitter>(); + setTimeout( + () => + generateStream(prevList, nextList, referenceProperty, options, emitter), + 0, + ); + return emitter as StreamListener; +} diff --git a/src/lib/stream-list-diff/utils.ts b/src/lib/stream-list-diff/utils.ts index 8356343..d823c30 100644 --- a/src/lib/stream-list-diff/utils.ts +++ b/src/lib/stream-list-diff/utils.ts @@ -1,10 +1,12 @@ import { isObject } from "@lib/utils"; +import { IEmitter } from "@models/emitter"; +import { ListType } from "@models/list"; import { ListStreamOptions, ReferenceProperty, + StreamEvent, StreamListDiff, } from "@models/stream"; -import { Emitter, StreamEvent } from "./emitter"; export function isValidChunkSize( chunksSize: ListStreamOptions["chunksSize"], @@ -17,7 +19,7 @@ export function isValidChunkSize( export function isDataValid>( data: T, referenceProperty: ReferenceProperty, - listType: "prevList" | "nextList", + listType: ListType, ): { isValid: boolean; message?: string } { if (!isObject(data)) { return { @@ -38,7 +40,7 @@ export function isDataValid>( } export function outputDiffChunk>( - emitter: Emitter, + emitter: IEmitter, ) { let chunks: StreamListDiff[] = []; diff --git a/src/lib/stream-list-diff/emitter.ts b/src/models/emitter/index.ts similarity index 67% rename from src/lib/stream-list-diff/emitter.ts rename to src/models/emitter/index.ts index b768784..feb8ef1 100644 --- a/src/lib/stream-list-diff/emitter.ts +++ b/src/models/emitter/index.ts @@ -1,14 +1,14 @@ import { StreamListDiff } from "@models/stream"; -type Listener = (...args: T) => void; +export type Listener = (...args: T) => void; -export enum StreamEvent { - Data = "data", - Finish = "finish", - Error = "error", -} +export type EmitterEvents> = { + data: [StreamListDiff[]]; + error: [Error]; + finish: []; +}; -export type Emitter> = EventEmitter<{ +export type IEmitter> = EventEmitter<{ data: [StreamListDiff[]]; error: [Error]; finish: []; @@ -31,16 +31,3 @@ export class EventEmitter> { } } } - -export type EmitterEvents> = { - data: [StreamListDiff[]]; - error: [Error]; - finish: []; -}; - -export interface StreamListener> { - on>( - event: E, - listener: Listener[E]>, - ): this; -} diff --git a/src/models/list/index.ts b/src/models/list/index.ts index 8616faa..2f56730 100644 --- a/src/models/list/index.ts +++ b/src/models/list/index.ts @@ -1,4 +1,11 @@ -export enum LIST_STATUS { +export const DEFAULT_LIST_DIFF_OPTIONS = { + showOnly: [], + referenceProperty: undefined, + considerMoveAsUpdate: false, + ignoreArrayOrder: false, +}; + +export enum ListStatus { ADDED = "added", EQUAL = "equal", DELETED = "deleted", @@ -6,28 +13,26 @@ export enum LIST_STATUS { MOVED = "moved", } +export enum ListType { + PREV = "prevList", + NEXT = "nextList", +} + export type ListDiffOptions = { - showOnly?: `${LIST_STATUS}`[]; + showOnly?: `${ListStatus}`[]; referenceProperty?: string; considerMoveAsUpdate?: boolean; ignoreArrayOrder?: boolean; }; -export const DEFAULT_LIST_DIFF_OPTIONS = { - showOnly: [], - referenceProperty: undefined, - considerMoveAsUpdate: false, - ignoreArrayOrder: false, -}; - export type ListDiff = { type: "list"; - status: LIST_STATUS; + status: `${ListStatus}`; diff: { value: unknown; prevIndex: number | null; newIndex: number | null; indexDiff: number | null; - status: LIST_STATUS; + status: ListStatus; }[]; }; diff --git a/src/models/object/index.ts b/src/models/object/index.ts index 86fa19e..7fa9874 100644 --- a/src/models/object/index.ts +++ b/src/models/object/index.ts @@ -1,11 +1,11 @@ -export enum OBJECT_STATUS { +export enum ObjectStatus { ADDED = "added", EQUAL = "equal", DELETED = "deleted", UPDATED = "updated", } -export enum GRANULARITY { +export enum Granularity { BASIC = "basic", DEEP = "deep", } @@ -15,14 +15,14 @@ export type ObjectData = Record | undefined | null; export type ObjectDiffOptions = { ignoreArrayOrder?: boolean; showOnly?: { - statuses: `${OBJECT_STATUS}`[]; - granularity?: `${GRANULARITY}`; + statuses: `${ObjectStatus}`[]; + granularity?: `${Granularity}`; }; }; export const DEFAULT_OBJECT_DIFF_OPTIONS = { ignoreArrayOrder: false, - showOnly: { statuses: [], granularity: GRANULARITY.BASIC }, + showOnly: { statuses: [], granularity: Granularity.BASIC }, }; /** recursive diff in case of subproperties */ @@ -30,12 +30,12 @@ export type Diff = { property: string; previousValue: unknown; currentValue: unknown; - status: OBJECT_STATUS; + status: `${ObjectStatus}`; diff?: Diff[]; }; export type ObjectDiff = { type: "object"; - status: OBJECT_STATUS; + status: `${ObjectStatus}`; diff: Diff[]; }; diff --git a/src/models/stream/index.ts b/src/models/stream/index.ts index f62b50d..68b1c0f 100644 --- a/src/models/stream/index.ts +++ b/src/models/stream/index.ts @@ -1,4 +1,21 @@ -import { LIST_STATUS } from "@models/list"; +import { EmitterEvents, Listener } from "@models/emitter"; +import { ListStatus } from "@models/list"; + +export const READABLE_STREAM_ALERT = `Warning: using Readable streams may impact workers' performance since they need to be converted to arrays. + Consider using arrays or files for optimal performance. Alternatively, you can turn the 'useWorker' option off. + To disable this warning, set 'showWarnings' to false in production.`; + +export const DEFAULT_LIST_STREAM_OPTIONS: ListStreamOptions = { + chunksSize: 0, + useWorker: true, + showWarnings: true, +}; + +export enum StreamEvent { + Data = "data", + Finish = "finish", + Error = "error", +} export type StreamListDiff> = { currentValue: T | null; @@ -6,7 +23,7 @@ export type StreamListDiff> = { prevIndex: number | null; newIndex: number | null; indexDiff: number | null; - status: LIST_STATUS; + status: `${ListStatus}`; }; export type ReferenceProperty> = keyof T; @@ -26,12 +43,17 @@ export type DataBuffer> = Map< export type ListStreamOptions = { chunksSize?: number; // 0 by default. - showOnly?: `${LIST_STATUS}`[]; + showOnly?: `${ListStatus}`[]; considerMoveAsUpdate?: boolean; -}; - -export const DEFAULT_LIST_STREAM_OPTIONS: ListStreamOptions = { - chunksSize: 0, + useWorker?: boolean; // true by default + showWarnings?: boolean; // true by default }; export type FilePath = string; + +export interface StreamListener> { + on>( + event: E, + listener: Listener[E]>, + ): this; +} diff --git a/src/models/worker/index.ts b/src/models/worker/index.ts new file mode 100644 index 0000000..df9b261 --- /dev/null +++ b/src/models/worker/index.ts @@ -0,0 +1,22 @@ +import { StreamEvent, StreamListDiff } from "@models/stream"; + +export enum WorkerEvent { + Message = "message", + Error = "error", +} + +type WorkerData> = { + chunk: StreamListDiff[]; + error: string; + event: StreamEvent; +}; + +type WorkerMessage> = { + data: WorkerData; +}; + +export type WebWorkerMessage> = + WorkerMessage; + +export type NodeWorkerMessage> = + WorkerData; diff --git a/tsconfig.json b/tsconfig.json index 9fb7612..cc140a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,10 +18,9 @@ "skipLibCheck": true , "baseUrl": ".", "paths": { - "@models/*": ["./src/models/*"], "@lib/*": ["./src/lib/*"], - - + "@mocks/*": ["./src/mocks/*"], + "@models/*": ["./src/models/*"] } }, } diff --git a/tsup.config.ts b/tsup.config.ts index 874abf3..9c6ce2d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -15,17 +15,37 @@ export default defineConfig([ format: ["cjs", "esm"], ...sharedConfig, platform: "neutral", + name: "MAIN", + }, + { + entry: ["src/client.ts"], + format: ["esm"], + ...sharedConfig, + platform: "browser", + name: "CLIENT", + }, + { + entry: ["src/lib/stream-list-diff/client/worker/web-worker.ts"], + format: ["esm"], + ...sharedConfig, + splitting: false, + platform: "browser", + name: "WEB WORKER", }, { entry: ["src/server.ts"], - format: ["cjs", "esm"], + format: ["cjs"], ...sharedConfig, platform: "node", + name: "SERVER", }, { - entry: ["src/client.ts"], - format: ["cjs", "esm"], + entry: ["src/lib/stream-list-diff/server/worker/node-worker.ts"], + format: ["cjs"], ...sharedConfig, - platform: "browser", + splitting: false, + shims: false, + platform: "node", + name: "NODEJS WORKER", }, ]);