\ No newline at end of file
diff --git a/docs/functions/setLogger.html b/docs/functions/setLogger.html
index 0c479ab..e28ef7d 100644
--- a/docs/functions/setLogger.html
+++ b/docs/functions/setLogger.html
@@ -1 +1 @@
-
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
index 4b9b5f7..37aae5f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,4 +1,4 @@
-
\ No newline at end of file
diff --git a/docs/interfaces/BatchClusterEvents.html b/docs/interfaces/BatchClusterEvents.html
index 9db732a..5f923aa 100644
--- a/docs/interfaces/BatchClusterEvents.html
+++ b/docs/interfaces/BatchClusterEvents.html
@@ -1,56 +1,37 @@
-
\ No newline at end of file
diff --git a/docs/interfaces/BatchProcessOptions.html b/docs/interfaces/BatchProcessOptions.html
index d12f207..5477df2 100644
--- a/docs/interfaces/BatchProcessOptions.html
+++ b/docs/interfaces/BatchProcessOptions.html
@@ -1,24 +1,24 @@
-
\ No newline at end of file
diff --git a/docs/interfaces/ChildProcessFactory.html b/docs/interfaces/ChildProcessFactory.html
index 600cf5e..0f96d5f 100644
--- a/docs/interfaces/ChildProcessFactory.html
+++ b/docs/interfaces/ChildProcessFactory.html
@@ -1,15 +1,9 @@
-
\ No newline at end of file
diff --git a/docs/interfaces/Logger.html b/docs/interfaces/Logger.html
index 83d2624..ab9bdff 100644
--- a/docs/interfaces/Logger.html
+++ b/docs/interfaces/Logger.html
@@ -1,7 +1,7 @@
-
\ No newline at end of file
diff --git a/docs/interfaces/Parser.html b/docs/interfaces/Parser.html
index 015e523..bbe81fd 100644
--- a/docs/interfaces/Parser.html
+++ b/docs/interfaces/Parser.html
@@ -1,12 +1,12 @@
-
\ No newline at end of file
diff --git a/docs/interfaces/TypedEventEmitter.html b/docs/interfaces/TypedEventEmitter.html
index 88cc5fe..2e42e84 100644
--- a/docs/interfaces/TypedEventEmitter.html
+++ b/docs/interfaces/TypedEventEmitter.html
@@ -1,7 +1,7 @@
-
Codestin Search App Interface TypedEventEmitter<T> interface TypedEventEmitter < T > { emit < E > ( eventName ,
... args ) : boolean ; listeners < E > ( event ) : Function [] ; off < E > ( eventName ,
listener ) : this ; on < E > ( eventName ,
listener ) : this ; once < E > ( eventName ,
listener ) : this ; removeAllListeners ( eventName ? ) : this ; } Methods emit emit < E > ( eventName , ... args ) : boolean Type Parameters E extends string | number | symbol Parameters eventName : E Rest
... args : Args < T [ E ] > Returns boolean listeners listeners < E > ( event ) : Function [] Type Parameters E extends string | number | symbol Returns Function [] off off < E > ( eventName , listener ) : this Type Parameters E extends string | number | symbol Parameters eventName : E listener : ( ( ... args ) => void ) ( ... args ) : void Returns void Returns this on on < E > ( eventName , listener ) : this Type Parameters E extends string | number | symbol Parameters eventName : E listener : ( ( ... args ) => void ) ( ... args ) : void Returns void Returns this once once < E > ( eventName , listener ) : this Type Parameters E extends string | number | symbol Parameters eventName : E listener : ( ( ... args ) => void ) ( ... args ) : void Returns void Returns this remove All Listeners remove All Listeners ( eventName ? ) : this Parameters Optional
eventName : keyof T Returns this
\ No newline at end of file
+
Codestin Search App Interface TypedEventEmitter<T> interface TypedEventEmitter < T > { emit < E > ( eventName ,
... args ) : boolean ; listeners < E > ( event ) : Function [] ; off < E > ( eventName ,
listener ) : this ; on < E > ( eventName ,
listener ) : this ; once < E > ( eventName ,
listener ) : this ; removeAllListeners ( eventName ? ) : this ; } Methods emit emit < E > ( eventName , ... args ) : boolean Type Parameters E extends string | number | symbol Parameters eventName : E Rest
... args : Args < T [ E ] > Returns boolean listeners listeners < E > ( event ) : Function [] Type Parameters E extends string | number | symbol Returns Function [] off off < E > ( eventName , listener ) : this Type Parameters E extends string | number | symbol Parameters eventName : E listener : ( ( ... args ) => void ) ( ... args ) : void Returns void Returns this on on < E > ( eventName , listener ) : this Type Parameters E extends string | number | symbol Parameters eventName : E listener : ( ( ... args ) => void ) ( ... args ) : void Returns void Returns this once once < E > ( eventName , listener ) : this Type Parameters E extends string | number | symbol Parameters eventName : E listener : ( ( ... args ) => void ) ( ... args ) : void Returns void Returns this remove All Listeners remove All Listeners ( eventName ? ) : this Parameters Optional
eventName : keyof T Returns this
\ No newline at end of file
diff --git a/docs/modules.html b/docs/modules.html
index cbef775..2ce90ef 100644
--- a/docs/modules.html
+++ b/docs/modules.html
@@ -1,27 +1,27 @@
-
Codestin Search App
\ No newline at end of file
+
Codestin Search App
\ No newline at end of file
diff --git a/docs/types/BatchClusterEmitter.html b/docs/types/BatchClusterEmitter.html
index 94fc57e..2b61157 100644
--- a/docs/types/BatchClusterEmitter.html
+++ b/docs/types/BatchClusterEmitter.html
@@ -1,4 +1,4 @@
-
Codestin Search App Type alias BatchClusterEmitter
\ No newline at end of file
diff --git a/docs/types/ChildExitReason.html b/docs/types/ChildExitReason.html
index a02b14f..a935ca7 100644
--- a/docs/types/ChildExitReason.html
+++ b/docs/types/ChildExitReason.html
@@ -1 +1 @@
-
Codestin Search App Type alias ChildExitReason
\ No newline at end of file
+
Codestin Search App Type alias ChildExitReason
\ No newline at end of file
diff --git a/docs/types/WhyNotHealthy.html b/docs/types/WhyNotHealthy.html
index 74974d7..2ae8808 100644
--- a/docs/types/WhyNotHealthy.html
+++ b/docs/types/WhyNotHealthy.html
@@ -1 +1 @@
-
Codestin Search App Why Not Healthy : "broken" | "closed" | "ending" | "ended" | "idle" | "old" | "proc.close" | "proc.disconnect" | "proc.error" | "proc.exit" | "stderr.error" | "stderr" | "stdin.error" | "stdout.error" | "timeout" | "tooMany" | "startError" | "unhealthy" | "worn"
\ No newline at end of file
+
Codestin Search App Why Not Healthy : "broken" | "closed" | "ending" | "ended" | "idle" | "old" | "proc.close" | "proc.disconnect" | "proc.error" | "proc.exit" | "stderr.error" | "stderr" | "stdin.error" | "stdout.error" | "timeout" | "tooMany" | "startError" | "unhealthy" | "worn"
\ No newline at end of file
diff --git a/docs/types/WhyNotReady.html b/docs/types/WhyNotReady.html
index d2fa796..1098b47 100644
--- a/docs/types/WhyNotReady.html
+++ b/docs/types/WhyNotReady.html
@@ -1 +1 @@
-
Codestin Search App
\ No newline at end of file
+
Codestin Search App
\ No newline at end of file
diff --git a/docs/variables/ConsoleLogger.html b/docs/variables/ConsoleLogger.html
index df33ed4..347efea 100644
--- a/docs/variables/ConsoleLogger.html
+++ b/docs/variables/ConsoleLogger.html
@@ -1,4 +1,4 @@
-
Codestin Search App Variable ConsoleLoggerConst
\ No newline at end of file
diff --git a/docs/variables/Log.html b/docs/variables/Log.html
index f563e71..cd1f379 100644
--- a/docs/variables/Log.html
+++ b/docs/variables/Log.html
@@ -1 +1 @@
-
Codestin Search App Log : { filterLevels : ( ( l ,
minLogLevel ) => any ) ; withLevels : ( ( delegate ) => Logger ) ; withTimestamps : ( ( delegate ) => any ) ; } = ... Type declaration filter Levels : ( ( l , minLogLevel ) => any ) ( l , minLogLevel ) : any Returns any with Levels : ( ( delegate ) => Logger ) with Timestamps : ( ( delegate ) => any ) ( delegate ) : any Returns any
\ No newline at end of file
+
Codestin Search App Log : { filterLevels : ( ( l ,
minLogLevel ) => any ) ; withLevels : ( ( delegate ) => Logger ) ; withTimestamps : ( ( delegate ) => any ) ; } = ... Type declaration filter Levels : ( ( l , minLogLevel ) => any ) ( l , minLogLevel ) : any Returns any with Levels : ( ( delegate ) => Logger ) with Timestamps : ( ( delegate ) => any ) ( delegate ) : any Returns any
\ No newline at end of file
diff --git a/docs/variables/LogLevels.html b/docs/variables/LogLevels.html
index 974e82f..627276d 100644
--- a/docs/variables/LogLevels.html
+++ b/docs/variables/LogLevels.html
@@ -1 +1 @@
-
Codestin Search App Log Levels : ( keyof Logger ) [] = ...
\ No newline at end of file
+
Codestin Search App Log Levels : ( keyof Logger ) [] = ...
\ No newline at end of file
diff --git a/docs/variables/NoLogger.html b/docs/variables/NoLogger.html
index df78e77..5c8c0c6 100644
--- a/docs/variables/NoLogger.html
+++ b/docs/variables/NoLogger.html
@@ -1,2 +1,2 @@
-
Codestin Search App
\ No newline at end of file
+
Codestin Search App
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..3ed8913
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,74 @@
+// eslint.config.mjs
+import eslint from "@eslint/js";
+import globals from "globals";
+import tseslint from "typescript-eslint";
+import importPlugin from "eslint-plugin-import";
+
+export default tseslint.config(
+ {
+ ignores: ["dist/", "node_modules/", "**/*.d.ts", "coverage/", "docs/"],
+ },
+ {
+ files: ["src/**/*.ts"],
+ languageOptions: {
+ parser: tseslint.parser,
+ parserOptions: {
+ project: "./tsconfig.json",
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+ globals: globals.node,
+ },
+ },
+ eslint.configs.recommended,
+ ...tseslint.configs.recommendedTypeChecked,
+ ...tseslint.configs.stylistic,
+ {
+ files: ["src/**/*.ts"],
+ ignores: ["src/**/*.spec.ts", "src/test.ts"], // Exclude test files from strict rules
+ plugins: {
+ import: importPlugin,
+ },
+ rules: {
+ // Project-specific preferences that differ from defaults
+ "eqeqeq": ["error", "always", { null: "ignore" }], // Allow == null for defensive coding
+ "@typescript-eslint/no-unnecessary-condition": "off", // We want defensive null checks
+ "@typescript-eslint/prefer-optional-chain": "off", // Prefer explicit null checks for clarity
+
+ // Import rules
+ "import/no-cycle": "error", // TypeScript can't catch circular imports
+
+ // Stricter than defaults
+ "no-console": "error",
+ },
+ },
+ {
+ files: ["src/**/*.spec.ts", "src/test.ts", "src/_chai.spec.ts"],
+ plugins: {
+ import: importPlugin,
+ },
+ rules: {
+ // Relax rules that are problematic for test files
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-unused-expressions": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-floating-promises": "off",
+ "@typescript-eslint/switch-exhaustiveness-check": "off",
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-unsafe-call": "off",
+ "@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/no-unsafe-argument": "off",
+ "@typescript-eslint/no-unnecessary-type-assertion": "off",
+ "@typescript-eslint/await-thenable": "off",
+ "@typescript-eslint/no-misused-promises": "off",
+ "@typescript-eslint/restrict-plus-operands": "off",
+ "no-console": "off",
+ "@typescript-eslint/no-var-requires": "off",
+
+ // Re-enable one valuable rule that's safe for tests
+ "import/no-cycle": "error", // Circular imports are bad even in tests
+ },
+ },
+);
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..242cc94
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5853 @@
+{
+ "name": "batch-cluster",
+ "version": "14.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "batch-cluster",
+ "version": "14.0.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@eslint/js": "^9.27.0",
+ "@sinonjs/fake-timers": "^14.0.0",
+ "@types/chai": "^4.3.11",
+ "@types/chai-as-promised": "^7",
+ "@types/chai-string": "^1.4.5",
+ "@types/chai-subset": "^1.3.6",
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^22.15.21",
+ "@types/sinonjs__fake-timers": "^8.1.5",
+ "chai": "^4.3.10",
+ "chai-as-promised": "^7.1.2",
+ "chai-string": "^1.6.0",
+ "chai-subset": "^1.6.0",
+ "chai-withintoleranceof": "^1.0.1",
+ "eslint": "^9.27.0",
+ "eslint-plugin-import": "^2.31.0",
+ "globals": "^16.2.0",
+ "mocha": "^11.5.0",
+ "npm-check-updates": "^18.0.1",
+ "prettier": "^3.5.3",
+ "prettier-plugin-organize-imports": "^4.1.0",
+ "rimraf": "^5.0.10",
+ "seedrandom": "^3.0.5",
+ "serve": "^14.2.4",
+ "source-map-support": "^0.5.21",
+ "split2": "^4.2.0",
+ "ts-node": "^10.9.2",
+ "typedoc": "^0.28.5",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.32.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
+ "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
+ "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.27.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
+ "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
+ "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.14.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@gerrit0/mini-shiki": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.2.tgz",
+ "integrity": "sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/engine-oniguruma": "^3.4.2",
+ "@shikijs/langs": "^3.4.2",
+ "@shikijs/themes": "^3.4.2",
+ "@shikijs/types": "^3.4.2",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ }
+ },
+ "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,
+ "license": "Apache-2.0",
+ "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,
+ "license": "Apache-2.0",
+ "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,
+ "license": "Apache-2.0",
+ "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",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz",
+ "integrity": "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.4.2",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.2.tgz",
+ "integrity": "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.4.2"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.2.tgz",
+ "integrity": "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.4.2"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.2.tgz",
+ "integrity": "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/@shikijs/vscode-textmate": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-14.0.0.tgz",
+ "integrity": "sha512-QfoXRaUTjMVVn/ZbnD4LS3TPtqOkOdKIYCKldIVPnuClcwRKat6LI2mRZ2s5qiBfO6Fy03An35dSls/2/FEc0Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "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,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai": {
+ "version": "4.3.20",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz",
+ "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai-as-promised": {
+ "version": "7.1.8",
+ "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz",
+ "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "*"
+ }
+ },
+ "node_modules/@types/chai-string": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@types/chai-string/-/chai-string-1.4.5.tgz",
+ "integrity": "sha512-IecXRMSnpUvRnTztdpSdjcmcW7EdNme65bfDCQMi7XrSEPGmyDYYTEfc5fcactWDA6ioSm8o7NUqg9QxjBCCEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "*"
+ }
+ },
+ "node_modules/@types/chai-subset": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz",
+ "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/chai": "<5.2.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mocha": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
+ "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.15.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
+ "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/sinonjs__fake-timers": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz",
+ "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
+ "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.32.1",
+ "@typescript-eslint/type-utils": "8.32.1",
+ "@typescript-eslint/utils": "8.32.1",
+ "@typescript-eslint/visitor-keys": "8.32.1",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
+ "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
+ "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.32.1",
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/typescript-estree": "8.32.1",
+ "@typescript-eslint/visitor-keys": "8.32.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
+ "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/visitor-keys": "8.32.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
+ "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.32.1",
+ "@typescript-eslint/utils": "8.32.1",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
+ "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
+ "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/visitor-keys": "8.32.1",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
+ "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.32.1",
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/typescript-estree": "8.32.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
+ "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.32.1",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@zeit/schemas": {
+ "version": "2.36.0",
+ "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
+ "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-align": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+ "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "node_modules/ansi-align/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-align/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ansi-align/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-align/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/arch": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+ "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+ "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/boxen": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
+ "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-align": "^3.0.1",
+ "camelcase": "^7.0.0",
+ "chalk": "^5.0.1",
+ "cli-boxes": "^3.0.0",
+ "string-width": "^5.1.2",
+ "type-fest": "^2.13.0",
+ "widest-line": "^4.0.1",
+ "wrap-ansi": "^8.0.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/boxen/node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
+ "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chai": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.3",
+ "deep-eql": "^4.1.3",
+ "get-func-name": "^2.0.2",
+ "loupe": "^2.3.6",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chai-as-promised": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz",
+ "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==",
+ "dev": true,
+ "license": "WTFPL",
+ "dependencies": {
+ "check-error": "^1.0.2"
+ },
+ "peerDependencies": {
+ "chai": ">= 2.1.2 < 6"
+ }
+ },
+ "node_modules/chai-string": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.6.0.tgz",
+ "integrity": "sha512-sXV7whDmpax+8H++YaZelgin7aur1LGf9ZhjZa3ojETFJ0uPVuS4XEXuIagpZ/c8uVOtsSh4MwOjy5CBLjJSXA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "chai": "^4.1.2"
+ }
+ },
+ "node_modules/chai-subset": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz",
+ "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chai-withintoleranceof": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/chai-withintoleranceof/-/chai-withintoleranceof-1.0.1.tgz",
+ "integrity": "sha512-KxXzpcb/jWgBPNEVbOGbN4I4ChooIw0oTsxWDWN6EO/ZMivj+lkvm8ME4+vNVsSnjJGyWljj8CI3jS13NclYIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/chai/node_modules/type-detect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk-template": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
+ "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk-template?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/cli-boxes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
+ "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/clipboardy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
+ "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arch": "^2.2.0",
+ "execa": "^5.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
+ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/diff": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+ "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.23.10",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz",
+ "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-regex": "^1.2.1",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.27.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
+ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.20.0",
+ "@eslint/config-helpers": "^0.2.1",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.27.0",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.3.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",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "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"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
+ "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.31.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
+ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.8",
+ "array.prototype.findlastindex": "^1.2.5",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.0",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.15.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.0",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.8",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
+ "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "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,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/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,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
+ "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+ "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.0",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-port-reachable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
+ "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.1"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/lunr": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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,
+ "license": "ISC"
+ },
+ "node_modules/markdown-it": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "11.5.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.5.0.tgz",
+ "integrity": "sha512-VKDjhy6LMTKm0WgNEdlY77YVsD49LZnPSXJAaPNL9NRYQADxvORsyG1DIQY6v53BKTnlNbEE2MbVCDbnxr4K3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browser-stdout": "^1.3.1",
+ "chokidar": "^4.0.1",
+ "debug": "^4.3.5",
+ "diff": "^7.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-up": "^5.0.0",
+ "glob": "^10.4.5",
+ "he": "^1.2.0",
+ "js-yaml": "^4.1.0",
+ "log-symbols": "^4.1.0",
+ "minimatch": "^9.0.5",
+ "ms": "^2.1.3",
+ "picocolors": "^1.1.1",
+ "serialize-javascript": "^6.0.2",
+ "strip-json-comments": "^3.1.1",
+ "supports-color": "^8.1.1",
+ "workerpool": "^6.5.1",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1",
+ "yargs-unparser": "^2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/mocha/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/npm-check-updates": {
+ "version": "18.0.1",
+ "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz",
+ "integrity": "sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "ncu": "build/cli.js",
+ "npm-check-updates": "build/cli.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0",
+ "npm": ">=8.12.1"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
+ "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-organize-imports": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
+ "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "prettier": ">=2.0",
+ "typescript": ">=2.9",
+ "vue-tsc": "^2.1.0"
+ },
+ "peerDependenciesMeta": {
+ "vue-tsc": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/registry-auth-token": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
+ "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rc": "^1.1.6",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/registry-url": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
+ "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rc": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "5.0.10",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^10.3.7"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/seedrandom": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/serve": {
+ "version": "14.2.4",
+ "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
+ "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@zeit/schemas": "2.36.0",
+ "ajv": "8.12.0",
+ "arg": "5.0.2",
+ "boxen": "7.0.0",
+ "chalk": "5.0.1",
+ "chalk-template": "0.4.0",
+ "clipboardy": "3.0.0",
+ "compression": "1.7.4",
+ "is-port-reachable": "4.0.0",
+ "serve-handler": "6.1.6",
+ "update-check": "1.5.4"
+ },
+ "bin": {
+ "serve": "build/main.js"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/serve-handler": {
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
+ "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.0.0",
+ "content-disposition": "0.5.2",
+ "mime-types": "2.1.18",
+ "minimatch": "3.1.2",
+ "path-is-inside": "1.0.2",
+ "path-to-regexp": "3.3.0",
+ "range-parser": "1.2.0"
+ }
+ },
+ "node_modules/serve-handler/node_modules/mime-db": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+ "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-handler/node_modules/mime-types": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+ "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "~1.33.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/serve/node_modules/chalk": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
+ "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/serve/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "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/ts-node/node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-node/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,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typedoc": {
+ "version": "0.28.5",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz",
+ "integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@gerrit0/mini-shiki": "^3.2.2",
+ "lunr": "^2.3.9",
+ "markdown-it": "^14.1.0",
+ "minimatch": "^9.0.5",
+ "yaml": "^2.7.1"
+ },
+ "bin": {
+ "typedoc": "bin/typedoc"
+ },
+ "engines": {
+ "node": ">= 18",
+ "pnpm": ">= 10"
+ },
+ "peerDependencies": {
+ "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x"
+ }
+ },
+ "node_modules/typedoc/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/typedoc/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
+ "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.32.1",
+ "@typescript-eslint/parser": "8.32.1",
+ "@typescript-eslint/utils": "8.32.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-check": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
+ "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "registry-auth-token": "3.3.2",
+ "registry-url": "3.1.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/widest-line": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
+ "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
+ "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yaml": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 4820f67..1dc5ed3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "batch-cluster",
- "version": "13.0.0",
+ "version": "14.0.0",
"description": "Manage a cluster of child processes",
"main": "dist/BatchCluster.js",
"homepage": "https://photostructure.github.io/batch-cluster.js/",
@@ -13,22 +13,22 @@
"url": "https://github.com/photostructure/batch-cluster.js.git"
},
"engines": {
- "node": ">=14"
+ "node": ">=20"
},
"scripts": {
- "ci": "yarn install --frozen-lockfile",
+ "ci": "npm ci",
"clean": "rimraf dist",
- "prettier": "prettier --write src/*.ts",
- "lint": "yarn eslint src --ext .ts",
+ "fmt": "prettier --write src/*.ts",
+ "lint": "eslint src",
"compile": "tsc",
"watch": "rimraf dist & tsc --watch",
- "pretest": "yarn clean && yarn lint && yarn compile",
+ "pretest": "npm run clean && npm run lint && npm run compile",
"test": "mocha dist/**/*.spec.js",
"docs:1": "typedoc --options .typedoc.js",
"docs:2": "cp .serve.json docs/serve.json",
"docs:3": "touch docs/.nojekyll",
- "docs:4": "yarn serve docs",
- "docs": "bash -c 'for i in {1..4} ; do yarn docs:$i ; done'"
+ "docs:4": "serve docs",
+ "docs": "bash -c 'for i in {1..4} ; do npm run docs:$i ; done'"
},
"release-it": {
"src": {
@@ -38,8 +38,8 @@
},
"hooks": {
"before:init": [
- "yarn install",
- "yarn lint"
+ "npm install",
+ "npm run lint"
]
},
"github": {
@@ -49,33 +49,35 @@
"author": "Matthew McEachen
",
"license": "MIT",
"devDependencies": {
+ "@eslint/js": "^9.27.0",
+ "@sinonjs/fake-timers": "^14.0.0",
"@types/chai": "^4.3.11",
- "@types/chai-as-promised": "^7.1.8",
+ "@types/chai-as-promised": "^7",
"@types/chai-string": "^1.4.5",
- "@types/chai-subset": "^1.3.5",
- "@types/mocha": "^10.0.6",
- "@types/node": "^20.11.17",
- "@typescript-eslint/eslint-plugin": "^6.21.0",
- "@typescript-eslint/parser": "^6.21.0",
+ "@types/chai-subset": "^1.3.6",
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^22.15.21",
+ "@types/sinonjs__fake-timers": "^8.1.5",
"chai": "^4.3.10",
- "chai-as-promised": "^7.1.1",
- "chai-string": "^1.5.0",
+ "chai-as-promised": "^7.1.2",
+ "chai-string": "^1.6.0",
"chai-subset": "^1.6.0",
"chai-withintoleranceof": "^1.0.1",
- "eslint": "^8.56.0",
- "eslint-plugin-import": "^2.29.1",
- "mocha": "^10.3.0",
- "npm-check-updates": "^16.14.14",
- "prettier": "^3.2.5",
- "prettier-plugin-organize-imports": "^3.2.4",
- "rimraf": "^5.0.5",
- "release-it": "^17.0.3",
+ "eslint": "^9.27.0",
+ "eslint-plugin-import": "^2.31.0",
+ "globals": "^16.2.0",
+ "mocha": "^11.5.0",
+ "npm-check-updates": "^18.0.1",
+ "prettier": "^3.5.3",
+ "prettier-plugin-organize-imports": "^4.1.0",
+ "rimraf": "^5.0.10",
"seedrandom": "^3.0.5",
- "serve": "^14.2.1",
+ "serve": "^14.2.4",
"source-map-support": "^0.5.21",
"split2": "^4.2.0",
- "timekeeper": "^2.3.1",
- "typedoc": "^0.25.7",
- "typescript": "~5.3.3"
+ "ts-node": "^10.9.2",
+ "typedoc": "^0.28.5",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.32.1"
}
}
diff --git a/src/Array.ts b/src/Array.ts
index 527061d..99d1bf0 100644
--- a/src/Array.ts
+++ b/src/Array.ts
@@ -7,7 +7,6 @@ export function filterInPlace(arr: T[], filter: (t: T) => boolean): T[] {
let j = 0
// PERF: for-loop to avoid the additional closure from a forEach
for (let i = 0; i < len; i++) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ea = arr[i]!
if (filter(ea)) {
if (i !== j) arr[j] = ea
@@ -24,7 +23,6 @@ export function count(
): number {
let acc = 0
for (let idx = 0; idx < arr.length; idx++) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (predicate(arr[idx]!, idx)) acc++
}
return acc
diff --git a/src/Async.ts b/src/Async.ts
index fd507cf..44e63b0 100644
--- a/src/Async.ts
+++ b/src/Async.ts
@@ -28,23 +28,3 @@ export async function until(
}
return false
}
-
-/**
- * @return a thunk that will call the underlying thunk at most every `minDelayMs`
- * milliseconds. The thunk will accept a boolean, that, when set, will force the
- * underlying thunk to be called (mostly useful for tests)
- */
-export function ratelimit(
- f: () => T,
- minDelayMs: number,
-): () => T | undefined {
- let next = 0
- return (force?: boolean) => {
- if (Date.now() > next || force === true) {
- next = Date.now() + minDelayMs
- return f()
- } else {
- return
- }
- }
-}
diff --git a/src/BatchCluster.procps.spec.ts b/src/BatchCluster.procps.spec.ts
new file mode 100644
index 0000000..281b251
--- /dev/null
+++ b/src/BatchCluster.procps.spec.ts
@@ -0,0 +1,21 @@
+import { expect } from "chai"
+import { describe, it } from "mocha"
+import { BatchCluster, ProcpsMissingError } from "./BatchCluster"
+import { DefaultTestOptions } from "./DefaultTestOptions.spec"
+import { processFactory } from "./_chai.spec"
+
+describe("BatchCluster procps validation", () => {
+ it("should validate procps availability during construction", () => {
+ // This test verifies that BatchCluster calls validateProcpsAvailable()
+ // On systems where procps is available (like our test environment),
+ // construction should succeed
+ expect(() => {
+ new BatchCluster({ ...DefaultTestOptions, processFactory })
+ }).to.not.throw()
+ })
+
+ it("should export ProcpsMissingError for user handling", () => {
+ expect(ProcpsMissingError).to.be.a("function")
+ expect(new ProcpsMissingError().name).to.equal("ProcpsMissingError")
+ })
+})
diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts
index d3662f9..0f09333 100644
--- a/src/BatchCluster.spec.ts
+++ b/src/BatchCluster.spec.ts
@@ -1,14 +1,5 @@
+import FakeTimers from "@sinonjs/fake-timers"
import process from "node:process"
-import { filterInPlace } from "./Array"
-import { delay, until } from "./Async"
-import { BatchCluster } from "./BatchCluster"
-import { secondMs } from "./BatchClusterOptions"
-import { DefaultTestOptions } from "./DefaultTestOptions.spec"
-import { map, omit, orElse } from "./Object"
-import { isWin } from "./Platform"
-import { toS } from "./String"
-import { Task } from "./Task"
-import { thenOrTimeout } from "./Timeout"
import {
currentTestPids,
expect,
@@ -24,9 +15,18 @@ import {
times,
unhandledRejections,
} from "./_chai.spec"
+import { filterInPlace } from "./Array"
+import { delay, until } from "./Async"
+import { BatchCluster } from "./BatchCluster"
+import { secondMs } from "./BatchClusterOptions"
+import { DefaultTestOptions } from "./DefaultTestOptions.spec"
+import { map, omit } from "./Object"
+import { isWin } from "./Platform"
+import { toS } from "./String"
+import { Task } from "./Task"
+import { thenOrTimeout } from "./Timeout"
const isCI = process.env.CI === "1"
-const tk = require("timekeeper")
function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) {
const common = a.filter((ea) => b.includes(ea))
@@ -88,7 +88,7 @@ describe("BatchCluster", function () {
results.forEach((result, index) => {
if (!result.startsWith(ErrorPrefix)) {
expect(result).to.eql("ABC " + index)
- expect(dataResults).to.include(result)
+ expect(dataResults.toString()).to.include(result)
}
})
}
@@ -209,7 +209,7 @@ describe("BatchCluster", function () {
bc.on("taskResolved", (task: Task) => {
const runtimeMs = task.runtimeMs
expect(runtimeMs).to.not.eql(undefined)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+
events.runtimeMs.push(runtimeMs!)
})
@@ -232,7 +232,7 @@ describe("BatchCluster", function () {
newlines.push("crlf")
}
- it("supports .off()", async () => {
+ it("supports .off()", () => {
const emitTimes: number[] = []
const bc = new BatchCluster({ ...DefaultTestOptions, processFactory })
const listener = () => emitTimes.push(Date.now())
@@ -444,7 +444,7 @@ describe("BatchCluster", function () {
expect(bc.pids()).to.not.include.members(pids)
expect(bc.meanTasksPerProc).to.be.within(
- 0.25, // because flaky
+ 0.15, // because flaky (macOS on GHA resulted in 0.21)
opts.maxTasksPerProcess,
)
expect(bc.pids().length).to.be.lte(maxProcs)
@@ -477,13 +477,30 @@ describe("BatchCluster", function () {
times(maxProcs * 2, () =>
bc
.enqueueTask(new Task("nonsense", parser))
- .catch((err) => err),
+ .catch((err: unknown) => err),
),
)
- filterInPlace(
- errorResults,
- (ea) => ea != null && !String(ea).includes("EUNLUCKY"),
- )
+ function convertErrorToString(ea: unknown): string {
+ if (ea == null) return "[unknown]"
+ if (ea instanceof Error) return ea.message
+ if (typeof ea === "string") return ea
+ if (typeof ea === "object") {
+ try {
+ return JSON.stringify(ea)
+ } catch {
+ return "[object Object]"
+ }
+ }
+ if (typeof ea === "number" || typeof ea === "boolean") {
+ return String(ea)
+ }
+ return "[unknown]"
+ }
+
+ filterInPlace(errorResults, (ea) => {
+ const errorStr = convertErrorToString(ea)
+ return !errorStr.includes("EUNLUCKY")
+ })
if (
maxProcs === 1 &&
ignoreExit === false &&
@@ -634,7 +651,9 @@ describe("BatchCluster", function () {
const task = new Task("sleep " + sleepTimeMs, parser)
const resultP = bc.enqueueTask(task)
expect(bc.isIdle).to.eql(false)
- const result = JSON.parse(await resultP)
+ const result = JSON.parse(await resultP) as {
+ pid: number
+ } & Record
const end = Date.now()
return { i, start, end, ...result }
}),
@@ -642,7 +661,7 @@ describe("BatchCluster", function () {
const pid2count = new Map()
tasks.forEach((ea) => {
const pid = ea.pid
- const count = orElse(pid2count.get(pid), 0)
+ const count = pid2count.get(pid) ?? 0
pid2count.set(pid, count + 1)
})
expect(bc.isIdle).to.eql(true)
@@ -727,7 +746,7 @@ describe("BatchCluster", function () {
function stats() {
// we don't want msBeforeNextSpawn because it'll be wiggly and we're not
// freezing time (here)
- return omit(bc.stats(), "msBeforeNextSpawn")
+ return omit(bc.stats(), "msBeforeNextSpawn") as Record
}
it("shut down rejects long-running pending tasks", async () => {
@@ -769,7 +788,7 @@ describe("BatchCluster", function () {
ended: false,
})
- t.catch((err) => (caught = err))
+ t.catch((err: unknown) => (caught = err))
await delay(2)
expect(stats()).to.eql({
@@ -785,7 +804,7 @@ describe("BatchCluster", function () {
ended: false,
})
- let caught: any
+ let caught: unknown
expect(bc.isIdle).to.eql(false)
await bc.end(false) // not graceful just to shut down faster
@@ -803,7 +822,9 @@ describe("BatchCluster", function () {
})
expect(bc.isIdle).to.eql(true)
- expect(caught?.message).to.include("end() called before task completed")
+ expect((caught as Error)?.message).to.include(
+ "Process terminated before task completed",
+ )
expect(unhandledRejections).to.eql([])
})
})
@@ -901,11 +922,20 @@ describe("BatchCluster", function () {
describe("maxProcAgeMillis (recycling procs)", () => {
let bc: BatchCluster
+ let clock: FakeTimers.InstalledClock
+
+ beforeEach(() => {
+ clock = FakeTimers.install({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ })
+ })
afterEach(() => {
- tk.reset()
+ clock.uninstall()
return shutdown(bc)
})
+
for (const { maxProcAgeMillis, ctx, exp } of [
{
maxProcAgeMillis: 0,
@@ -929,8 +959,6 @@ describe("BatchCluster", function () {
it("(" + maxProcAgeMillis + "): " + ctx, async function () {
// TODO: look into why this fails in CI on windows
if (isWin && isCI) return this.skip()
- const start = Date.now()
- tk.freeze(start)
setFailratePct(0)
bc = listen(
@@ -944,7 +972,7 @@ describe("BatchCluster", function () {
)
assertExpectedResults(await Promise.all(runTasks(bc, 2)))
const pidsBefore = bc.pids()
- tk.freeze(start + 7000)
+ clock.tick(7000)
assertExpectedResults(await Promise.all(runTasks(bc, 2)))
const pidsAfter = bc.pids()
console.dir({ maxProcAgeMillis, pidsBefore, pidsAfter })
diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts
index 306e6a0..50705fc 100644
--- a/src/BatchCluster.ts
+++ b/src/BatchCluster.ts
@@ -1,31 +1,27 @@
-import child_process from "node:child_process"
import events from "node:events"
import process from "node:process"
import timers from "node:timers"
-import { count, filterInPlace } from "./Array"
import {
BatchClusterEmitter,
BatchClusterEvents,
ChildEndReason,
TypedEventEmitter,
} from "./BatchClusterEmitter"
-import {
- AllOpts,
- BatchClusterOptions,
- verifyOptions,
-} from "./BatchClusterOptions"
-import { BatchProcess, WhyNotHealthy, WhyNotReady } from "./BatchProcess"
+import { BatchClusterOptions } from "./BatchClusterOptions"
+import type { BatchClusterStats } from "./BatchClusterStats"
import { BatchProcessOptions } from "./BatchProcessOptions"
+import type { ChildProcessFactory } from "./ChildProcessFactory"
+import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"
import { Deferred } from "./Deferred"
-import { asError } from "./Error"
import { Logger } from "./Logger"
-import { Mean } from "./Mean"
-import { fromEntries, map } from "./Object"
+import { verifyOptions } from "./OptionsVerifier"
import { Parser } from "./Parser"
-import { Rate } from "./Rate"
-import { toS } from "./String"
+import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator"
+import { ProcessPoolManager } from "./ProcessPoolManager"
+import { validateProcpsAvailable } from "./ProcpsChecker"
import { Task } from "./Task"
-import { Timeout, thenOrTimeout } from "./Timeout"
+import { TaskQueueManager } from "./TaskQueueManager"
+import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"
export { BatchClusterOptions } from "./BatchClusterOptions"
export { BatchProcess } from "./BatchProcess"
@@ -33,37 +29,22 @@ export { Deferred } from "./Deferred"
export * from "./Logger"
export { SimpleParser } from "./Parser"
export { kill, pidExists, pids } from "./Pids"
+export { ProcpsMissingError } from "./ProcpsChecker"
export { Rate } from "./Rate"
export { Task } from "./Task"
export type {
BatchClusterEmitter,
BatchClusterEvents,
+ BatchClusterStats,
BatchProcessOptions,
ChildEndReason as ChildExitReason,
+ ChildProcessFactory,
Parser,
TypedEventEmitter,
WhyNotHealthy,
WhyNotReady,
}
-/**
- * These are required parameters for a given BatchCluster.
- */
-export interface ChildProcessFactory {
- /**
- * Expected to be a simple call to execFile. Platform-specific code is the
- * responsibility of this thunk. Error handlers will be registered as
- * appropriate.
- *
- * If this function throws an error or rejects the promise _after_ you've
- * spawned a child process, **the child process may continue to run** and leak
- * system resources.
- */
- readonly processFactory: () =>
- | child_process.ChildProcess
- | Promise
-}
-
/**
* BatchCluster instances manage 0 or more homogeneous child processes, and
* provide the main interface for enqueuing `Task`s via `enqueueTask`.
@@ -75,20 +56,14 @@ export interface ChildProcessFactory {
* child tasks can be verified and shut down.
*/
export class BatchCluster {
- readonly #tasksPerProc = new Mean()
readonly #logger: () => Logger
- readonly options: AllOpts
- readonly #procs: BatchProcess[] = []
+ readonly options: CombinedBatchProcessOptions
+ readonly #processPool: ProcessPoolManager
+ readonly #taskQueue: TaskQueueManager
+ readonly #eventCoordinator: BatchClusterEventCoordinator
#onIdleRequested = false
- #nextSpawnTime = 0
- #lastPidsCheckTime = 0
- readonly #tasks: Task[] = []
#onIdleInterval: NodeJS.Timeout | undefined
- readonly #startErrorRate = new Rate()
- #spawnedProcs = 0
#endPromise?: Deferred
- #internalErrorCount = 0
- readonly #childEndCounts = new Map()
readonly emitter = new events.EventEmitter() as BatchClusterEmitter
constructor(
@@ -96,54 +71,29 @@ export class BatchCluster {
BatchProcessOptions &
ChildProcessFactory,
) {
+ // Validate that required process listing commands are available
+ validateProcpsAvailable()
+
this.options = verifyOptions({ ...opts, observer: this.emitter })
+ this.#logger = this.options.logger
- this.on("childEnd", (bp, why) => {
- this.#tasksPerProc.push(bp.taskCount)
- this.#childEndCounts.set(why, (this.#childEndCounts.get(why) ?? 0) + 1)
- this.#onIdleLater()
- })
-
- this.on("internalError", (error) => {
- this.#logger().error("BatchCluster: INTERNAL ERROR: " + error)
- this.#internalErrorCount++
- })
-
- this.on("noTaskData", (stdout, stderr, proc) => {
- this.#logger().warn(
- "BatchCluster: child process emitted data with no current task. Consider setting streamFlushMillis to a higher value.",
- {
- streamFlushMillis: this.options.streamFlushMillis,
- stdout: toS(stdout),
- stderr: toS(stderr),
- proc_pid: proc?.pid,
- },
- )
- this.#internalErrorCount++
- })
-
- this.on("startError", (error) => {
- this.#logger().warn("BatchCluster.onStartError(): " + error)
- this.#startErrorRate.onEvent()
- if (
- this.options.maxReasonableProcessFailuresPerMinute > 0 &&
- this.#startErrorRate.eventsPerMinute >
- this.options.maxReasonableProcessFailuresPerMinute
- ) {
- this.emitter.emit(
- "fatalError",
- new Error(
- error +
- "(start errors/min: " +
- this.#startErrorRate.eventsPerMinute.toFixed(2) +
- ")",
- ),
- )
- this.end()
- } else {
- this.#onIdleLater()
- }
- })
+ // Initialize the managers
+ this.#processPool = new ProcessPoolManager(this.options, this.emitter, () =>
+ this.#onIdleLater(),
+ )
+ this.#taskQueue = new TaskQueueManager(this.#logger, this.emitter)
+
+ // Initialize event coordinator to handle all event processing
+ this.#eventCoordinator = new BatchClusterEventCoordinator(
+ this.emitter,
+ {
+ streamFlushMillis: this.options.streamFlushMillis,
+ maxReasonableProcessFailuresPerMinute: this.options.maxReasonableProcessFailuresPerMinute,
+ logger: this.#logger,
+ },
+ () => this.#onIdleLater(),
+ () => void this.end(),
+ )
if (this.options.onIdleIntervalMillis > 0) {
this.#onIdleInterval = timers.setInterval(
@@ -169,8 +119,12 @@ export class BatchCluster {
*/
readonly off = this.emitter.off.bind(this.emitter)
- readonly #beforeExitListener = () => this.end(true)
- readonly #exitListener = () => this.end(false)
+ readonly #beforeExitListener = () => {
+ void this.end(true)
+ }
+ readonly #exitListener = () => {
+ void this.end(false)
+ }
get ended(): boolean {
return this.#endPromise != null
@@ -187,7 +141,8 @@ export class BatchCluster {
if (this.#endPromise == null) {
this.emitter.emit("beforeEnd")
- map(this.#onIdleInterval, timers.clearInterval)
+ if (this.#onIdleInterval != null)
+ timers.clearInterval(this.#onIdleInterval)
this.#onIdleInterval = undefined
process.removeListener("beforeExit", this.#beforeExitListener)
process.removeListener("exit", this.#exitListener)
@@ -213,7 +168,7 @@ export class BatchCluster {
new Error("BatchCluster has ended, cannot enqueue " + task.command),
)
}
- this.#tasks.push(task)
+ this.#taskQueue.enqueue(task as Task)
// Run #onIdle now (not later), to make sure the task gets enqueued asap if
// possible
@@ -236,70 +191,60 @@ export class BatchCluster {
* @return the number of pending tasks
*/
get pendingTaskCount(): number {
- return this.#tasks.length
+ return this.#taskQueue.pendingTaskCount
}
/**
* @returns {number} the mean number of tasks completed by child processes
*/
get meanTasksPerProc(): number {
- return this.#tasksPerProc.mean
+ return this.#eventCoordinator.meanTasksPerProc
}
/**
* @return the total number of child processes created by this instance
*/
get spawnedProcCount(): number {
- return this.#spawnedProcs
+ return this.#processPool.spawnedProcCount
}
/**
* @return the current number of spawned child processes. Some (or all) may be idle.
*/
get procCount(): number {
- return this.#procs.length
+ return this.#processPool.processCount
}
/**
* @return the current number of child processes currently servicing tasks
*/
get busyProcCount(): number {
- return count(
- this.#procs,
- // don't count procs that are starting up as "busy":
- (ea) => !ea.starting && !ea.ending && !ea.idle,
- )
+ return this.#processPool.busyProcCount
}
get startingProcCount(): number {
- return count(
- this.#procs,
- // don't count procs that are starting up as "busy":
- (ea) => ea.starting && !ea.ending,
- )
+ return this.#processPool.startingProcCount
}
/**
* @return the current pending Tasks (mostly for testing)
*/
get pendingTasks() {
- return this.#tasks
+ return this.#taskQueue.pendingTasks
}
/**
* @return the current running Tasks (mostly for testing)
*/
get currentTasks(): Task[] {
- return this.#procs
- .map((ea) => ea.currentTask)
- .filter((ea) => ea != null) as Task[]
+ return this.#processPool.currentTasks()
}
/**
* For integration tests:
*/
get internalErrorCount(): number {
- return this.#internalErrorCount
+ return this.#eventCoordinator.internalErrorCount
}
/**
@@ -308,28 +253,21 @@ export class BatchCluster {
* @return the spawned PIDs that are still in the process table.
*/
pids(): number[] {
- const arr: number[] = []
- for (const proc of [...this.#procs]) {
- if (proc != null && proc.running()) {
- arr.push(proc.pid)
- }
- }
- return arr
+ return this.#processPool.pids()
}
/**
* For diagnostics. Contents may change.
*/
- stats() {
- const readyProcCount = count(this.#procs, (ea) => ea.ready)
+ stats(): BatchClusterStats {
return {
- pendingTaskCount: this.#tasks.length,
- currentProcCount: this.#procs.length,
- readyProcCount,
+ pendingTaskCount: this.pendingTaskCount,
+ currentProcCount: this.procCount,
+ readyProcCount: this.#processPool.readyProcCount,
maxProcCount: this.options.maxProcs,
- internalErrorCount: this.#internalErrorCount,
- startErrorRatePerMinute: this.#startErrorRate.eventsPerMinute,
- msBeforeNextSpawn: Math.max(0, this.#nextSpawnTime - Date.now()),
+ internalErrorCount: this.#eventCoordinator.internalErrorCount,
+ startErrorRatePerMinute: this.#eventCoordinator.startErrorRatePerMinute,
+ msBeforeNextSpawn: this.#processPool.msBeforeNextSpawn,
spawnedProcCount: this.spawnedProcCount,
childEndCounts: this.childEndCounts,
ending: this.#endPromise != null,
@@ -341,27 +279,19 @@ export class BatchCluster {
* Get ended process counts (used for tests)
*/
countEndedChildProcs(why: ChildEndReason): number {
- return this.#childEndCounts.get(why) ?? 0
+ return this.#eventCoordinator.countEndedChildProcs(why)
}
- get childEndCounts(): { [key in NonNullable]: number } {
- return fromEntries([...this.#childEndCounts.entries()])
+ get childEndCounts(): Record, number> {
+ return this.#eventCoordinator.childEndCounts
}
/**
* Shut down any currently-running child processes. New child processes will
* be started automatically to handle new tasks.
*/
- async closeChildProcesses(gracefully = true) {
- const procs = [...this.#procs]
- this.#procs.length = 0
- await Promise.all(
- procs.map((proc) =>
- proc
- .end(gracefully, "ending")
- .catch((err) => this.emitter.emit("endError", asError(err), proc)),
- ),
- )
+ async closeChildProcesses(gracefully = true): Promise {
+ return this.#processPool.closeChildProcesses(gracefully)
}
/**
@@ -370,7 +300,7 @@ export class BatchCluster {
* completed.
*/
setMaxProcs(maxProcs: number) {
- this.options.maxProcs = maxProcs
+ this.#processPool.setMaxProcs(maxProcs)
// we may now be able to handle an enqueued task. Vacuum pids and see:
this.#onIdleLater()
}
@@ -385,22 +315,11 @@ export class BatchCluster {
// NOT ASYNC: updates internal state:
#onIdle() {
this.#onIdleRequested = false
- this.vacuumProcs()
+ void this.vacuumProcs()
while (this.#execNextTask()) {
//
}
- this.#maybeSpawnProcs()
- }
-
- #maybeCheckPids() {
- if (
- this.options.cleanupChildProcs &&
- this.options.pidCheckIntervalMillis > 0 &&
- this.#lastPidsCheckTime + this.options.pidCheckIntervalMillis < Date.now()
- ) {
- this.#lastPidsCheckTime = Date.now()
- void this.pids()
- }
+ void this.#maybeSpawnProcs()
}
/**
@@ -411,28 +330,7 @@ export class BatchCluster {
*/
// NOT ASYNC: updates internal state. only exported for tests.
vacuumProcs() {
- this.#maybeCheckPids()
- const endPromises: Promise[] = []
- let pidsToReap = Math.max(0, this.#procs.length - this.options.maxProcs)
- filterInPlace(this.#procs, (proc) => {
- // Only check `.idle` (not `.ready`) procs. We don't want to reap busy
- // procs unless we're ending, and unhealthy procs (that we want to reap)
- // won't be `.ready`.
- if (proc.idle) {
- // don't reap more than pidsToReap pids. We can't use #procs.length
- // within filterInPlace because #procs.length only changes at iteration
- // completion: the prior impl resulted in all idle pids getting reaped
- // when maxProcs was reduced.
- const why = proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null)
- if (why != null) {
- endPromises.push(proc.end(true, why))
- return false
- }
- proc.maybeRunHealthcheck()
- }
- return true
- })
- return Promise.all(endPromises)
+ return this.#processPool.vacuumProcs()
}
/**
@@ -440,133 +338,15 @@ export class BatchCluster {
* @return true iff a task was submitted to a child process
*/
#execNextTask(retries = 1): boolean {
- if (this.#tasks.length === 0 || this.ended || retries < 0) return false
- const readyProc = this.#procs.find((ea) => ea.ready)
- // no procs are idle and healthy :(
- if (readyProc == null) {
- return false
- }
-
- const task = this.#tasks.shift()
- if (task == null) {
- this.emitter.emit("internalError", new Error("unexpected null task"))
- return false
- }
-
- const submitted = readyProc.execTask(task)
- if (!submitted) {
- // This isn't an internal error: the proc may have needed to run a health
- // check. Let's reschedule the task and try again:
- this.#tasks.push(task)
- // We don't want to return false here (it'll stop the onIdle loop) unless
- // we actually can't submit the task:
- return this.#execNextTask(retries--)
- }
- this.#logger().trace("BatchCluster.#execNextTask(): submitted task", {
- child_pid: readyProc.pid,
- task,
- })
-
- return submitted
- }
-
- #maxSpawnDelay() {
- // 10s delay is certainly long enough for .spawn() to return, even on a
- // loaded windows machine.
- return Math.max(10_000, this.options.spawnTimeoutMillis)
- }
-
- #procsToSpawn() {
- const remainingCapacity = this.options.maxProcs - this.#procs.length
-
- // take into account starting procs, so one task doesn't result in multiple
- // processes being spawned:
- const requestedCapacity = this.#tasks.length - this.startingProcCount
-
- const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity))
-
- return this.options.minDelayBetweenSpawnMillis === 0
- ? // we can spin up multiple processes in parallel.
- atLeast0
- : // Don't spin up more than 1:
- Math.min(1, atLeast0)
+ if (this.ended) return false
+ const readyProc = this.#processPool.findReadyProcess()
+ return this.#taskQueue.tryAssignNextTask(readyProc, retries)
}
async #maybeSpawnProcs() {
- let procsToSpawn = this.#procsToSpawn()
-
- if (this.ended || this.#nextSpawnTime > Date.now() || procsToSpawn === 0) {
- return
- }
-
- // prevent concurrent runs:
- this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay()
-
- for (let i = 0; i < procsToSpawn; i++) {
- if (this.ended) {
- break
- }
-
- // Kick the lock down the road:
- this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay()
- this.#spawnedProcs++
-
- try {
- const proc = this.#spawnNewProc()
- const result = await thenOrTimeout(
- proc,
- this.options.spawnTimeoutMillis,
- )
- if (result === Timeout) {
- void proc
- .then((bp) => {
- void bp.end(false, "startError")
- this.emitter.emit(
- "startError",
- asError(
- "Failed to spawn process in " +
- this.options.spawnTimeoutMillis +
- "ms",
- ),
- bp,
- )
- })
- .catch((err) => {
- // this should only happen if the processFactory throws a
- // rejection:
- this.emitter.emit("startError", asError(err))
- })
- } else {
- this.#logger().debug(
- "BatchCluster.#maybeSpawnProcs() started healthy child process",
- { pid: result.pid },
- )
- }
-
- // tasks may have been popped off or setMaxProcs may have reduced
- // maxProcs. Do this at the end so the for loop ends properly.
- procsToSpawn = Math.min(this.#procsToSpawn(), procsToSpawn)
- } catch (err) {
- this.emitter.emit("startError", asError(err))
- }
- }
-
- // YAY WE MADE IT.
- // Only let more children get spawned after minDelay:
- const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis)
- this.#nextSpawnTime = Date.now() + delay
-
- // And schedule #onIdle for that time:
- timers.setTimeout(this.#onIdleLater, delay).unref()
- }
-
- // must only be called by this.#maybeSpawnProcs()
- async #spawnNewProc() {
- // no matter how long it takes to spawn, always push the result into #procs
- // so we don't leak child processes:
- const proc = await this.options.processFactory()
- const result = new BatchProcess(proc, this.options, this.#onIdleLater)
- this.#procs.push(result)
- return result
+ return this.#processPool.maybeSpawnProcs(
+ this.#taskQueue.pendingTaskCount,
+ this.ended,
+ )
}
}
diff --git a/src/BatchClusterEmitter.ts b/src/BatchClusterEmitter.ts
index ec186ee..b19668f 100644
--- a/src/BatchClusterEmitter.ts
+++ b/src/BatchClusterEmitter.ts
@@ -1,5 +1,6 @@
-import { BatchProcess, WhyNotHealthy } from "./BatchProcess"
+import { BatchProcess } from "./BatchProcess"
import { Task } from "./Task"
+import { WhyNotHealthy } from "./WhyNotHealthy"
type Args = T extends (...args: infer A) => void ? A : never
@@ -23,7 +24,7 @@ export interface TypedEventEmitter {
): this
emit(eventName: E, ...args: Args): boolean
- // eslint-disable-next-line @typescript-eslint/ban-types
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
listeners(event: E): Function[]
removeAllListeners(eventName?: keyof T): this
@@ -70,24 +71,28 @@ export interface BatchClusterEvents {
*/
taskData: (
data: Buffer | string,
- task: Task | undefined,
+ task: Task | undefined,
proc: BatchProcess,
) => void
/**
* Emitted when a task has been resolved
*/
- taskResolved: (task: Task, proc: BatchProcess) => void
+ taskResolved: (task: Task, proc: BatchProcess) => void
/**
* Emitted when a task times out. Note that a `taskError` event always succeeds these events.
*/
- taskTimeout: (timeoutMs: number, task: Task, proc: BatchProcess) => void
+ taskTimeout: (
+ timeoutMs: number,
+ task: Task,
+ proc: BatchProcess,
+ ) => void
/**
* Emitted when a task has an error
*/
- taskError: (error: Error, task: Task, proc: BatchProcess) => void
+ taskError: (error: Error, task: Task, proc: BatchProcess) => void
/**
* Emitted when child processes write to stdout or stderr without a current
diff --git a/src/BatchClusterEventCoordinator.spec.ts b/src/BatchClusterEventCoordinator.spec.ts
new file mode 100644
index 0000000..07bac7d
--- /dev/null
+++ b/src/BatchClusterEventCoordinator.spec.ts
@@ -0,0 +1,361 @@
+import events from "node:events"
+import { expect } from "./_chai.spec"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import {
+ BatchClusterEventCoordinator,
+ EventCoordinatorOptions,
+} from "./BatchClusterEventCoordinator"
+import { BatchProcess } from "./BatchProcess"
+import { logger } from "./Logger"
+import { Task } from "./Task"
+
+describe("BatchClusterEventCoordinator", function () {
+ let eventCoordinator: BatchClusterEventCoordinator
+ let emitter: BatchClusterEmitter
+ let onIdleCalledCount = 0
+ let endClusterCalledCount = 0
+
+ const options: EventCoordinatorOptions = {
+ streamFlushMillis: 100,
+ maxReasonableProcessFailuresPerMinute: 5,
+ logger,
+ }
+
+ const onIdleLater = () => {
+ onIdleCalledCount++
+ }
+
+ const endCluster = () => {
+ endClusterCalledCount++
+ }
+
+ beforeEach(function () {
+ emitter = new events.EventEmitter() as BatchClusterEmitter
+ eventCoordinator = new BatchClusterEventCoordinator(
+ emitter,
+ options,
+ onIdleLater,
+ endCluster,
+ )
+ onIdleCalledCount = 0
+ endClusterCalledCount = 0
+ })
+
+ describe("initial state", function () {
+ it("should start with clean statistics", function () {
+ expect(eventCoordinator.meanTasksPerProc).to.eql(0)
+ expect(eventCoordinator.internalErrorCount).to.eql(0)
+ expect(eventCoordinator.startErrorRatePerMinute).to.eql(0)
+ expect(eventCoordinator.countEndedChildProcs("ended")).to.eql(0)
+ expect(eventCoordinator.childEndCounts).to.eql({})
+ })
+
+ it("should provide clean event statistics", function () {
+ const stats = eventCoordinator.getEventStats()
+ expect(stats.meanTasksPerProc).to.eql(0)
+ expect(stats.internalErrorCount).to.eql(0)
+ expect(stats.startErrorRatePerMinute).to.eql(0)
+ expect(stats.totalChildEndEvents).to.eql(0)
+ expect(stats.childEndReasons).to.eql([])
+ })
+ })
+
+ describe("childEnd event handling", function () {
+ it("should handle childEnd events and update statistics", function () {
+ const mockProcess = {
+ taskCount: 5,
+ pid: 12345,
+ } as BatchProcess
+
+ // Emit childEnd event
+ emitter.emit("childEnd", mockProcess, "worn")
+
+ expect(eventCoordinator.meanTasksPerProc).to.eql(5)
+ expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(1)
+ expect(eventCoordinator.childEndCounts.worn).to.eql(1)
+ expect(onIdleCalledCount).to.eql(1)
+ })
+
+ it("should track multiple childEnd events", function () {
+ const mockProcess1 = { taskCount: 3 } as BatchProcess
+ const mockProcess2 = { taskCount: 7 } as BatchProcess
+ const mockProcess3 = { taskCount: 5 } as BatchProcess
+
+ emitter.emit("childEnd", mockProcess1, "worn")
+ emitter.emit("childEnd", mockProcess2, "old")
+ emitter.emit("childEnd", mockProcess3, "worn")
+
+ expect(eventCoordinator.meanTasksPerProc).to.eql(5) // (3+7+5)/3
+ expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2)
+ expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1)
+ expect(eventCoordinator.childEndCounts.worn).to.eql(2)
+ expect(eventCoordinator.childEndCounts.old).to.eql(1)
+ expect(onIdleCalledCount).to.eql(3)
+ })
+ })
+
+ describe("internalError event handling", function () {
+ it("should handle internalError events and increment counter", function () {
+ const error = new Error("Internal error occurred")
+
+ emitter.emit("internalError", error)
+
+ expect(eventCoordinator.internalErrorCount).to.eql(1)
+ })
+
+ it("should handle multiple internalError events", function () {
+ emitter.emit("internalError", new Error("Error 1"))
+ emitter.emit("internalError", new Error("Error 2"))
+ emitter.emit("internalError", new Error("Error 3"))
+
+ expect(eventCoordinator.internalErrorCount).to.eql(3)
+ })
+ })
+
+ describe("noTaskData event handling", function () {
+ it("should handle noTaskData events and increment internal error count", function () {
+ const mockProcess = { pid: 12345 } as BatchProcess
+
+ emitter.emit("noTaskData", "some stdout", "some stderr", mockProcess)
+
+ expect(eventCoordinator.internalErrorCount).to.eql(1)
+ })
+
+ it("should handle noTaskData with null data", function () {
+ const mockProcess = { pid: 12345 } as BatchProcess
+
+ emitter.emit("noTaskData", null, null, mockProcess)
+
+ expect(eventCoordinator.internalErrorCount).to.eql(1)
+ })
+
+ it("should handle noTaskData with buffer data", function () {
+ const mockProcess = { pid: 12345 } as BatchProcess
+ const bufferData = Buffer.from("test data")
+
+ emitter.emit("noTaskData", bufferData, null, mockProcess)
+
+ expect(eventCoordinator.internalErrorCount).to.eql(1)
+ })
+ })
+
+ describe("startError event handling", function () {
+ it("should handle startError events without triggering fatal error", function () {
+ const error = new Error("Start error")
+
+ emitter.emit("startError", error)
+
+ // Rate might be 0 initially due to warmup period
+ expect(eventCoordinator.startErrorRatePerMinute).to.be.greaterThanOrEqual(
+ 0,
+ )
+ expect(endClusterCalledCount).to.eql(0)
+ expect(onIdleCalledCount).to.eql(1)
+ })
+
+ it("should have logic to trigger fatal error based on rate", function () {
+ // This test verifies the logic exists, but doesn't test timing-dependent rate calculation
+ // which depends on the Rate class's warmup period
+
+ const testOptions: EventCoordinatorOptions = {
+ ...options,
+ maxReasonableProcessFailuresPerMinute: 5,
+ }
+
+ const testCoordinator = new BatchClusterEventCoordinator(
+ emitter,
+ testOptions,
+ onIdleLater,
+ endCluster,
+ )
+
+ // Verify that start error rate tracking is working
+ emitter.emit("startError", new Error("Test error"))
+ expect(testCoordinator.startErrorRatePerMinute).to.be.greaterThanOrEqual(
+ 0,
+ )
+
+ // The actual fatal error triggering depends on Rate class timing
+ // which is tested in the Rate class's own tests
+ })
+
+ it("should not trigger fatal error when rate limit is disabled", function () {
+ const noLimitOptions: EventCoordinatorOptions = {
+ ...options,
+ maxReasonableProcessFailuresPerMinute: 0, // Disabled
+ }
+
+ new BatchClusterEventCoordinator(
+ emitter,
+ noLimitOptions,
+ onIdleLater,
+ endCluster,
+ )
+
+ let fatalErrorEmitted = false
+ emitter.on("fatalError", () => {
+ fatalErrorEmitted = true
+ })
+
+ // Emit many start errors
+ for (let i = 0; i < 20; i++) {
+ emitter.emit("startError", new Error(`Start error ${i}`))
+ }
+
+ expect(fatalErrorEmitted).to.be.false
+ expect(endClusterCalledCount).to.eql(0)
+ })
+ })
+
+ describe("event access", function () {
+ it("should provide access to the underlying emitter", function () {
+ expect(eventCoordinator.events).to.equal(emitter)
+ })
+
+ it("should allow direct event emission through events property", function () {
+ let eventReceived = false
+ let receivedData: any
+
+ emitter.on("taskData", (data, task, proc) => {
+ eventReceived = true
+ receivedData = { data, task, proc }
+ })
+
+ const mockTask = {} as Task
+ const mockProcess = {} as BatchProcess
+ const testData = "test data"
+
+ const result = eventCoordinator.events.emit(
+ "taskData",
+ testData,
+ mockTask,
+ mockProcess,
+ )
+
+ expect(result).to.be.true
+ expect(eventReceived).to.be.true
+ expect(receivedData.data).to.eql(testData)
+ expect(receivedData.task).to.eql(mockTask)
+ expect(receivedData.proc).to.eql(mockProcess)
+ })
+
+ it("should allow direct event listener management through events property", function () {
+ let eventReceived = false
+
+ const listener = () => {
+ eventReceived = true
+ }
+
+ eventCoordinator.events.on("beforeEnd", listener)
+ emitter.emit("beforeEnd")
+ expect(eventReceived).to.be.true
+
+ eventReceived = false
+ eventCoordinator.events.off("beforeEnd", listener)
+ emitter.emit("beforeEnd")
+ expect(eventReceived).to.be.false
+ })
+ })
+
+ describe("statistics and monitoring", function () {
+ beforeEach(function () {
+ // Set up some test data
+ const mockProcess1 = { taskCount: 10 } as BatchProcess
+ const mockProcess2 = { taskCount: 20 } as BatchProcess
+
+ emitter.emit("childEnd", mockProcess1, "worn")
+ emitter.emit("childEnd", mockProcess2, "old")
+ emitter.emit("internalError", new Error("Test error"))
+ emitter.emit("startError", new Error("Start error"))
+ })
+
+ it("should provide comprehensive event statistics", function () {
+ const stats = eventCoordinator.getEventStats()
+
+ expect(stats.meanTasksPerProc).to.eql(15) // (10+20)/2
+ expect(stats.internalErrorCount).to.eql(1)
+ expect(stats.startErrorRatePerMinute).to.be.greaterThanOrEqual(0) // Rate might be 0 due to warmup
+ expect(stats.totalChildEndEvents).to.eql(2)
+ expect(stats.childEndReasons).to.include("worn")
+ expect(stats.childEndReasons).to.include("old")
+ })
+
+ it("should reset statistics correctly", function () {
+ // Verify we have some data
+ expect(eventCoordinator.meanTasksPerProc).to.eql(15)
+ expect(eventCoordinator.internalErrorCount).to.eql(1)
+
+ eventCoordinator.resetStats()
+
+ // Verify everything is reset
+ expect(eventCoordinator.meanTasksPerProc).to.eql(0)
+ expect(eventCoordinator.internalErrorCount).to.eql(0)
+ expect(eventCoordinator.startErrorRatePerMinute).to.eql(0)
+ expect(eventCoordinator.childEndCounts).to.eql({})
+
+ const stats = eventCoordinator.getEventStats()
+ expect(stats.totalChildEndEvents).to.eql(0)
+ expect(stats.childEndReasons).to.eql([])
+ })
+
+ it("should track child end counts accurately", function () {
+ // Add more events of different types
+ const mockProcess3 = { taskCount: 5 } as BatchProcess
+ const mockProcess4 = { taskCount: 8 } as BatchProcess
+
+ emitter.emit("childEnd", mockProcess3, "worn") // Second worn
+ emitter.emit("childEnd", mockProcess4, "broken") // New type
+
+ expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2)
+ expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1)
+ expect(eventCoordinator.countEndedChildProcs("broken")).to.eql(1)
+ expect(eventCoordinator.countEndedChildProcs("timeout")).to.eql(0)
+
+ const childEndCounts = eventCoordinator.childEndCounts
+ expect(childEndCounts.worn).to.eql(2)
+ expect(childEndCounts.old).to.eql(1)
+ expect(childEndCounts.broken).to.eql(1)
+ })
+ })
+
+ describe("callback integration", function () {
+ it("should call onIdleLater for appropriate events", function () {
+ const initialCount = onIdleCalledCount
+
+ // Events that should trigger onIdleLater
+ emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn")
+ emitter.emit("startError", new Error("Start error"))
+
+ expect(onIdleCalledCount).to.eql(initialCount + 2)
+ })
+
+ it("should have callback integration for endCluster", function () {
+ // This test verifies that the endCluster callback is properly integrated
+ // The actual triggering depends on Rate class timing which is complex to test
+
+ const testCoordinator = new BatchClusterEventCoordinator(
+ emitter,
+ options,
+ onIdleLater,
+ endCluster,
+ )
+
+ // Verify the coordinator is set up and callbacks are connected
+ expect(testCoordinator.events).to.equal(emitter)
+
+ // The endCluster callback integration is verified through the logic
+ // The actual rate-based triggering is tested in integration scenarios
+ })
+
+ it("should not call endCluster for non-fatal events", function () {
+ const initialCount = endClusterCalledCount
+
+ // Events that should not trigger endCluster
+ emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn")
+ emitter.emit("internalError", new Error("Internal error"))
+ emitter.emit("noTaskData", "data", null, {} as BatchProcess)
+
+ expect(endClusterCalledCount).to.eql(initialCount)
+ })
+ })
+})
diff --git a/src/BatchClusterEventCoordinator.ts b/src/BatchClusterEventCoordinator.ts
new file mode 100644
index 0000000..0e09989
--- /dev/null
+++ b/src/BatchClusterEventCoordinator.ts
@@ -0,0 +1,190 @@
+import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter"
+import { BatchProcess } from "./BatchProcess"
+import { Logger } from "./Logger"
+import { Mean } from "./Mean"
+import { Rate } from "./Rate"
+import { toS } from "./String"
+
+/**
+ * Configuration for event handling behavior
+ */
+export interface EventCoordinatorOptions {
+ readonly streamFlushMillis: number
+ readonly maxReasonableProcessFailuresPerMinute: number
+ readonly logger: () => Logger
+}
+
+/**
+ * Centralized coordinator for BatchCluster events.
+ * Handles event processing, statistics tracking, and automated responses to events.
+ */
+export class BatchClusterEventCoordinator {
+ readonly #logger: () => Logger
+ #tasksPerProc = new Mean()
+ #startErrorRate = new Rate()
+ readonly #childEndCounts = new Map()
+ #internalErrorCount = 0
+
+ constructor(
+ private readonly emitter: BatchClusterEmitter,
+ private readonly options: EventCoordinatorOptions,
+ private readonly onIdleLater: () => void,
+ private readonly endCluster: () => void,
+ ) {
+ this.#logger = options.logger
+ this.#setupEventHandlers()
+ }
+
+ /**
+ * Set up all event handlers for the BatchCluster
+ */
+ #setupEventHandlers(): void {
+ this.emitter.on("childEnd", (bp, why) => this.#handleChildEnd(bp, why))
+ this.emitter.on("internalError", (error) =>
+ this.#handleInternalError(error),
+ )
+ this.emitter.on("noTaskData", (stdout, stderr, proc) =>
+ this.#handleNoTaskData(stdout, stderr, proc),
+ )
+ this.emitter.on("startError", (error) => this.#handleStartError(error))
+ }
+
+ /**
+ * Handle child process end events
+ */
+ #handleChildEnd(process: BatchProcess, reason: ChildEndReason): void {
+ this.#tasksPerProc.push(process.taskCount)
+ this.#childEndCounts.set(
+ reason,
+ (this.#childEndCounts.get(reason) ?? 0) + 1,
+ )
+ this.onIdleLater()
+ }
+
+ /**
+ * Handle internal error events
+ */
+ #handleInternalError(error: Error): void {
+ this.#logger().error("BatchCluster: INTERNAL ERROR: " + String(error))
+ this.#internalErrorCount++
+ }
+
+ /**
+ * Handle no task data events (data received without current task)
+ */
+ #handleNoTaskData(
+ stdout: string | Buffer | null,
+ stderr: string | Buffer | null,
+ proc: BatchProcess,
+ ): void {
+ this.#logger().warn(
+ "BatchCluster: child process emitted data with no current task. Consider setting streamFlushMillis to a higher value.",
+ {
+ streamFlushMillis: this.options.streamFlushMillis,
+ stdout: toS(stdout),
+ stderr: toS(stderr),
+ proc_pid: proc?.pid,
+ },
+ )
+ this.#internalErrorCount++
+ }
+
+ /**
+ * Handle start error events
+ */
+ #handleStartError(error: Error): void {
+ this.#logger().warn("BatchCluster.onStartError(): " + String(error))
+ this.#startErrorRate.onEvent()
+
+ if (
+ this.options.maxReasonableProcessFailuresPerMinute > 0 &&
+ this.#startErrorRate.eventsPerMinute >
+ this.options.maxReasonableProcessFailuresPerMinute
+ ) {
+ this.emitter.emit(
+ "fatalError",
+ new Error(
+ String(error) +
+ "(start errors/min: " +
+ this.#startErrorRate.eventsPerMinute.toFixed(2) +
+ ")",
+ ),
+ )
+ this.endCluster()
+ } else {
+ this.onIdleLater()
+ }
+ }
+
+ /**
+ * Get the mean number of tasks completed by child processes
+ */
+ get meanTasksPerProc(): number {
+ const mean = this.#tasksPerProc.mean
+ return isNaN(mean) ? 0 : mean
+ }
+
+ /**
+ * Get internal error count
+ */
+ get internalErrorCount(): number {
+ return this.#internalErrorCount
+ }
+
+ /**
+ * Get start error rate per minute
+ */
+ get startErrorRatePerMinute(): number {
+ return this.#startErrorRate.eventsPerMinute
+ }
+
+ /**
+ * Get count of ended child processes by reason
+ */
+ countEndedChildProcs(reason: ChildEndReason): number {
+ return this.#childEndCounts.get(reason) ?? 0
+ }
+
+ /**
+ * Get all child end counts
+ */
+ get childEndCounts(): Record, number> {
+ return Object.fromEntries([...this.#childEndCounts.entries()]) as Record<
+ NonNullable,
+ number
+ >
+ }
+
+ /**
+ * Get event statistics for monitoring
+ */
+ getEventStats() {
+ return {
+ meanTasksPerProc: this.meanTasksPerProc,
+ internalErrorCount: this.internalErrorCount,
+ startErrorRatePerMinute: this.startErrorRatePerMinute,
+ totalChildEndEvents: [...this.#childEndCounts.values()].reduce(
+ (sum, count) => sum + count,
+ 0,
+ ),
+ childEndReasons: Object.keys(this.childEndCounts),
+ }
+ }
+
+ /**
+ * Reset event statistics (useful for testing)
+ */
+ resetStats(): void {
+ this.#tasksPerProc = new Mean()
+ this.#startErrorRate = new Rate()
+ this.#childEndCounts.clear()
+ this.#internalErrorCount = 0
+ }
+
+ /**
+ * Get the underlying emitter for direct event access
+ */
+ get events(): BatchClusterEmitter {
+ return this.emitter
+ }
+}
diff --git a/src/BatchClusterOptions.spec.ts b/src/BatchClusterOptions.spec.ts
index dfcfc50..31f1c8f 100644
--- a/src/BatchClusterOptions.spec.ts
+++ b/src/BatchClusterOptions.spec.ts
@@ -1,14 +1,14 @@
import { BatchCluster } from "./BatchCluster"
-import { verifyOptions } from "./BatchClusterOptions"
import { DefaultTestOptions } from "./DefaultTestOptions.spec"
+import { verifyOptions } from "./OptionsVerifier"
import { expect, processFactory } from "./_chai.spec"
describe("BatchClusterOptions", () => {
let bc: BatchCluster
afterEach(() => bc?.end(false))
describe("verifyOptions()", () => {
- function errToArr(err: any) {
- return err.toString().split(/\s*[:;]\s*/)
+ function errToArr(err: unknown): string[] {
+ return String(err).split(/\s*[:;]\s*/)
}
it("allows 0 maxProcAgeMillis", () => {
diff --git a/src/BatchClusterOptions.ts b/src/BatchClusterOptions.ts
index c13ca6e..64f9710 100644
--- a/src/BatchClusterOptions.ts
+++ b/src/BatchClusterOptions.ts
@@ -1,10 +1,6 @@
-import { ChildProcessFactory } from "./BatchCluster"
import { BatchClusterEmitter } from "./BatchClusterEmitter"
-import { BatchProcessOptions } from "./BatchProcessOptions"
-import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
import { logger, Logger } from "./Logger"
import { isMac, isWin } from "./Platform"
-import { blank, toS } from "./String"
export const secondMs = 1000
export const minuteMs = 60 * secondMs
@@ -181,83 +177,3 @@ export class BatchClusterOptions {
export interface WithObserver {
observer: BatchClusterEmitter
}
-
-export type AllOpts = BatchClusterOptions &
- InternalBatchProcessOptions &
- ChildProcessFactory &
- WithObserver
-
-function escapeRegExp(s: string) {
- return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&")
-}
-
-function toRe(s: string | RegExp) {
- return s instanceof RegExp
- ? s
- : new RegExp("(?:\\n|^)" + escapeRegExp(s) + "(?:\\r?\\n|$)")
-}
-
-export function verifyOptions(
- opts: Partial &
- BatchProcessOptions &
- ChildProcessFactory &
- WithObserver,
-): AllOpts {
- const result = {
- ...new BatchClusterOptions(),
- ...opts,
- passRE: toRe(opts.pass),
- failRE: toRe(opts.fail),
- }
-
- const errors: string[] = []
-
- function notBlank(fieldName: keyof AllOpts) {
- const v = toS(result[fieldName])
- if (blank(v)) {
- errors.push(fieldName + " must not be blank")
- }
- }
-
- function gte(fieldName: keyof AllOpts, value: number, why?: string) {
- const v = result[fieldName] as number
- if (v < value) {
- const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}`
- errors.push(msg)
- }
- }
-
- notBlank("versionCommand")
- notBlank("pass")
- notBlank("fail")
-
- gte("maxTasksPerProcess", 1)
-
- gte("maxProcs", 1)
-
- if (
- opts.maxProcAgeMillis != null &&
- opts.maxProcAgeMillis > 0 &&
- result.taskTimeoutMillis
- ) {
- gte(
- "maxProcAgeMillis",
- Math.max(result.spawnTimeoutMillis, result.taskTimeoutMillis),
- `the max value of spawnTimeoutMillis (${result.spawnTimeoutMillis}) and taskTimeoutMillis (${result.taskTimeoutMillis})`,
- )
- }
- // 0 disables:
- gte("minDelayBetweenSpawnMillis", 0)
- gte("onIdleIntervalMillis", 0)
- gte("endGracefulWaitTimeMillis", 0)
- gte("maxReasonableProcessFailuresPerMinute", 0)
- gte("streamFlushMillis", 0)
-
- if (errors.length > 0) {
- throw new Error(
- "BatchCluster was given invalid options: " + errors.join("; "),
- )
- }
-
- return result
-}
diff --git a/src/BatchClusterStats.ts b/src/BatchClusterStats.ts
new file mode 100644
index 0000000..ea9adf6
--- /dev/null
+++ b/src/BatchClusterStats.ts
@@ -0,0 +1,16 @@
+import { ChildEndReason } from "./BatchClusterEmitter"
+
+export interface BatchClusterStats {
+ pendingTaskCount: number
+ currentProcCount: number
+ readyProcCount: number
+ maxProcCount: number
+ internalErrorCount: number
+ startErrorRatePerMinute: number
+ msBeforeNextSpawn: number
+ spawnedProcCount: number
+ childEndCounts: Record, number>
+ ending: boolean
+ ended: boolean
+ [key: string]: unknown
+}
diff --git a/src/BatchProcess.ts b/src/BatchProcess.ts
index a6d7eb0..390761b 100644
--- a/src/BatchProcess.ts
+++ b/src/BatchProcess.ts
@@ -1,40 +1,18 @@
import child_process from "node:child_process"
import timers from "node:timers"
-import { until } from "./Async"
import { Deferred } from "./Deferred"
import { cleanError } from "./Error"
import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
import { Logger } from "./Logger"
import { map } from "./Object"
import { SimpleParser } from "./Parser"
-import { kill, pidExists } from "./Pids"
-import { destroy } from "./Stream"
-import { blank, ensureSuffix } from "./String"
+import { pidExists } from "./Pids"
+import { ProcessHealthMonitor } from "./ProcessHealthMonitor"
+import { ProcessTerminator } from "./ProcessTerminator"
+import { StreamContext, StreamHandler } from "./StreamHandler"
+import { ensureSuffix } from "./String"
import { Task } from "./Task"
-import { thenOrTimeout } from "./Timeout"
-
-export type WhyNotHealthy =
- | "broken"
- | "closed"
- | "ending"
- | "ended"
- | "idle"
- | "old"
- | "proc.close"
- | "proc.disconnect"
- | "proc.error"
- | "proc.exit"
- | "stderr.error"
- | "stderr"
- | "stdin.error"
- | "stdout.error"
- | "timeout"
- | "tooMany" // < only sent by BatchCluster when maxProcs is reduced
- | "startError"
- | "unhealthy"
- | "worn"
-
-export type WhyNotReady = WhyNotHealthy | "busy"
+import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"
/**
* BatchProcess manages the care and feeding of a single child process.
@@ -43,13 +21,13 @@ export class BatchProcess {
readonly name: string
readonly pid: number
readonly start = Date.now()
- #lastHealthCheck = Date.now()
- #healthCheckFailures = 0
readonly startupTaskId: number
readonly #logger: () => Logger
+ readonly #terminator: ProcessTerminator
+ readonly #healthMonitor: ProcessHealthMonitor
+ readonly #streamHandler: StreamHandler
#lastJobFinshedAt = Date.now()
- #lastJobFailed = false
// Only set to true when `proc.pid` is no longer in the process table.
#starting = true
@@ -66,7 +44,29 @@ export class BatchProcess {
/**
* Should be undefined if this instance is not currently processing a task.
*/
- #currentTask: Task | undefined
+ #currentTask: Task | undefined
+
+ /**
+ * Getter for current task (required by StreamContext interface)
+ */
+ get currentTask(): Task | undefined {
+ return this.#currentTask
+ }
+
+ /**
+ * Create a StreamContext adapter for this BatchProcess
+ */
+ #createStreamContext = (): StreamContext => {
+ return {
+ name: this.name,
+ isEnding: () => this.ending,
+ getCurrentTask: () => this.#currentTask,
+ onError: (reason: string, error: Error) =>
+ this.#onError(reason as WhyNotHealthy, error),
+ end: (gracefully: boolean, reason: string) =>
+ void this.end(gracefully, reason as WhyNotHealthy),
+ }
+ }
#currentTaskTimeout: NodeJS.Timeout | undefined
#endPromise: undefined | Deferred
@@ -79,9 +79,17 @@ export class BatchProcess {
readonly proc: child_process.ChildProcess,
readonly opts: InternalBatchProcessOptions,
private readonly onIdle: () => void,
+ healthMonitor?: ProcessHealthMonitor,
) {
this.name = "BatchProcess(" + proc.pid + ")"
this.#logger = opts.logger
+ this.#terminator = new ProcessTerminator(opts)
+ this.#healthMonitor =
+ healthMonitor ?? new ProcessHealthMonitor(opts, opts.observer)
+ this.#streamHandler = new StreamHandler(
+ { logger: this.#logger },
+ opts.observer,
+ )
// don't let node count the child processes as a reason to stay alive
this.proc.unref()
@@ -92,23 +100,21 @@ export class BatchProcess {
this.pid = proc.pid
this.proc.on("error", (err) => this.#onError("proc.error", err))
- this.proc.on("close", () => this.end(false, "proc.close"))
- this.proc.on("exit", () => this.end(false, "proc.exit"))
- this.proc.on("disconnect", () => this.end(false, "proc.disconnect"))
-
- const stdin = this.proc.stdin
- if (stdin == null) throw new Error("Given proc had no stdin")
- stdin.on("error", (err) => this.#onError("stdin.error", err))
-
- const stdout = this.proc.stdout
- if (stdout == null) throw new Error("Given proc had no stdout")
- stdout.on("error", (err) => this.#onError("stdout.error", err))
- stdout.on("data", (d) => this.#onStdout(d))
-
- map(this.proc.stderr, (stderr) => {
- stderr.on("error", (err) => this.#onError("stderr.error", err))
- stderr.on("data", (err) => this.#onStderr(err))
+ this.proc.on("close", () => {
+ void this.end(false, "proc.close")
+ })
+ this.proc.on("exit", () => {
+ void this.end(false, "proc.exit")
})
+ this.proc.on("disconnect", () => {
+ void this.end(false, "proc.disconnect")
+ })
+
+ // Set up stream handlers using StreamHandler
+ this.#streamHandler.setupStreamListeners(
+ this.proc,
+ this.#createStreamContext(),
+ )
const startupTask = new Task(opts.versionCommand, SimpleParser)
this.startupTaskId = startupTask.taskId
@@ -119,15 +125,15 @@ export class BatchProcess {
new Error(this.name + " startup task was not submitted"),
)
}
+
+ // Initialize health monitoring for this process
+ this.#healthMonitor.initializeProcess(this.pid)
+
// this needs to be at the end of the constructor, to ensure everything is
// set up on `this`
this.opts.observer.emit("childStart", this)
}
- get currentTask(): Task | undefined {
- return this.#currentTask
- }
-
get taskCount(): number {
return this.#taskCount
}
@@ -171,43 +177,7 @@ export class BatchProcess {
* know if a process can handle a new task.
*/
get whyNotHealthy(): WhyNotHealthy | null {
- if (this.#whyNotHealthy != null) return this.#whyNotHealthy
- if (this.ended) {
- return "ended"
- } else if (this.ending) {
- return "ending"
- } else if (this.#healthCheckFailures > 0) {
- return "unhealthy"
- } else if (this.proc.stdin == null || this.proc.stdin.destroyed) {
- return "closed"
- } else if (
- this.opts.maxTasksPerProcess > 0 &&
- this.taskCount >= this.opts.maxTasksPerProcess
- ) {
- return "worn"
- } else if (
- this.opts.maxIdleMsPerProcess > 0 &&
- this.idleMs > this.opts.maxIdleMsPerProcess
- ) {
- return "idle"
- } else if (
- this.opts.maxFailedTasksPerProcess > 0 &&
- this.failedTaskCount >= this.opts.maxFailedTasksPerProcess
- ) {
- return "broken"
- } else if (
- this.opts.maxProcAgeMillis > 0 &&
- this.start + this.opts.maxProcAgeMillis < Date.now()
- ) {
- return "old"
- } else if (
- (this.opts.taskTimeoutMillis > 0 && this.#currentTask?.runtimeMs) ??
- 0 > this.opts.taskTimeoutMillis
- ) {
- return "timeout"
- } else {
- return null
- }
+ return this.#healthMonitor.assessHealth(this, this.#whyNotHealthy)
}
/**
@@ -255,7 +225,7 @@ export class BatchProcess {
if (!alive) {
this.#exited = true
// once a PID leaves the process table, it's gone for good.
- this.end(false, "proc.exit")
+ void this.end(false, "proc.exit")
}
return alive
}
@@ -264,48 +234,21 @@ export class BatchProcess {
return !this.running()
}
- maybeRunHealthcheck(): Task | undefined {
- const hcc = this.opts.healthCheckCommand
- // if there's no health check command, no-op.
- if (hcc == null || blank(hcc)) return
-
- // if the prior health check failed, .ready will be false
- if (!this.ready) return
-
- if (
- this.#lastJobFailed ||
- (this.opts.healthCheckIntervalMillis > 0 &&
- Date.now() - this.#lastHealthCheck >
- this.opts.healthCheckIntervalMillis)
- ) {
- this.#lastHealthCheck = Date.now()
- const t = new Task(hcc, SimpleParser)
- t.promise
- .catch((err) => {
- this.opts.observer.emit("healthCheckError", err, this)
- this.#healthCheckFailures++
- // BatchCluster will see we're unhealthy and reap us later
- })
- .finally(() => {
- this.#lastHealthCheck = Date.now()
- })
- this.#execTask(t)
- return t
- }
- return
+ maybeRunHealthcheck(): Task | undefined {
+ return this.#healthMonitor.maybeRunHealthcheck(this)
}
// This must not be async, or new instances aren't started as busy (until the
// startup task is complete)
- execTask(task: Task): boolean {
+ execTask(task: Task): boolean {
return this.ready ? this.#execTask(task) : false
}
- #execTask(task: Task): boolean {
+ #execTask(task: Task): boolean {
if (this.ending) return false
this.#taskCount++
- this.#currentTask = task
+ this.#currentTask = task as Task
const cmd = ensureSuffix(task.command, "\n")
const isStartupTask = task.taskId === this.startupTaskId
const taskTimeoutMs = isStartupTask
@@ -315,7 +258,7 @@ export class BatchProcess {
// add the stream flush millis to the taskTimeoutMs, because that time
// should not be counted against the task.
this.#currentTaskTimeout = timers.setTimeout(
- () => this.#onTimeout(task, taskTimeoutMs),
+ () => this.#onTimeout(task as Task, taskTimeoutMs),
taskTimeoutMs + this.opts.streamFlushMillis,
)
}
@@ -323,27 +266,35 @@ export class BatchProcess {
// rejections:
void task.promise.then(
() => {
- this.#clearCurrentTask(task)
+ this.#clearCurrentTask(task as Task)
// this.#logger().trace("task completed", { task })
if (isStartupTask) {
// no need to emit taskResolved for startup tasks.
this.#starting = false
} else {
- this.opts.observer.emit("taskResolved", task, this)
+ this.opts.observer.emit("taskResolved", task as Task, this)
}
// Call _after_ we've cleared the current task:
this.onIdle()
},
(error) => {
- this.#clearCurrentTask(task)
+ this.#clearCurrentTask(task as Task)
// this.#logger().trace("task failed", { task, err: error })
if (isStartupTask) {
- this.opts.observer.emit("startError", error)
- this.end(false, "startError")
+ this.opts.observer.emit(
+ "startError",
+ error instanceof Error ? error : new Error(String(error)),
+ )
+ void this.end(false, "startError")
} else {
- this.opts.observer.emit("taskError", error, task, this)
+ this.opts.observer.emit(
+ "taskError",
+ error instanceof Error ? error : new Error(String(error)),
+ task as Task,
+ this,
+ )
}
// Call _after_ we've cleared the current task:
@@ -365,9 +316,9 @@ export class BatchProcess {
})
return true
}
- } catch (err) {
+ } catch {
// child process went away. We should too.
- this.end(false, "stdin.error")
+ void this.end(false, "stdin.error")
return false
}
}
@@ -395,106 +346,30 @@ export class BatchProcess {
const lastTask = this.#currentTask
this.#clearCurrentTask()
- // NOTE: We wait on all tasks (even startup tasks) so we can assert that
- // BatchCluster is idle (and this proc is idle) when the end promise is
- // resolved.
-
- // NOTE: holy crap there are a lot of notes here.
-
- // We don't need to wait for the startup task to complete, and we certainly
- // don't need to fuss about ending when we're just getting started.
- if (lastTask != null && lastTask.taskId !== this.startupTaskId) {
- try {
- // Let's wait for the process to complete and the streams to flush, as
- // that may actually allow the task to complete successfully. Let's not
- // wait forever, though.
- await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250)
- } catch {
- //
- }
- if (lastTask.pending) {
- lastTask.reject(
- new Error(
- `end() called before task completed (${JSON.stringify({
- gracefully,
- lastTask,
- })})`,
- ),
- )
- }
- }
-
- // Ignore EPIPE on .end(): if the process immediately ends after the exit
- // command, we'll get an EPIPE, so, shush error events *before* we tell the
- // child process to exit. See https://github.com/nodejs/node/issues/26828
- for (const ea of [
+ await this.#terminator.terminate(
this.proc,
- this.proc.stdin,
- this.proc.stdout,
- this.proc.stderr,
- ]) {
- ea?.removeAllListeners("error")
- }
-
- if (true === this.proc.stdin?.writable) {
- const exitCmd =
- this.opts.exitCommand == null
- ? null
- : ensureSuffix(this.opts.exitCommand, "\n")
- try {
- this.proc.stdin?.end(exitCmd)
- } catch {
- // don't care
- }
- }
+ this.name,
+ lastTask,
+ this.startupTaskId,
+ gracefully,
+ this.#exited,
+ () => this.running(),
+ )
- // None of this *should* be necessary, but we're trying to be as hygienic as
- // we can to avoid process zombification.
- destroy(this.proc.stdin)
- destroy(this.proc.stdout)
- destroy(this.proc.stderr)
-
- if (
- this.opts.cleanupChildProcs &&
- gracefully &&
- this.opts.endGracefulWaitTimeMillis > 0 &&
- !this.#exited
- ) {
- // Wait for the exit command to take effect:
- await this.#awaitNotRunning(this.opts.endGracefulWaitTimeMillis / 2)
- // If it's still running, send the pid a signal:
- if (this.running() && this.proc.pid != null) this.proc.kill()
- // Wait for the signal handler to work:
- await this.#awaitNotRunning(this.opts.endGracefulWaitTimeMillis / 2)
- }
+ // Clean up health monitoring for this process
+ this.#healthMonitor.cleanupProcess(this.pid)
- if (
- this.opts.cleanupChildProcs &&
- this.proc.pid != null &&
- this.running()
- ) {
- this.#logger().warn(
- this.name + ".end(): force-killing still-running child.",
- )
- kill(this.proc.pid, true)
- }
- // disconnect may not be a function on proc!
- this.proc.disconnect?.()
this.opts.observer.emit("childEnd", this, reason)
}
- #awaitNotRunning(timeout: number) {
- return until(() => this.notRunning(), timeout)
- }
-
- #onTimeout(task: Task, timeoutMs: number): void {
+ #onTimeout(task: Task, timeoutMs: number): void {
if (task.pending) {
this.opts.observer.emit("taskTimeout", timeoutMs, task, this)
this.#onError("timeout", new Error("waited " + timeoutMs + "ms"), task)
}
}
- #onError(reason: WhyNotHealthy, error: Error, task?: Task) {
+ #onError(reason: WhyNotHealthy, error: Error, task?: Task) {
if (task == null) {
task = this.#currentTask
}
@@ -521,7 +396,7 @@ export class BatchProcess {
if (task != null && this.taskCount === 1) {
this.#logger().warn(
- this.name + ".onError(): startup task failed: " + cleanedError,
+ this.name + ".onError(): startup task failed: " + String(cleanedError),
)
this.opts.observer.emit("startError", cleanedError)
}
@@ -540,35 +415,14 @@ export class BatchProcess {
}
}
- #onStderr(data: string | Buffer) {
- if (blank(data)) return
- this.#logger().warn(this.name + ".onStderr(): " + data)
- const task = this.#currentTask
- if (task != null && task.pending) {
- task.onStderr(data)
- } else if (!this.ending) {
- // If we're ending and there isn't a task, don't worry about it.
- this.opts.observer.emit("noTaskData", null, data, this)
- void this.end(false, "stderr")
+ #clearCurrentTask(task?: Task) {
+ const taskFailed = task?.state === "rejected"
+ if (taskFailed) {
+ this.#healthMonitor.recordJobFailure(this.pid)
+ } else if (task != null) {
+ this.#healthMonitor.recordJobSuccess(this.pid)
}
- }
-
- #onStdout(data: string | Buffer) {
- if (data == null) return
- const task = this.#currentTask
- if (task != null && task.pending) {
- this.opts.observer.emit("taskData", data, task, this)
- task.onStdout(data)
- } else if (this.ending) {
- // don't care if we're already being shut down.
- } else if (!blank(data)) {
- this.opts.observer.emit("noTaskData", data, null, this)
- void this.end(false, "stdout.error")
- }
- }
- #clearCurrentTask(task?: Task) {
- this.#lastJobFailed = task?.state === "rejected"
if (task != null && task.taskId !== this.#currentTask?.taskId) return
map(this.#currentTaskTimeout, (ea) => clearTimeout(ea))
this.#currentTaskTimeout = undefined
diff --git a/src/ChildProcessFactory.ts b/src/ChildProcessFactory.ts
new file mode 100644
index 0000000..fa03034
--- /dev/null
+++ b/src/ChildProcessFactory.ts
@@ -0,0 +1,20 @@
+import child_process from "node:child_process"
+
+/**
+ * These are required parameters for a given BatchCluster.
+ */
+
+export interface ChildProcessFactory {
+ /**
+ * Expected to be a simple call to execFile. Platform-specific code is the
+ * responsibility of this thunk. Error handlers will be registered as
+ * appropriate.
+ *
+ * If this function throws an error or rejects the promise _after_ you've
+ * spawned a child process, **the child process may continue to run** and leak
+ * system resources.
+ */
+ readonly processFactory: () =>
+ | child_process.ChildProcess
+ | Promise
+}
diff --git a/src/CombinedBatchProcessOptions.ts b/src/CombinedBatchProcessOptions.ts
new file mode 100644
index 0000000..9321e5d
--- /dev/null
+++ b/src/CombinedBatchProcessOptions.ts
@@ -0,0 +1,8 @@
+import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"
+import { ChildProcessFactory } from "./ChildProcessFactory"
+import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
+
+export type CombinedBatchProcessOptions = BatchClusterOptions &
+ InternalBatchProcessOptions &
+ ChildProcessFactory &
+ WithObserver
diff --git a/src/Deferred.ts b/src/Deferred.ts
index 5bc1b8b..796959f 100644
--- a/src/Deferred.ts
+++ b/src/Deferred.ts
@@ -16,7 +16,7 @@ export class Deferred implements PromiseLike {
readonly [Symbol.toStringTag] = "Deferred"
readonly promise: Promise
#resolve!: (value: T | PromiseLike) => void
- #reject!: (reason?: any) => void
+ #reject!: (reason?: unknown) => void
#state: State = State.pending
constructor() {
@@ -55,23 +55,14 @@ export class Deferred implements PromiseLike {
}
then(
- onfulfilled?:
- | ((value: T) => TResult1 | PromiseLike)
- | undefined
- | null,
- onrejected?:
- | ((reason: any) => TResult2 | PromiseLike)
- | undefined
- | null,
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null,
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null,
): Promise {
return this.promise.then(onfulfilled, onrejected)
}
catch(
- onrejected?:
- | ((reason: any) => TResult | PromiseLike)
- | undefined
- | null,
+ onrejected?: ((reason: unknown) => TResult | PromiseLike) | null,
): Promise {
return this.promise.catch(onrejected)
}
@@ -107,15 +98,15 @@ export class Deferred implements PromiseLike {
observeQuietly(p: Promise): Deferred {
void observeQuietly(this, p)
- return this as any
+ return this as Deferred
}
}
async function observe(d: Deferred, p: Promise) {
try {
d.resolve(await p)
- } catch (err: any) {
- d.reject(err)
+ } catch (err: unknown) {
+ d.reject(err instanceof Error ? err : new Error(String(err)))
}
}
@@ -123,6 +114,6 @@ async function observeQuietly(d: Deferred, p: Promise) {
try {
d.resolve(await p)
} catch {
- d.resolve(undefined as any)
+ d.resolve(undefined as T)
}
}
diff --git a/src/Error.ts b/src/Error.ts
index 9100aa8..853753d 100644
--- a/src/Error.ts
+++ b/src/Error.ts
@@ -1,4 +1,4 @@
-import { blank, toS } from "./String"
+import { toNotBlank } from "./String"
/**
* When we wrap errors, an Error always prefixes the toString() and stack with
@@ -8,20 +8,28 @@ export function tryEach(arr: (() => void)[]): void {
for (const f of arr) {
try {
f()
- } catch (_) {
+ } catch {
//
}
}
}
-export function cleanError(s: any): string {
+export function cleanError(s: unknown): string {
return String(s)
.trim()
.replace(/^error: /i, "")
}
-export function asError(err: any): Error {
+export function asError(err: unknown): Error {
return err instanceof Error
? err
- : new Error(blank(err) ? "(unknown)" : toS(err))
+ : new Error(
+ toNotBlank(
+ err != null && typeof err === "object" && "message" in err
+ ? err?.message
+ : undefined,
+ ) ??
+ toNotBlank(err) ??
+ "(unknown)",
+ )
}
diff --git a/src/HealthCheckStrategy.ts b/src/HealthCheckStrategy.ts
new file mode 100644
index 0000000..eeb0b28
--- /dev/null
+++ b/src/HealthCheckStrategy.ts
@@ -0,0 +1,157 @@
+import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
+import { HealthCheckable } from "./ProcessHealthMonitor"
+import { WhyNotHealthy } from "./WhyNotHealthy"
+
+/**
+ * Strategy interface for different health check approaches
+ */
+export interface HealthCheckStrategy {
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null
+}
+
+/**
+ * Checks if process has ended or is ending
+ */
+export class LifecycleHealthCheck implements HealthCheckStrategy {
+ assess(process: HealthCheckable): WhyNotHealthy | null {
+ if (process.ended) {
+ return "ended"
+ } else if (process.ending) {
+ return "ending"
+ }
+ return null
+ }
+}
+
+/**
+ * Checks if process stdin is available
+ */
+export class StreamHealthCheck implements HealthCheckStrategy {
+ assess(process: HealthCheckable): WhyNotHealthy | null {
+ if (process.proc.stdin == null || process.proc.stdin.destroyed) {
+ return "closed"
+ }
+ return null
+ }
+}
+
+/**
+ * Checks if process has exceeded task limits
+ */
+export class TaskLimitHealthCheck implements HealthCheckStrategy {
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null {
+ if (
+ options.maxTasksPerProcess > 0 &&
+ process.taskCount >= options.maxTasksPerProcess
+ ) {
+ return "worn"
+ }
+ return null
+ }
+}
+
+/**
+ * Checks if process has been idle too long
+ */
+export class IdleTimeHealthCheck implements HealthCheckStrategy {
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null {
+ if (
+ options.maxIdleMsPerProcess > 0 &&
+ process.idleMs > options.maxIdleMsPerProcess
+ ) {
+ return "idle"
+ }
+ return null
+ }
+}
+
+/**
+ * Checks if process has too many failed tasks
+ */
+export class FailureCountHealthCheck implements HealthCheckStrategy {
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null {
+ if (
+ options.maxFailedTasksPerProcess > 0 &&
+ process.failedTaskCount >= options.maxFailedTasksPerProcess
+ ) {
+ return "broken"
+ }
+ return null
+ }
+}
+
+/**
+ * Checks if process is too old
+ */
+export class AgeHealthCheck implements HealthCheckStrategy {
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null {
+ if (
+ options.maxProcAgeMillis > 0 &&
+ process.start + options.maxProcAgeMillis < Date.now()
+ ) {
+ return "old"
+ }
+ return null
+ }
+}
+
+/**
+ * Checks if current task has timed out
+ */
+export class TaskTimeoutHealthCheck implements HealthCheckStrategy {
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null {
+ if (
+ options.taskTimeoutMillis > 0 &&
+ (process.currentTask?.runtimeMs ?? 0) > options.taskTimeoutMillis
+ ) {
+ return "timeout"
+ }
+ return null
+ }
+}
+
+/**
+ * Composite strategy that runs all health checks in order of priority
+ */
+export class CompositeHealthCheckStrategy implements HealthCheckStrategy {
+ private readonly strategies: HealthCheckStrategy[] = [
+ new LifecycleHealthCheck(),
+ new StreamHealthCheck(),
+ new TaskLimitHealthCheck(),
+ new IdleTimeHealthCheck(),
+ new FailureCountHealthCheck(),
+ new AgeHealthCheck(),
+ new TaskTimeoutHealthCheck(),
+ ]
+
+ assess(
+ process: HealthCheckable,
+ options: InternalBatchProcessOptions,
+ ): WhyNotHealthy | null {
+ for (const strategy of this.strategies) {
+ const result = strategy.assess(process, options)
+ if (result != null) {
+ return result
+ }
+ }
+ return null
+ }
+}
diff --git a/src/Logger.ts b/src/Logger.ts
index a91caf7..2c4bd54 100644
--- a/src/Logger.ts
+++ b/src/Logger.ts
@@ -2,7 +2,7 @@ import util from "node:util"
import { map } from "./Object"
import { notBlank } from "./String"
-type LogFunc = (message: string, ...optionalParams: any[]) => void
+type LogFunc = (message: string, ...optionalParams: unknown[]) => void
/**
* Simple interface for logging.
@@ -56,11 +56,17 @@ export const ConsoleLogger: Logger = Object.freeze({
/**
* Delegates to `console.warn`
*/
- warn: console.warn,
+ warn: (...args: unknown[]) => {
+ // eslint-disable-next-line no-console
+ console.warn(...args)
+ },
/**
* Delegates to `console.error`
*/
- error: console.error,
+ error: (...args: unknown[]) => {
+ // eslint-disable-next-line no-console
+ console.error(...args)
+ },
})
/**
@@ -74,11 +80,11 @@ export const NoLogger: Logger = Object.freeze({
error: noop,
})
-let _logger: Logger = NoLogger
+let _logger: Logger = _debuglog.enabled ? ConsoleLogger : NoLogger
export function setLogger(l: Logger): void {
if (LogLevels.some((ea) => typeof l[ea] !== "function")) {
- throw new Error("invalid logger, must implement " + LogLevels)
+ throw new Error("invalid logger, must implement " + LogLevels.join(", "))
}
_logger = l
}
@@ -89,12 +95,12 @@ export function logger(): Logger {
export const Log = {
withLevels: (delegate: Logger): Logger => {
- const timestamped: any = {}
+ const timestamped: Logger = {} as Logger
LogLevels.forEach((ea) => {
const prefix = (ea + " ").substring(0, 5) + " | "
- timestamped[ea] = (message?: any, ...optionalParams: any[]) => {
- if (notBlank(message)) {
- delegate[ea](prefix + message, ...optionalParams)
+ timestamped[ea] = (message?: unknown, ...optionalParams: unknown[]) => {
+ if (notBlank(String(message))) {
+ delegate[ea](prefix + String(message), ...optionalParams)
}
}
})
@@ -102,13 +108,16 @@ export const Log = {
},
withTimestamps: (delegate: Logger) => {
- const timestamped: any = {}
+ const timestamped: Logger = {} as Logger
LogLevels.forEach(
(level) =>
- (timestamped[level] = (message?: any, ...optionalParams: any[]) =>
+ (timestamped[level] = (
+ message?: unknown,
+ ...optionalParams: unknown[]
+ ) =>
map(message, (ea) =>
delegate[level](
- new Date().toISOString() + " | " + ea,
+ new Date().toISOString() + " | " + String(ea),
...optionalParams,
),
)),
@@ -118,7 +127,7 @@ export const Log = {
filterLevels: (l: Logger, minLogLevel: keyof Logger) => {
const minLogLevelIndex = LogLevels.indexOf(minLogLevel)
- const filtered: any = {}
+ const filtered: Logger = {} as Logger
LogLevels.forEach(
(ea, idx) =>
(filtered[ea] = idx < minLogLevelIndex ? noop : l[ea].bind(l)),
diff --git a/src/Mutex.ts b/src/Mutex.ts
index a572c14..accab3a 100644
--- a/src/Mutex.ts
+++ b/src/Mutex.ts
@@ -6,7 +6,7 @@ import { Deferred } from "./Deferred"
*/
export class Mutex {
private _pushCount = 0
- private readonly _arr: Deferred[] = []
+ private readonly _arr: Deferred[] = []
private get arr() {
filterInPlace(this._arr, (ea) => ea.pending)
diff --git a/src/Object.ts b/src/Object.ts
index c1a2b3c..7bb4839 100644
--- a/src/Object.ts
+++ b/src/Object.ts
@@ -9,20 +9,14 @@ export function map(
return obj != null ? f(obj) : undefined
}
-export function isFunction(obj: any): obj is () => any {
+export function isFunction(obj: unknown): obj is () => unknown {
return typeof obj === "function"
}
-export function orElse(obj: T | undefined, defaultValue: T | (() => T)): T {
- return obj != null
- ? obj
- : isFunction(defaultValue)
- ? defaultValue()
- : defaultValue
-}
-
-export function fromEntries(arr: [string | undefined, any][]) {
- const o: any = {}
+export function fromEntries(
+ arr: [string | undefined, unknown][],
+): Record {
+ const o: Record = {}
for (const [key, value] of arr) {
if (key != null) {
o[key] = value
@@ -31,7 +25,7 @@ export function fromEntries(arr: [string | undefined, any][]) {
return o
}
-export function omit, S extends keyof T>(
+export function omit, S extends keyof T>(
t: T,
...keysToOmit: S[]
): Omit {
diff --git a/src/OptionsVerifier.ts b/src/OptionsVerifier.ts
new file mode 100644
index 0000000..bd776fd
--- /dev/null
+++ b/src/OptionsVerifier.ts
@@ -0,0 +1,102 @@
+import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"
+import { BatchProcessOptions } from "./BatchProcessOptions"
+import { ChildProcessFactory } from "./ChildProcessFactory"
+import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"
+import { blank, toS } from "./String"
+
+/**
+ * Verifies and sanitizes the provided options for BatchCluster.
+ *
+ * It merges partial options with default BatchClusterOptions,
+ * converts pass/fail strings to RegExp, and validates various constraints.
+ *
+ * @param opts - The partial options to verify. These are merged with default
+ * BatchClusterOptions.
+ * @returns The fully verified and sanitized options.
+ * @throws Error if any options are invalid.
+ */
+export function verifyOptions(
+ opts: Partial &
+ BatchProcessOptions &
+ ChildProcessFactory &
+ WithObserver,
+): CombinedBatchProcessOptions {
+ const result: CombinedBatchProcessOptions = {
+ ...new BatchClusterOptions(),
+ ...opts,
+ passRE: toRe(opts.pass),
+ failRE: toRe(opts.fail),
+ } as CombinedBatchProcessOptions
+
+ const errors: string[] = []
+
+ function notBlank(fieldName: keyof CombinedBatchProcessOptions) {
+ const v = toS(result[fieldName])
+ if (blank(v)) {
+ errors.push(fieldName + " must not be blank")
+ }
+ }
+
+ function gte(
+ fieldName: keyof CombinedBatchProcessOptions,
+ value: number,
+ why?: string,
+ ) {
+ const v = result[fieldName] as number
+ if (v < value) {
+ const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}`
+ errors.push(msg)
+ }
+ }
+
+ notBlank("versionCommand")
+ notBlank("pass")
+ notBlank("fail")
+
+ gte("maxTasksPerProcess", 1)
+
+ gte("maxProcs", 1)
+
+ if (
+ opts.maxProcAgeMillis != null &&
+ opts.maxProcAgeMillis > 0 &&
+ result.taskTimeoutMillis
+ ) {
+ gte(
+ "maxProcAgeMillis",
+ Math.max(result.spawnTimeoutMillis, result.taskTimeoutMillis),
+ `the max value of spawnTimeoutMillis (${result.spawnTimeoutMillis}) and taskTimeoutMillis (${result.taskTimeoutMillis})`,
+ )
+ }
+ // 0 disables:
+ gte("minDelayBetweenSpawnMillis", 0)
+ gte("onIdleIntervalMillis", 0)
+ gte("endGracefulWaitTimeMillis", 0)
+ gte("maxReasonableProcessFailuresPerMinute", 0)
+ gte("streamFlushMillis", 0)
+
+ if (errors.length > 0) {
+ throw new Error(
+ "BatchCluster was given invalid options: " + errors.join("; "),
+ )
+ }
+
+ return result
+}
+function escapeRegExp(s: string) {
+ return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&")
+}
+function toRe(s: string | RegExp) {
+ return s instanceof RegExp
+ ? s
+ : blank(s)
+ ? /$./
+ : s.includes("*")
+ ? new RegExp(
+ s
+ .split("*")
+ .map((ea) => escapeRegExp(ea))
+ .join(".*"),
+ )
+ : new RegExp(escapeRegExp(s))
+}
diff --git a/src/Parser.ts b/src/Parser.ts
index 2ebbee0..8c2b011 100644
--- a/src/Parser.ts
+++ b/src/Parser.ts
@@ -5,24 +5,26 @@ import { notBlank } from "./String"
* process to a more useable format. This can be a no-op passthrough if no
* parsing is necessary.
*/
-export interface Parser {
- /**
- * Invoked once per task.
- *
- * @param stdout the concatenated stream from `stdin`, stripped of the `PASS`
- * or `FAIL` tokens from `BatchProcessOptions`.
- *
- * @param stderr if defined, includes all text emitted to stderr.
- *
- * @param passed `true` iff the `PASS` pattern was found in stdout.
- *
- * @throws an error if the Parser implementation wants to reject the task. It
- * is valid to raise Errors if stderr is undefined.
- *
- * @see BatchProcessOptions
- */
- (stdout: string, stderr: string | undefined, passed: boolean): T | Promise
-}
+/**
+ * Invoked once per task.
+ *
+ * @param stdout the concatenated stream from `stdin`, stripped of the `PASS`
+ * or `FAIL` tokens from `BatchProcessOptions`.
+ *
+ * @param stderr if defined, includes all text emitted to stderr.
+ *
+ * @param passed `true` iff the `PASS` pattern was found in stdout.
+ *
+ * @throws an error if the Parser implementation wants to reject the task. It
+ * is valid to raise Errors if stderr is undefined.
+ *
+ * @see BatchProcessOptions
+ */
+export type Parser = (
+ stdout: string,
+ stderr: string | undefined,
+ passed: boolean,
+) => T | Promise
export const SimpleParser: Parser = (
stdout: string,
diff --git a/src/Pids.ts b/src/Pids.ts
index ddf3034..5cff0ec 100644
--- a/src/Pids.ts
+++ b/src/Pids.ts
@@ -1,6 +1,7 @@
import child_process from "node:child_process"
-import process from "node:process"
-import { map } from "./Object"
+import { existsSync } from "node:fs"
+import { readdir } from "node:fs/promises"
+import { asError } from "./Error"
import { isWin } from "./Platform"
/**
@@ -14,11 +15,11 @@ export function pidExists(pid: number | undefined): boolean {
// signal 0 can be used to test for the existence of a process
// see https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processkillpid-signal
return process.kill(pid, 0)
- } catch (err: any) {
+ } catch (err: unknown) {
// We're expecting err.code to be either "EPERM" (if we don't have
// permission to send `pid` and message), or "ESRCH" if that pid doesn't
// exist. EPERM means it _does_ exist!
- if (err?.code === "EPERM") return true
+ if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true
// failed to get priority--assume the pid is gone.
return false
@@ -44,35 +45,44 @@ export function kill(pid: number | undefined, force = false): boolean {
}
}
-const winRe = /^".+?","(\d+)"/
-const posixRe = /^\s*(\d+)/
-
/**
* Only used by tests
*
* @returns {Promise} all the Process IDs in the process table.
*/
-export function pids(): Promise {
- return new Promise((resolve, reject) => {
- child_process.execFile(
- isWin ? "tasklist" : "ps",
- // NoHeader, FOrmat CSV
- isWin ? ["/NH", "/FO", "CSV"] : ["-e"],
- (error: Error | null, stdout: string, stderr: string) => {
- if (error != null) {
- reject(error)
- } else if (("" + stderr).trim().length > 0) {
- reject(new Error(stderr))
- } else
- resolve(
- ("" + stdout)
- .trim()
- .split(/[\n\r]+/)
- .map((ea) => ea.match(isWin ? winRe : posixRe))
- .map((m) => map(m?.[0], parseInt))
- .filter((ea) => ea != null) as number[],
- )
- },
- )
+export async function pids(): Promise {
+ // Linux‐style: read /proc
+ if (!isWin && existsSync("/proc")) {
+ const names = await readdir("/proc")
+ return names.filter((d) => /^\d+$/.test(d)).map((d) => parseInt(d, 10))
+ }
+
+ // fallback: ps or tasklist
+ const cmd = isWin ? "tasklist" : "ps"
+ const args = isWin ? ["/NH", "/FO", "CSV"] : ["-e", "-o", "pid="]
+
+ return new Promise((resolve, reject) => {
+ child_process.execFile(cmd, args, (err, stdout, stderr) => {
+ if (err) return reject(asError(err))
+ if (stderr.trim()) return reject(new Error(stderr))
+
+ const pids = stdout
+ .trim()
+ .split(/[\r\n]+/)
+ .map((line) => {
+ if (isWin) {
+ // "Image","PID",…
+ // split on "," and strip outer quotes:
+ const cols = line.split('","')
+ const pidStr = cols[1]?.replace(/"/g, "")
+ return Number(pidStr)
+ }
+ // ps -o pid= gives you just the number
+ return Number(line.trim())
+ })
+ .filter((n) => Number.isFinite(n) && n > 0)
+
+ resolve(pids)
+ })
})
}
diff --git a/src/ProcessHealthMonitor.spec.ts b/src/ProcessHealthMonitor.spec.ts
new file mode 100644
index 0000000..74b40b5
--- /dev/null
+++ b/src/ProcessHealthMonitor.spec.ts
@@ -0,0 +1,384 @@
+import events from "node:events"
+import { expect, processFactory } from "./_chai.spec"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { DefaultTestOptions } from "./DefaultTestOptions.spec"
+import { verifyOptions } from "./OptionsVerifier"
+import { HealthCheckable, ProcessHealthMonitor } from "./ProcessHealthMonitor"
+import { Task } from "./Task"
+
+describe("ProcessHealthMonitor", function () {
+ let healthMonitor: ProcessHealthMonitor
+ let emitter: BatchClusterEmitter
+ let mockProcess: HealthCheckable
+
+ beforeEach(function () {
+ emitter = new events.EventEmitter() as BatchClusterEmitter
+
+ const options = verifyOptions({
+ ...DefaultTestOptions,
+ processFactory,
+ observer: emitter,
+ healthCheckCommand: "healthcheck",
+ healthCheckIntervalMillis: 1000,
+ maxTasksPerProcess: 5,
+ maxIdleMsPerProcess: 2000,
+ maxFailedTasksPerProcess: 3,
+ maxProcAgeMillis: 20000, // Must be > spawnTimeoutMillis
+ taskTimeoutMillis: 1000,
+ })
+
+ healthMonitor = new ProcessHealthMonitor(options, emitter)
+
+ // Create a healthy mock process
+ mockProcess = {
+ pid: 12345,
+ start: Date.now(),
+ taskCount: 0,
+ failedTaskCount: 0,
+ idleMs: 0,
+ idle: true,
+ ending: false,
+ ended: false,
+ proc: { stdin: { destroyed: false } },
+ currentTask: null,
+ }
+ })
+
+ describe("process lifecycle", function () {
+ it("should initialize process health monitoring", function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+
+ const state = healthMonitor.getProcessHealthState(mockProcess.pid)
+ expect(state).to.not.be.undefined
+ expect(state?.healthCheckFailures).to.eql(0)
+ expect(state?.lastJobFailed).to.be.false
+ })
+
+ it("should cleanup process health monitoring", function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+ expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.not.be
+ .undefined
+
+ healthMonitor.cleanupProcess(mockProcess.pid)
+ expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.be
+ .undefined
+ })
+ })
+
+ describe("health assessment", function () {
+ beforeEach(function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+ })
+
+ it("should assess healthy process as healthy", function () {
+ const healthReason = healthMonitor.assessHealth(mockProcess)
+ expect(healthReason).to.be.null
+ expect(healthMonitor.isHealthy(mockProcess)).to.be.true
+ })
+
+ it("should detect ended process", function () {
+ const endedProcess = { ...mockProcess, ended: true }
+
+ const healthReason = healthMonitor.assessHealth(endedProcess)
+ expect(healthReason).to.eql("ended")
+ expect(healthMonitor.isHealthy(endedProcess)).to.be.false
+ })
+
+ it("should detect ending process", function () {
+ const endingProcess = { ...mockProcess, ending: true }
+
+ const healthReason = healthMonitor.assessHealth(endingProcess)
+ expect(healthReason).to.eql("ending")
+ expect(healthMonitor.isHealthy(endingProcess)).to.be.false
+ })
+
+ it("should detect closed stdin", function () {
+ const closedProcess = {
+ ...mockProcess,
+ proc: { stdin: { destroyed: true } },
+ }
+
+ const healthReason = healthMonitor.assessHealth(closedProcess)
+ expect(healthReason).to.eql("closed")
+ expect(healthMonitor.isHealthy(closedProcess)).to.be.false
+ })
+
+ it("should detect null stdin", function () {
+ const nullStdinProcess = {
+ ...mockProcess,
+ proc: { stdin: null },
+ }
+
+ const healthReason = healthMonitor.assessHealth(nullStdinProcess)
+ expect(healthReason).to.eql("closed")
+ expect(healthMonitor.isHealthy(nullStdinProcess)).to.be.false
+ })
+
+ it("should detect worn process (too many tasks)", function () {
+ const wornProcess = { ...mockProcess, taskCount: 5 }
+
+ const healthReason = healthMonitor.assessHealth(wornProcess)
+ expect(healthReason).to.eql("worn")
+ expect(healthMonitor.isHealthy(wornProcess)).to.be.false
+ })
+
+ it("should detect idle process (idle too long)", function () {
+ const idleProcess = { ...mockProcess, idleMs: 3000 }
+
+ const healthReason = healthMonitor.assessHealth(idleProcess)
+ expect(healthReason).to.eql("idle")
+ expect(healthMonitor.isHealthy(idleProcess)).to.be.false
+ })
+
+ it("should detect broken process (too many failed tasks)", function () {
+ const brokenProcess = { ...mockProcess, failedTaskCount: 3 }
+
+ const healthReason = healthMonitor.assessHealth(brokenProcess)
+ expect(healthReason).to.eql("broken")
+ expect(healthMonitor.isHealthy(brokenProcess)).to.be.false
+ })
+
+ it("should detect old process", function () {
+ const oldProcess = {
+ ...mockProcess,
+ start: Date.now() - 25000, // 25 seconds ago (older than maxProcAgeMillis)
+ }
+
+ const healthReason = healthMonitor.assessHealth(oldProcess)
+ expect(healthReason).to.eql("old")
+ expect(healthMonitor.isHealthy(oldProcess)).to.be.false
+ })
+
+ it("should detect timed out task", function () {
+ // Create a mock task that simulates a long runtime
+ const mockTask = {
+ runtimeMs: 1500, // longer than 1000ms timeout
+ } as Task
+
+ const timedOutProcess = {
+ ...mockProcess,
+ currentTask: mockTask,
+ }
+
+ const healthReason = healthMonitor.assessHealth(timedOutProcess)
+ expect(healthReason).to.eql("timeout")
+ expect(healthMonitor.isHealthy(timedOutProcess)).to.be.false
+ })
+
+ it("should respect override reason", function () {
+ const healthReason = healthMonitor.assessHealth(mockProcess, "startError")
+ expect(healthReason).to.eql("startError")
+ expect(healthMonitor.isHealthy(mockProcess, "startError")).to.be.false
+ })
+
+ it("should detect unhealthy process after health check failures", function () {
+ // Simulate a health check failure
+ const state = healthMonitor.getProcessHealthState(mockProcess.pid)
+ if (state != null) {
+ state.healthCheckFailures = 1
+ }
+
+ const healthReason = healthMonitor.assessHealth(mockProcess)
+ expect(healthReason).to.eql("unhealthy")
+ expect(healthMonitor.isHealthy(mockProcess)).to.be.false
+ })
+ })
+
+ describe("readiness assessment", function () {
+ beforeEach(function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+ })
+
+ it("should assess idle healthy process as ready", function () {
+ const readinessReason = healthMonitor.assessReadiness(mockProcess)
+ expect(readinessReason).to.be.null
+ expect(healthMonitor.isReady(mockProcess)).to.be.true
+ })
+
+ it("should detect busy process", function () {
+ const busyProcess = { ...mockProcess, idle: false }
+
+ const readinessReason = healthMonitor.assessReadiness(busyProcess)
+ expect(readinessReason).to.eql("busy")
+ expect(healthMonitor.isReady(busyProcess)).to.be.false
+ })
+
+ it("should detect unhealthy idle process", function () {
+ const unhealthyProcess = { ...mockProcess, ended: true }
+
+ const readinessReason = healthMonitor.assessReadiness(unhealthyProcess)
+ expect(readinessReason).to.eql("ended")
+ expect(healthMonitor.isReady(unhealthyProcess)).to.be.false
+ })
+ })
+
+ describe("job state tracking", function () {
+ beforeEach(function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+ })
+
+ it("should record job failures", function () {
+ healthMonitor.recordJobFailure(mockProcess.pid)
+
+ const state = healthMonitor.getProcessHealthState(mockProcess.pid)
+ expect(state?.lastJobFailed).to.be.true
+ })
+
+ it("should record job successes", function () {
+ // First record a failure
+ healthMonitor.recordJobFailure(mockProcess.pid)
+ expect(
+ healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed,
+ ).to.be.true
+
+ // Then record a success
+ healthMonitor.recordJobSuccess(mockProcess.pid)
+ expect(
+ healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed,
+ ).to.be.false
+ })
+
+ it("should handle recording for non-existent process gracefully", function () {
+ // Should not throw when recording for unknown PID
+ expect(() => {
+ healthMonitor.recordJobFailure(99999)
+ healthMonitor.recordJobSuccess(99999)
+ }).to.not.throw()
+ })
+ })
+
+ describe("health check execution", function () {
+ let mockBatchProcess: HealthCheckable & {
+ execTask: (task: Task) => boolean
+ }
+
+ beforeEach(function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+
+ mockBatchProcess = {
+ ...mockProcess,
+ execTask: () => true, // Mock successful task execution
+ }
+ })
+
+ it("should skip health check when no command configured", function () {
+ // Create monitor with no health check command
+ const options = verifyOptions({
+ ...DefaultTestOptions,
+ processFactory,
+ observer: emitter,
+ healthCheckCommand: "",
+ })
+ const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter)
+
+ const result = noHealthCheckMonitor.maybeRunHealthcheck(mockBatchProcess)
+ expect(result).to.be.undefined
+ })
+
+ it("should skip health check when process not ready", function () {
+ const unreadyProcess = { ...mockBatchProcess, idle: false }
+
+ const result = healthMonitor.maybeRunHealthcheck(unreadyProcess)
+ expect(result).to.be.undefined
+ })
+
+ it("should run health check after job failure", function () {
+ healthMonitor.recordJobFailure(mockProcess.pid)
+
+ const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess)
+ expect(result).to.not.be.undefined
+ expect(result?.command).to.eql("healthcheck")
+ })
+
+ it("should run health check after interval expires", function () {
+ // Mock an old health check
+ const state = healthMonitor.getProcessHealthState(mockProcess.pid)
+ if (state != null) {
+ state.lastHealthCheck = Date.now() - 2000 // 2 seconds ago
+ }
+
+ const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess)
+ expect(result).to.not.be.undefined
+ expect(result?.command).to.eql("healthcheck")
+ })
+
+ it("should not run health check when interval hasn't expired", function () {
+ // Health check was just done
+ const state = healthMonitor.getProcessHealthState(mockProcess.pid)
+ if (state != null) {
+ state.lastHealthCheck = Date.now()
+ }
+
+ const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess)
+ expect(result).to.be.undefined
+ })
+
+ it("should handle failed task execution gracefully", function () {
+ const failingProcess = {
+ ...mockBatchProcess,
+ execTask: () => false, // Mock failed task execution
+ }
+
+ healthMonitor.recordJobFailure(mockProcess.pid)
+
+ const result = healthMonitor.maybeRunHealthcheck(failingProcess)
+ expect(result).to.be.undefined
+ })
+ })
+
+ describe("health statistics", function () {
+ it("should provide accurate health statistics", function () {
+ // Initialize multiple processes
+ healthMonitor.initializeProcess(1)
+ healthMonitor.initializeProcess(2)
+ healthMonitor.initializeProcess(3)
+
+ // Add some failures
+ const state1 = healthMonitor.getProcessHealthState(1)
+ const state2 = healthMonitor.getProcessHealthState(2)
+ if (state1 != null) state1.healthCheckFailures = 2
+ if (state2 != null) state2.healthCheckFailures = 1
+
+ const stats = healthMonitor.getHealthStats()
+ expect(stats.monitoredProcesses).to.eql(3)
+ expect(stats.totalHealthCheckFailures).to.eql(3)
+ expect(stats.processesWithFailures).to.eql(2)
+ })
+
+ it("should reset health check failures", function () {
+ healthMonitor.initializeProcess(mockProcess.pid)
+
+ // Add some failures
+ const state = healthMonitor.getProcessHealthState(mockProcess.pid)
+ if (state != null) {
+ state.healthCheckFailures = 5
+ }
+
+ expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(5)
+
+ healthMonitor.resetHealthCheckFailures(mockProcess.pid)
+
+ expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(0)
+ expect(healthMonitor.isHealthy(mockProcess)).to.be.true
+ })
+ })
+
+ describe("edge cases", function () {
+ it("should handle assessment of process without initialized state", function () {
+ // Don't initialize the process
+ const healthReason = healthMonitor.assessHealth(mockProcess)
+ expect(healthReason).to.be.null // Should still work, just no health check state
+ })
+
+ it("should handle health check on process without state", function () {
+ const mockBatchProcess = {
+ ...mockProcess,
+ execTask: () => true,
+ }
+
+ // Don't initialize the process
+ const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess)
+ expect(result).to.be.undefined
+ })
+ })
+})
diff --git a/src/ProcessHealthMonitor.ts b/src/ProcessHealthMonitor.ts
new file mode 100644
index 0000000..0bc69d4
--- /dev/null
+++ b/src/ProcessHealthMonitor.ts
@@ -0,0 +1,221 @@
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import {
+ CompositeHealthCheckStrategy,
+ HealthCheckStrategy,
+} from "./HealthCheckStrategy"
+import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
+import { SimpleParser } from "./Parser"
+import { blank } from "./String"
+import { Task } from "./Task"
+import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"
+
+/**
+ * Interface for objects that can be health checked
+ */
+export interface HealthCheckable {
+ readonly pid: number
+ readonly start: number
+ readonly taskCount: number
+ readonly failedTaskCount: number
+ readonly idleMs: number
+ readonly idle: boolean
+ readonly ending: boolean
+ readonly ended: boolean
+ readonly proc: {
+ stdin?: { destroyed?: boolean } | null
+ }
+ readonly currentTask?: Task | null | undefined
+}
+
+/**
+ * Manages health checking logic for processes.
+ * Provides centralized health assessment and monitoring capabilities.
+ */
+export class ProcessHealthMonitor {
+ readonly #healthCheckStates = new Map<
+ number,
+ {
+ lastHealthCheck: number
+ healthCheckFailures: number
+ lastJobFailed: boolean
+ }
+ >()
+
+ private readonly healthStrategy: HealthCheckStrategy
+
+ constructor(
+ private readonly options: InternalBatchProcessOptions,
+ private readonly emitter: BatchClusterEmitter,
+ healthStrategy?: HealthCheckStrategy,
+ ) {
+ this.healthStrategy = healthStrategy ?? new CompositeHealthCheckStrategy()
+ }
+
+ /**
+ * Initialize health monitoring for a process
+ */
+ initializeProcess(pid: number): void {
+ this.#healthCheckStates.set(pid, {
+ lastHealthCheck: Date.now(),
+ healthCheckFailures: 0,
+ lastJobFailed: false,
+ })
+ }
+
+ /**
+ * Clean up health monitoring for a process
+ */
+ cleanupProcess(pid: number): void {
+ this.#healthCheckStates.delete(pid)
+ }
+
+ /**
+ * Record that a job failed for a process
+ */
+ recordJobFailure(pid: number): void {
+ const state = this.#healthCheckStates.get(pid)
+ if (state != null) {
+ state.lastJobFailed = true
+ }
+ }
+
+ /**
+ * Record that a job succeeded for a process
+ */
+ recordJobSuccess(pid: number): void {
+ const state = this.#healthCheckStates.get(pid)
+ if (state != null) {
+ state.lastJobFailed = false
+ }
+ }
+
+ /**
+ * Assess the health of a process and return why it's not healthy, or null if healthy
+ */
+ assessHealth(
+ process: HealthCheckable,
+ overrideReason?: WhyNotHealthy,
+ ): WhyNotHealthy | null {
+ if (overrideReason != null) return overrideReason
+
+ const state = this.#healthCheckStates.get(process.pid)
+ if (state != null && state.healthCheckFailures > 0) {
+ return "unhealthy"
+ }
+
+ return this.healthStrategy.assess(process, this.options)
+ }
+
+ /**
+ * Check if a process is healthy
+ */
+ isHealthy(process: HealthCheckable, overrideReason?: WhyNotHealthy): boolean {
+ return this.assessHealth(process, overrideReason) == null
+ }
+
+ /**
+ * Assess why a process is not ready (combines health and business)
+ */
+ assessReadiness(
+ process: HealthCheckable,
+ overrideReason?: WhyNotHealthy,
+ ): WhyNotReady | null {
+ return !process.idle ? "busy" : this.assessHealth(process, overrideReason)
+ }
+
+ /**
+ * Check if a process is ready to handle tasks
+ */
+ isReady(process: HealthCheckable, overrideReason?: WhyNotHealthy): boolean {
+ return this.assessReadiness(process, overrideReason) == null
+ }
+
+ /**
+ * Run a health check on a process if needed
+ */
+ maybeRunHealthcheck(
+ process: HealthCheckable & { execTask: (task: Task) => boolean },
+ ): Task | undefined {
+ const hcc = this.options.healthCheckCommand
+ // if there's no health check command, no-op.
+ if (hcc == null || blank(hcc)) return
+
+ // if the prior health check failed, .ready will be false
+ if (!this.isReady(process)) return
+
+ const state = this.#healthCheckStates.get(process.pid)
+ if (state == null) return
+
+ if (
+ state.lastJobFailed ||
+ (this.options.healthCheckIntervalMillis > 0 &&
+ Date.now() - state.lastHealthCheck >
+ this.options.healthCheckIntervalMillis)
+ ) {
+ state.lastHealthCheck = Date.now()
+ const t = new Task(hcc, SimpleParser)
+ t.promise
+ .catch((err) => {
+ this.emitter.emit(
+ "healthCheckError",
+ err instanceof Error ? err : new Error(String(err)),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
+ process as any, // Type assertion for event emission
+ )
+ state.healthCheckFailures++
+ // BatchCluster will see we're unhealthy and reap us later
+ })
+ .finally(() => {
+ state.lastHealthCheck = Date.now()
+ })
+
+ // Execute the health check task on the process
+ if (process.execTask(t as Task)) {
+ return t as Task
+ }
+ }
+ return
+ }
+
+ /**
+ * Get health statistics for monitoring
+ */
+ getHealthStats(): {
+ monitoredProcesses: number
+ totalHealthCheckFailures: number
+ processesWithFailures: number
+ } {
+ let totalFailures = 0
+ let processesWithFailures = 0
+
+ for (const state of this.#healthCheckStates.values()) {
+ totalFailures += state.healthCheckFailures
+ if (state.healthCheckFailures > 0) {
+ processesWithFailures++
+ }
+ }
+
+ return {
+ monitoredProcesses: this.#healthCheckStates.size,
+ totalHealthCheckFailures: totalFailures,
+ processesWithFailures,
+ }
+ }
+
+ /**
+ * Reset health check failures for a process (useful for recovery scenarios)
+ */
+ resetHealthCheckFailures(pid: number): void {
+ const state = this.#healthCheckStates.get(pid)
+ if (state != null) {
+ state.healthCheckFailures = 0
+ }
+ }
+
+ /**
+ * Get health check state for a specific process
+ */
+ getProcessHealthState(pid: number) {
+ return this.#healthCheckStates.get(pid)
+ }
+}
diff --git a/src/ProcessPoolManager.spec.ts b/src/ProcessPoolManager.spec.ts
new file mode 100644
index 0000000..a372dc8
--- /dev/null
+++ b/src/ProcessPoolManager.spec.ts
@@ -0,0 +1,253 @@
+import events from "node:events"
+import {
+ currentTestPids,
+ expect,
+ processFactory,
+ setFailratePct,
+ setIgnoreExit,
+} from "./_chai.spec"
+import { delay, until } from "./Async"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { DefaultTestOptions } from "./DefaultTestOptions.spec"
+import { verifyOptions } from "./OptionsVerifier"
+import { ProcessPoolManager } from "./ProcessPoolManager"
+
+describe("ProcessPoolManager", function () {
+ let poolManager: ProcessPoolManager
+ let emitter: BatchClusterEmitter
+
+ const onIdle = () => {
+ // callback for when pool manager needs to signal idle state
+ }
+
+ beforeEach(function () {
+ setFailratePct(0) // no failures for pool manager tests
+ setIgnoreExit(false)
+ emitter = new events.EventEmitter() as BatchClusterEmitter
+
+ const options = verifyOptions({
+ ...DefaultTestOptions,
+ processFactory,
+ observer: emitter,
+ })
+
+ poolManager = new ProcessPoolManager(options, emitter, onIdle)
+ })
+
+ afterEach(async function () {
+ if (poolManager != null) {
+ await poolManager.closeChildProcesses(false)
+ // Wait for processes to actually exit
+ await until(async () => (await currentTestPids()).length === 0, 5000)
+ }
+ })
+
+ describe("initial state", function () {
+ it("should start with no processes", function () {
+ expect(poolManager.procCount).to.eql(0)
+ expect(poolManager.busyProcCount).to.eql(0)
+ expect(poolManager.startingProcCount).to.eql(0)
+ expect(poolManager.spawnedProcCount).to.eql(0)
+ expect(poolManager.processes).to.eql([])
+ expect(poolManager.findReadyProcess()).to.be.undefined
+ })
+
+ it("should return empty pids array", function () {
+ expect(poolManager.pids()).to.eql([])
+ })
+ })
+
+ describe("process spawning", function () {
+ it("should spawn processes when there are pending tasks", async function () {
+ const pendingTaskCount = 2
+ await poolManager.maybeSpawnProcs(pendingTaskCount, false)
+
+ expect(poolManager.procCount).to.be.greaterThan(0)
+ expect(poolManager.spawnedProcCount).to.be.greaterThan(0)
+
+ // Wait for processes to be ready
+ await until(() => poolManager.findReadyProcess() != null, 2000)
+ expect(poolManager.findReadyProcess()).to.not.be.undefined
+ })
+
+ it("should not spawn more processes than maxProcs", async function () {
+ const maxProcs = 2
+ poolManager.setMaxProcs(maxProcs)
+
+ // Try to spawn more than maxProcs
+ await poolManager.maybeSpawnProcs(5, false)
+
+ expect(poolManager.procCount).to.be.at.most(maxProcs)
+ })
+
+ it("should not spawn processes when ended", async function () {
+ await poolManager.maybeSpawnProcs(2, true) // ended = true
+
+ expect(poolManager.procCount).to.eql(0)
+ expect(poolManager.spawnedProcCount).to.eql(0)
+ })
+
+ it("should spawn multiple processes for multiple pending tasks", async function () {
+ const pendingTaskCount = 3
+ poolManager.setMaxProcs(4)
+
+ await poolManager.maybeSpawnProcs(pendingTaskCount, false)
+
+ // Should spawn up to the number of pending tasks or maxProcs
+ expect(poolManager.procCount).to.be.at.least(1)
+ expect(poolManager.procCount).to.be.at.most(Math.min(pendingTaskCount, 4))
+ })
+ })
+
+ describe("process management", function () {
+ beforeEach(async function () {
+ // Spawn some processes for testing
+ await poolManager.maybeSpawnProcs(2, false)
+ await until(() => poolManager.procCount >= 1, 2000)
+ })
+
+ it("should track process PIDs", function () {
+ const pids = poolManager.pids()
+ expect(pids.length).to.be.greaterThan(0)
+ expect(pids.every((pid) => typeof pid === "number" && pid > 0)).to.be.true
+ })
+
+ it("should find ready processes", async function () {
+ await until(() => poolManager.findReadyProcess() != null, 2000)
+ const readyProcess = poolManager.findReadyProcess()
+ expect(readyProcess).to.not.be.undefined
+ expect(readyProcess?.ready).to.be.true
+ })
+
+ it("should vacuum unhealthy processes", async function () {
+ // Wait for processes to be ready
+ await until(() => poolManager.findReadyProcess() != null, 2000)
+
+ const initialCount = poolManager.procCount
+ expect(initialCount).to.be.greaterThan(0)
+
+ // Vacuum should not remove healthy processes
+ await poolManager.vacuumProcs()
+ expect(poolManager.procCount).to.eql(initialCount)
+ })
+
+ it("should reduce process count when maxProcs is lowered", async function () {
+ // Ensure we have multiple processes
+ poolManager.setMaxProcs(3)
+ await poolManager.maybeSpawnProcs(3, false)
+ await until(() => poolManager.procCount >= 2, 2000)
+
+ const initialCount = poolManager.procCount
+
+ // Reduce maxProcs
+ poolManager.setMaxProcs(1)
+ await poolManager.vacuumProcs()
+
+ // Should eventually reduce to 1 process (may take time for idle processes to be reaped)
+ await until(() => poolManager.procCount <= 1, 3000)
+ expect(poolManager.procCount).to.be.at.most(1)
+ expect(poolManager.procCount).to.be.lessThanOrEqual(initialCount)
+ })
+ })
+
+ describe("process lifecycle", function () {
+ it("should close all processes gracefully", async function () {
+ await poolManager.maybeSpawnProcs(2, false)
+ await until(() => poolManager.procCount >= 1, 2000)
+
+ const initialPids = poolManager.pids()
+ expect(initialPids.length).to.be.greaterThan(0)
+
+ await poolManager.closeChildProcesses(true)
+
+ expect(poolManager.procCount).to.eql(0)
+
+ // Wait for processes to actually exit
+ await until(async () => {
+ const remainingPids = await currentTestPids()
+ return (
+ remainingPids.filter((pid) => initialPids.includes(pid)).length === 0
+ )
+ }, 5000)
+ })
+
+ it("should close all processes forcefully", async function () {
+ await poolManager.maybeSpawnProcs(2, false)
+ await until(() => poolManager.procCount >= 1, 2000)
+
+ const initialPids = poolManager.pids()
+ expect(initialPids.length).to.be.greaterThan(0)
+
+ await poolManager.closeChildProcesses(false)
+
+ expect(poolManager.procCount).to.eql(0)
+
+ // Wait for processes to actually exit
+ await until(async () => {
+ const remainingPids = await currentTestPids()
+ return (
+ remainingPids.filter((pid) => initialPids.includes(pid)).length === 0
+ )
+ }, 5000)
+ })
+ })
+
+ describe("process counting", function () {
+ it("should track starting processes", async function () {
+ // Start spawning processes but don't wait for completion
+ const spawnPromise = poolManager.maybeSpawnProcs(2, false)
+
+ // Should show starting processes initially
+ await delay(50) // Give it a moment to start
+ const totalProcs = poolManager.procCount
+ const startingProcs = poolManager.startingProcCount
+
+ expect(totalProcs).to.be.greaterThan(0)
+ expect(startingProcs).to.be.greaterThan(0)
+
+ await spawnPromise
+
+ // Wait for processes to be ready
+ await until(() => poolManager.startingProcCount === 0, 2000)
+ expect(poolManager.startingProcCount).to.eql(0)
+ })
+
+ it("should track busy vs idle processes", async function () {
+ await poolManager.maybeSpawnProcs(1, false)
+ await until(() => poolManager.findReadyProcess() != null, 2000)
+
+ // Initially all processes should be idle (not busy)
+ expect(poolManager.busyProcCount).to.eql(0)
+
+ const readyProcess = poolManager.findReadyProcess()
+ expect(readyProcess).to.not.be.undefined
+ expect(readyProcess?.idle).to.be.true
+ })
+ })
+
+ describe("event integration", function () {
+ it("should work with emitter for process lifecycle events", async function () {
+ const childStartEvents: any[] = []
+ const childEndEvents: any[] = []
+
+ emitter.on("childStart", (proc) => {
+ childStartEvents.push(proc)
+ })
+
+ emitter.on("childEnd", (proc, reason) => {
+ childEndEvents.push({ proc, reason })
+ })
+
+ await poolManager.maybeSpawnProcs(1, false)
+ await until(() => childStartEvents.length >= 1, 2000)
+
+ expect(childStartEvents.length).to.be.greaterThan(0)
+
+ await poolManager.closeChildProcesses(true)
+ await until(() => childEndEvents.length >= 1, 2000)
+
+ expect(childEndEvents.length).to.be.greaterThan(0)
+ expect(childEndEvents[0].reason).to.eql("ending")
+ })
+ })
+})
diff --git a/src/ProcessPoolManager.ts b/src/ProcessPoolManager.ts
new file mode 100644
index 0000000..fd62fed
--- /dev/null
+++ b/src/ProcessPoolManager.ts
@@ -0,0 +1,309 @@
+import timers from "node:timers"
+import { count, filterInPlace } from "./Array"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { BatchProcess } from "./BatchProcess"
+import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"
+import { asError } from "./Error"
+import { Logger } from "./Logger"
+import { ProcessHealthMonitor } from "./ProcessHealthMonitor"
+import { Task } from "./Task"
+import { Timeout, thenOrTimeout } from "./Timeout"
+
+/**
+ * Manages the lifecycle of a pool of BatchProcess instances.
+ * Handles spawning, health monitoring, and cleanup of child processes.
+ */
+export class ProcessPoolManager {
+ readonly #procs: BatchProcess[] = []
+ readonly #logger: () => Logger
+ readonly #healthMonitor: ProcessHealthMonitor
+ #nextSpawnTime = 0
+ #lastPidsCheckTime = 0
+ #spawnedProcs = 0
+
+ constructor(
+ private readonly options: CombinedBatchProcessOptions,
+ private readonly emitter: BatchClusterEmitter,
+ private readonly onIdle: () => void,
+ ) {
+ this.#logger = options.logger
+ this.#healthMonitor = new ProcessHealthMonitor(options, emitter)
+ }
+
+ /**
+ * Get all current processes
+ */
+ get processes(): readonly BatchProcess[] {
+ return this.#procs
+ }
+
+ /**
+ * Get the current number of spawned child processes
+ */
+ get procCount(): number {
+ return this.#procs.length
+ }
+
+ /**
+ * Alias for procCount to match BatchCluster interface
+ */
+ get processCount(): number {
+ return this.procCount
+ }
+
+ /**
+ * Get the current number of child processes currently servicing tasks
+ */
+ get busyProcCount(): number {
+ return count(
+ this.#procs,
+ // don't count procs that are starting up as "busy":
+ (ea) => !ea.starting && !ea.ending && !ea.idle,
+ )
+ }
+
+ /**
+ * Get the current number of starting processes
+ */
+ get startingProcCount(): number {
+ return count(
+ this.#procs,
+ // don't count procs that are starting up as "busy":
+ (ea) => ea.starting && !ea.ending,
+ )
+ }
+
+ /**
+ * Get the current number of ready processes
+ */
+ get readyProcCount(): number {
+ return count(this.#procs, (ea) => ea.ready)
+ }
+
+ /**
+ * Get the total number of child processes created by this instance
+ */
+ get spawnedProcCount(): number {
+ return this.#spawnedProcs
+ }
+
+ /**
+ * Get the milliseconds until the next spawn is allowed
+ */
+ get msBeforeNextSpawn(): number {
+ return Math.max(0, this.#nextSpawnTime - Date.now())
+ }
+
+ /**
+ * Get all currently running tasks from all processes
+ */
+ currentTasks(): Task[] {
+ const tasks: Task[] = []
+ for (const proc of this.#procs) {
+ if (proc.currentTask != null) {
+ tasks.push(proc.currentTask)
+ }
+ }
+ return tasks
+ }
+
+ /**
+ * Find the first ready process that can handle a new task
+ */
+ findReadyProcess(): BatchProcess | undefined {
+ return this.#procs.find((ea) => ea.ready)
+ }
+
+ /**
+ * Verify that each BatchProcess PID is actually alive.
+ * @return the spawned PIDs that are still in the process table.
+ */
+ pids(): number[] {
+ const arr: number[] = []
+ for (const proc of [...this.#procs]) {
+ if (proc != null && proc.running()) {
+ arr.push(proc.pid)
+ }
+ }
+ return arr
+ }
+
+ /**
+ * Shut down any currently-running child processes.
+ */
+ async closeChildProcesses(gracefully = true): Promise {
+ const procs = [...this.#procs]
+ this.#procs.length = 0
+ await Promise.all(
+ procs.map((proc) =>
+ proc
+ .end(gracefully, "ending")
+ .catch((err) => this.emitter.emit("endError", asError(err), proc)),
+ ),
+ )
+ }
+
+ /**
+ * Run maintenance on currently spawned child processes.
+ * Removes unhealthy processes and enforces maxProcs limit.
+ */
+ vacuumProcs(): Promise {
+ this.#maybeCheckPids()
+ const endPromises: Promise[] = []
+ let pidsToReap = Math.max(0, this.#procs.length - this.options.maxProcs)
+
+ filterInPlace(this.#procs, (proc) => {
+ // Only check `.idle` (not `.ready`) procs. We don't want to reap busy
+ // procs unless we're ending, and unhealthy procs (that we want to reap)
+ // won't be `.ready`.
+ if (proc.idle) {
+ // don't reap more than pidsToReap pids. We can't use #procs.length
+ // within filterInPlace because #procs.length only changes at iteration
+ // completion: the prior impl resulted in all idle pids getting reaped
+ // when maxProcs was reduced.
+ const why = proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null)
+ if (why != null) {
+ endPromises.push(proc.end(true, why))
+ return false
+ }
+ proc.maybeRunHealthcheck()
+ }
+ return true
+ })
+
+ return Promise.all(endPromises)
+ }
+
+ /**
+ * Spawn new processes if needed based on pending task count and capacity
+ */
+ async maybeSpawnProcs(
+ pendingTaskCount: number,
+ ended: boolean,
+ ): Promise {
+ let procsToSpawn = this.#procsToSpawn(pendingTaskCount)
+
+ if (ended || this.#nextSpawnTime > Date.now() || procsToSpawn === 0) {
+ return
+ }
+
+ // prevent concurrent runs:
+ this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay()
+
+ for (let i = 0; i < procsToSpawn; i++) {
+ if (ended) {
+ break
+ }
+
+ // Kick the lock down the road:
+ this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay()
+ this.#spawnedProcs++
+
+ try {
+ const proc = this.#spawnNewProc()
+ const result = await thenOrTimeout(
+ proc,
+ this.options.spawnTimeoutMillis,
+ )
+ if (result === Timeout) {
+ void proc
+ .then((bp) => {
+ void bp.end(false, "startError")
+ this.emitter.emit(
+ "startError",
+ asError(
+ "Failed to spawn process in " +
+ this.options.spawnTimeoutMillis +
+ "ms",
+ ),
+ bp,
+ )
+ })
+ .catch((err) => {
+ // this should only happen if the processFactory throws a
+ // rejection:
+ this.emitter.emit("startError", asError(err))
+ })
+ } else {
+ this.#logger().debug(
+ "ProcessPoolManager.maybeSpawnProcs() started healthy child process",
+ { pid: result.pid },
+ )
+ }
+
+ // tasks may have been popped off or setMaxProcs may have reduced
+ // maxProcs. Do this at the end so the for loop ends properly.
+ procsToSpawn = Math.min(
+ this.#procsToSpawn(pendingTaskCount),
+ procsToSpawn,
+ )
+ } catch (err) {
+ this.emitter.emit("startError", asError(err))
+ }
+ }
+
+ // YAY WE MADE IT.
+ // Only let more children get spawned after minDelay:
+ const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis)
+ this.#nextSpawnTime = Date.now() + delay
+
+ // And schedule #onIdle for that time:
+ timers.setTimeout(this.onIdle, delay).unref()
+ }
+
+ /**
+ * Update the maximum number of processes allowed
+ */
+ setMaxProcs(maxProcs: number): void {
+ this.options.maxProcs = maxProcs
+ }
+
+ #maybeCheckPids(): void {
+ if (
+ this.options.cleanupChildProcs &&
+ this.options.pidCheckIntervalMillis > 0 &&
+ this.#lastPidsCheckTime + this.options.pidCheckIntervalMillis < Date.now()
+ ) {
+ this.#lastPidsCheckTime = Date.now()
+ void this.pids()
+ }
+ }
+
+ #maxSpawnDelay(): number {
+ // 10s delay is certainly long enough for .spawn() to return, even on a
+ // loaded windows machine.
+ return Math.max(10_000, this.options.spawnTimeoutMillis)
+ }
+
+ #procsToSpawn(pendingTaskCount: number): number {
+ const remainingCapacity = this.options.maxProcs - this.#procs.length
+
+ // take into account starting procs, so one task doesn't result in multiple
+ // processes being spawned:
+ const requestedCapacity = pendingTaskCount - this.startingProcCount
+
+ const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity))
+
+ return this.options.minDelayBetweenSpawnMillis === 0
+ ? // we can spin up multiple processes in parallel.
+ atLeast0
+ : // Don't spin up more than 1:
+ Math.min(1, atLeast0)
+ }
+
+ // must only be called by this.maybeSpawnProcs()
+ async #spawnNewProc(): Promise {
+ // no matter how long it takes to spawn, always push the result into #procs
+ // so we don't leak child processes:
+ const procOrPromise = this.options.processFactory()
+ const proc = await procOrPromise
+ const result = new BatchProcess(
+ proc,
+ this.options,
+ this.onIdle,
+ this.#healthMonitor,
+ )
+ this.#procs.push(result)
+ return result
+ }
+}
diff --git a/src/ProcessTerminator.spec.ts b/src/ProcessTerminator.spec.ts
new file mode 100644
index 0000000..68f29f7
--- /dev/null
+++ b/src/ProcessTerminator.spec.ts
@@ -0,0 +1,687 @@
+import events from "node:events"
+import stream from "node:stream"
+import { expect } from "./_chai.spec"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
+import { logger } from "./Logger"
+import { SimpleParser } from "./Parser"
+import { ProcessTerminator } from "./ProcessTerminator"
+import { Task } from "./Task"
+
+describe("ProcessTerminator", function () {
+ let terminator: ProcessTerminator
+ let mockProcess: MockChildProcess
+ let emitter: BatchClusterEmitter
+ let options: InternalBatchProcessOptions
+ let isRunningResult: boolean
+ let childEndEvents: { process: any; reason: string }[]
+
+ // Mock child process class
+ class MockChildProcess extends events.EventEmitter {
+ pid = 12345
+ stdin = new MockWritableStream()
+ stdout = new MockReadableStream()
+ stderr = new MockReadableStream()
+ killed = false
+ disconnected = false
+
+ kill() {
+ this.killed = true
+ return true
+ }
+
+ disconnect() {
+ this.disconnected = true
+ }
+
+ unref() {
+ // no-op for tests
+ }
+ }
+
+ class MockWritableStream extends stream.Writable {
+ override destroyed = false
+ override writable = true
+ data: string[] = []
+
+ override _write(chunk: any, _encoding: any, callback: any) {
+ this.data.push(chunk.toString())
+ callback()
+ }
+
+ override end(data?: any): this {
+ if (data != null) {
+ this.data.push(data.toString())
+ }
+ this.writable = false
+ super.end()
+ return this
+ }
+
+ override destroy(): this {
+ this.destroyed = true
+ super.destroy()
+ return this
+ }
+ }
+
+ class MockReadableStream extends stream.Readable {
+ override destroyed = false
+
+ override _read() {
+ // no-op for tests
+ }
+
+ override destroy(): this {
+ this.destroyed = true
+ super.destroy()
+ return this
+ }
+ }
+
+ beforeEach(function () {
+ emitter = new events.EventEmitter() as BatchClusterEmitter
+ childEndEvents = []
+
+ // Track childEnd events
+ emitter.on("childEnd", (process: any, reason: string) => {
+ childEndEvents.push({ process, reason })
+ })
+
+ options = {
+ logger,
+ observer: emitter,
+ cleanupChildProcs: true,
+ endGracefulWaitTimeMillis: 1000,
+ exitCommand: "exit",
+ spawnTimeoutMillis: 5000,
+ taskTimeoutMillis: 30000,
+ streamFlushMillis: 100,
+ versionCommand: "version",
+ healthCheckCommand: "healthcheck",
+ healthCheckIntervalMillis: 60000,
+ maxTasksPerProcess: 100,
+ maxIdleMsPerProcess: 300000,
+ maxFailedTasksPerProcess: 3,
+ maxProcAgeMillis: 600000,
+ pass: "PASS",
+ fail: "FAIL",
+ passRE: /PASS/,
+ failRE: /FAIL/,
+ maxProcs: 4,
+ onIdleIntervalMillis: 10,
+ maxReasonableProcessFailuresPerMinute: 10,
+ minDelayBetweenSpawnMillis: 100,
+ pidCheckIntervalMillis: 150,
+ }
+
+ terminator = new ProcessTerminator(options)
+ mockProcess = new MockChildProcess()
+ isRunningResult = true
+ })
+
+ function createMockTask(
+ taskId = 1,
+ command = "test",
+ pending = true,
+ ): Task {
+ const task = new Task(command, SimpleParser)
+ if (!pending) {
+ // Simulate task completion by calling onStdout with PASS token
+ task.onStart(options)
+ task.onStdout("PASS")
+ }
+ // Override taskId for testing
+ Object.defineProperty(task, "taskId", { value: taskId, writable: true })
+ return task as Task
+ }
+
+ function mockIsRunning(): boolean {
+ return isRunningResult
+ }
+
+ describe("basic termination", function () {
+ it("should terminate process without current task", async function () {
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined, // no current task
+ 999, // startup task id
+ true, // graceful
+ false, // not exited
+ mockIsRunning,
+ )
+
+ // Should send exit command
+ expect(mockProcess.stdin.data).to.include("exit\n")
+
+ // Should destroy streams
+ expect(mockProcess.stdin.destroyed).to.be.true
+ expect(mockProcess.stdout.destroyed).to.be.true
+ expect(mockProcess.stderr.destroyed).to.be.true
+
+ // Should disconnect
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should terminate process forcefully when not graceful", async function () {
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ false, // not graceful
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.stdin.data).to.include("exit\n")
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should handle process that is already exited", async function () {
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ true, // already exited
+ mockIsRunning,
+ )
+
+ expect(mockProcess.stdin.data).to.include("exit\n")
+ expect(mockProcess.disconnected).to.be.true
+ })
+ })
+
+ describe("task completion handling", function () {
+ it("should wait for non-startup task to complete gracefully", async function () {
+ const task = createMockTask(1, "test command", true)
+ let taskCompleted = false
+
+ // Simulate task completion after a delay
+ setTimeout(() => {
+ taskCompleted = true
+ task.onStart(options)
+ task.onStdout("PASS") // Complete the task
+ }, 50)
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ task,
+ 999, // different from task ID
+ true, // graceful
+ false,
+ mockIsRunning,
+ )
+
+ expect(taskCompleted).to.be.true
+ expect(task.state !== "pending").to.be.true
+ })
+
+ it("should reject pending task if termination timeout occurs", async function () {
+ const task = createMockTask(1, "slow task", true)
+ let taskRejected = false
+ let rejectionReason = ""
+
+ task.promise.catch((err) => {
+ taskRejected = true
+ rejectionReason = err.message
+ })
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ task,
+ 999,
+ false, // not graceful - shorter timeout
+ false,
+ mockIsRunning,
+ )
+
+ expect(taskRejected).to.be.true
+ expect(rejectionReason).to.include(
+ "Process terminated before task completed",
+ )
+ })
+
+ it("should skip task completion wait for startup task", async function () {
+ const startupTask = createMockTask(999, "version", true)
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ startupTask,
+ 999, // same as task ID - is startup task
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ // Should not wait for or reject startup task
+ expect(startupTask.pending).to.be.true
+ })
+
+ it("should skip task completion wait when no current task", async function () {
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined, // no current task
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ // Should complete without errors
+ expect(mockProcess.disconnected).to.be.true
+ })
+ })
+
+ describe("stream handling", function () {
+ it("should remove error listeners from all streams", async function () {
+ // Add some error listeners
+ const errorHandler = () => {
+ // no-op for test
+ }
+ mockProcess.on("error", errorHandler)
+ mockProcess.stdin.on("error", errorHandler)
+ mockProcess.stdout.on("error", errorHandler)
+ mockProcess.stderr.on("error", errorHandler)
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ // Error listeners should be removed
+ expect(mockProcess.listenerCount("error")).to.equal(0)
+ expect(mockProcess.stdin.listenerCount("error")).to.equal(0)
+ expect(mockProcess.stdout.listenerCount("error")).to.equal(0)
+ expect(mockProcess.stderr.listenerCount("error")).to.equal(0)
+ })
+
+ it("should send exit command if stdin is writable", async function () {
+ mockProcess.stdin.writable = true
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.stdin.data).to.include("exit\n")
+ })
+
+ it("should skip exit command if stdin is not writable", async function () {
+ mockProcess.stdin.writable = false
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.stdin.data).to.be.empty
+ })
+
+ it("should handle missing exit command gracefully", async function () {
+ const optionsNoExit = { ...options, exitCommand: undefined }
+ const terminatorNoExit = new ProcessTerminator(optionsNoExit)
+
+ await terminatorNoExit.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ // Should complete without sending exit command
+ expect(mockProcess.stdin.data).to.be.empty
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should destroy all streams", async function () {
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.stdin.destroyed).to.be.true
+ expect(mockProcess.stdout.destroyed).to.be.true
+ expect(mockProcess.stderr.destroyed).to.be.true
+ })
+ })
+
+ describe("graceful shutdown", function () {
+ it("should wait for process to exit gracefully", async function () {
+ let killCalled = false
+ mockProcess.kill = () => {
+ killCalled = true
+ return true
+ }
+
+ // Simulate process still running initially, then stopping
+ let callCount = 0
+ const mockIsRunningGraceful = () => {
+ callCount++
+ if (callCount <= 2) {
+ return true // Still running for first few checks
+ }
+ return false // Then stops running
+ }
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true, // graceful
+ false, // not already exited
+ mockIsRunningGraceful,
+ )
+
+ expect(killCalled).to.be.false // Should not need to kill
+ })
+
+ it("should send SIGTERM if process doesn't exit gracefully", async function () {
+ let killCalled = false
+ mockProcess.kill = () => {
+ killCalled = true
+ isRunningResult = false // Process stops after kill signal
+ return true
+ }
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true, // graceful
+ false, // not already exited
+ mockIsRunning, // Always returns true until killed
+ )
+
+ expect(killCalled).to.be.true
+ })
+
+ it("should skip graceful shutdown when cleanup disabled", async function () {
+ const optionsNoCleanup = { ...options, cleanupChildProcs: false }
+ const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup)
+
+ let killCalled = false
+ mockProcess.kill = () => {
+ killCalled = true
+ return true
+ }
+
+ await terminatorNoCleanup.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(killCalled).to.be.false
+ })
+
+ it("should skip graceful shutdown when wait time is 0", async function () {
+ const optionsNoWait = { ...options, endGracefulWaitTimeMillis: 0 }
+ const terminatorNoWait = new ProcessTerminator(optionsNoWait)
+
+ let killCalled = false
+ mockProcess.kill = () => {
+ killCalled = true
+ return true
+ }
+
+ await terminatorNoWait.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(killCalled).to.be.false
+ })
+ })
+
+ describe("force killing", function () {
+ it("should complete termination even with stubborn process", async function () {
+ // Process keeps running even after signals
+ const mockIsRunningStubborn = () => true
+
+ // Should complete without throwing
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunningStubborn,
+ )
+
+ // Should still disconnect and destroy streams
+ expect(mockProcess.disconnected).to.be.true
+ expect(mockProcess.stdin.destroyed).to.be.true
+ })
+
+ it("should complete termination when cleanup disabled", async function () {
+ const optionsNoCleanup = { ...options, cleanupChildProcs: false }
+ const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup)
+
+ await terminatorNoCleanup.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ () => true, // Always running
+ )
+
+ // Should still complete basic cleanup
+ expect(mockProcess.disconnected).to.be.true
+ expect(mockProcess.stdin.destroyed).to.be.true
+ })
+
+ it("should handle process with no PID gracefully", async function () {
+ ;(mockProcess as any).pid = undefined
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ () => true,
+ )
+
+ // Should complete without issues
+ expect(mockProcess.disconnected).to.be.true
+ expect(mockProcess.stdin.destroyed).to.be.true
+ })
+ })
+
+ describe("error handling", function () {
+ it("should handle stdin.end() errors gracefully", async function () {
+ mockProcess.stdin.end = () => {
+ throw new Error("EPIPE")
+ }
+
+ // Should not throw
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should handle stream destruction errors gracefully", async function () {
+ mockProcess.stdout.destroy = () => {
+ throw new Error("Stream error")
+ }
+
+ // Should not throw
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should handle disconnect errors gracefully", async function () {
+ mockProcess.disconnect = () => {
+ throw new Error("Disconnect error")
+ }
+
+ // Should not throw
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+ })
+ })
+
+ describe("edge cases", function () {
+ it("should handle null stderr stream", async function () {
+ ;(mockProcess as any).stderr = null
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should handle already completed task", async function () {
+ const completedTask = createMockTask(1, "completed", false)
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ completedTask,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(completedTask.state !== "pending").to.be.true
+ expect(mockProcess.disconnected).to.be.true
+ })
+
+ it("should handle process with undefined PID", async function () {
+ ;(mockProcess as any).pid = undefined
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ undefined,
+ 999,
+ true,
+ false,
+ mockIsRunning,
+ )
+
+ expect(mockProcess.disconnected).to.be.true
+ })
+ })
+
+ describe("timing and timeouts", function () {
+ it("should respect graceful task timeout", async function () {
+ const slowTask = createMockTask(1, "slow", true)
+ const startTime = Date.now()
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ slowTask,
+ 999,
+ true, // graceful - should wait up to 2000ms for task
+ false,
+ mockIsRunning,
+ )
+
+ const elapsed = Date.now() - startTime
+ // Should have waited some time but not too long
+ expect(elapsed).to.be.greaterThan(50)
+ expect(elapsed).to.be.lessThan(4000)
+ expect(slowTask.pending).to.be.false // Should be rejected
+ })
+
+ it("should respect non-graceful task timeout", async function () {
+ const slowTask = createMockTask(1, "slow", true)
+ const startTime = Date.now()
+
+ await terminator.terminate(
+ mockProcess as any,
+ "TestProcess(12345)",
+ slowTask,
+ 999,
+ false, // not graceful - should wait only 250ms for task
+ false,
+ mockIsRunning,
+ )
+
+ const elapsed = Date.now() - startTime
+ // Should have waited less time
+ expect(elapsed).to.be.lessThan(1000)
+ expect(slowTask.pending).to.be.false // Should be rejected
+ })
+ })
+})
diff --git a/src/ProcessTerminator.ts b/src/ProcessTerminator.ts
new file mode 100644
index 0000000..547bedf
--- /dev/null
+++ b/src/ProcessTerminator.ts
@@ -0,0 +1,185 @@
+import child_process from "node:child_process"
+import { until } from "./Async"
+import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"
+import { Logger } from "./Logger"
+import { kill } from "./Pids"
+import { destroy } from "./Stream"
+import { ensureSuffix } from "./String"
+import { Task } from "./Task"
+import { thenOrTimeout } from "./Timeout"
+
+/**
+ * Utility class for managing process termination lifecycle
+ */
+export class ProcessTerminator {
+ readonly #logger: () => Logger
+
+ constructor(private readonly opts: InternalBatchProcessOptions) {
+ this.#logger = opts.logger
+ }
+
+ /**
+ * Terminates a child process gracefully or forcefully
+ *
+ * @param proc The child process to terminate
+ * @param processName Name for logging purposes
+ * @param pid Process ID
+ * @param lastTask Current task being processed
+ * @param startupTaskId ID of the startup task
+ * @param gracefully Whether to wait for current task completion
+ * @param reason Reason for termination
+ * @param isExited Whether the process has already exited
+ * @param isRunning Function to check if process is still running
+ * @returns Promise that resolves when termination is complete
+ */
+ async terminate(
+ proc: child_process.ChildProcess,
+ processName: string,
+ lastTask: Task | undefined,
+ startupTaskId: number,
+ gracefully: boolean,
+ isExited: boolean,
+ isRunning: () => boolean,
+ ): Promise {
+ // Wait for current task to complete if graceful termination requested
+ await this.#waitForTaskCompletion(lastTask, startupTaskId, gracefully)
+
+ // Remove error listeners to prevent EPIPE errors during termination
+ this.#removeErrorListeners(proc)
+
+ // Send exit command to process
+ this.#sendExitCommand(proc)
+
+ // Destroy streams
+ this.#destroyStreams(proc)
+
+ // Handle graceful shutdown with timeouts
+ await this.#handleGracefulShutdown(proc, gracefully, isExited, isRunning)
+
+ // Force kill if still running
+ this.#forceKillIfRunning(proc, processName, isRunning)
+
+ // Final cleanup
+ try {
+ proc.disconnect?.()
+ } catch {
+ // Ignore disconnect errors
+ }
+ // Note: Caller should emit childEnd event with proper BatchProcess instance
+ }
+
+ async #waitForTaskCompletion(
+ lastTask: Task | undefined,
+ startupTaskId: number,
+ gracefully: boolean,
+ ): Promise {
+ // Don't wait for startup tasks or if no task is running
+ if (lastTask == null || lastTask.taskId === startupTaskId) {
+ return
+ }
+
+ try {
+ // Wait for the process to complete and streams to flush
+ await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250)
+ } catch {
+ // Ignore errors during task completion wait
+ }
+
+ // Reject task if still pending
+ if (lastTask.pending) {
+ lastTask.reject(
+ new Error(
+ `Process terminated before task completed (${JSON.stringify({
+ gracefully,
+ lastTask,
+ })})`,
+ ),
+ )
+ }
+ }
+
+ #removeErrorListeners(proc: child_process.ChildProcess): void {
+ // Remove error listeners to prevent EPIPE errors during termination
+ // See https://github.com/nodejs/node/issues/26828
+ for (const stream of [proc, proc.stdin, proc.stdout, proc.stderr]) {
+ stream?.removeAllListeners("error")
+ }
+ }
+
+ #sendExitCommand(proc: child_process.ChildProcess): void {
+ if (proc.stdin?.writable !== true) {
+ return
+ }
+
+ const exitCmd =
+ this.opts.exitCommand == null
+ ? null
+ : ensureSuffix(this.opts.exitCommand, "\n")
+
+ try {
+ proc.stdin.end(exitCmd)
+ } catch {
+ // Ignore errors when sending exit command
+ }
+ }
+
+ #destroyStreams(proc: child_process.ChildProcess): void {
+ // Destroy all streams to ensure cleanup
+ destroy(proc.stdin)
+ destroy(proc.stdout)
+ destroy(proc.stderr)
+ }
+
+ async #handleGracefulShutdown(
+ proc: child_process.ChildProcess,
+ gracefully: boolean,
+ isExited: boolean,
+ isRunning: () => boolean,
+ ): Promise {
+ if (
+ !this.opts.cleanupChildProcs ||
+ !gracefully ||
+ this.opts.endGracefulWaitTimeMillis <= 0 ||
+ isExited
+ ) {
+ return
+ }
+
+ // Wait for the exit command to take effect
+ await this.#awaitNotRunning(
+ this.opts.endGracefulWaitTimeMillis / 2,
+ isRunning,
+ )
+
+ // If still running, send kill signal
+ if (isRunning() && proc.pid != null) {
+ proc.kill()
+ }
+
+ // Wait for the signal handler to work
+ await this.#awaitNotRunning(
+ this.opts.endGracefulWaitTimeMillis / 2,
+ isRunning,
+ )
+ }
+
+ #forceKillIfRunning(
+ proc: child_process.ChildProcess,
+ processName: string,
+ isRunning: () => boolean,
+ ): void {
+ if (this.opts.cleanupChildProcs && proc.pid != null && isRunning()) {
+ this.#logger().warn(
+ `${processName}.terminate(): force-killing still-running child.`,
+ )
+ kill(proc.pid, true)
+ }
+ }
+
+ async #awaitNotRunning(
+ timeout: number,
+ isRunning: () => boolean,
+ ): Promise {
+ await until(() => !isRunning(), timeout)
+ }
+}
diff --git a/src/ProcpsChecker.spec.ts b/src/ProcpsChecker.spec.ts
new file mode 100644
index 0000000..5becc76
--- /dev/null
+++ b/src/ProcpsChecker.spec.ts
@@ -0,0 +1,33 @@
+import { expect } from "chai"
+import { describe, it } from "mocha"
+import { ProcpsMissingError, validateProcpsAvailable } from "./ProcpsChecker"
+
+describe("ProcpsChecker", () => {
+ describe("validateProcpsAvailable()", () => {
+ it("should not throw on systems with procps installed", () => {
+ // Since we're running tests, procps should be available
+ expect(() => validateProcpsAvailable()).to.not.throw()
+ })
+
+ it("should create appropriate error message for platform", () => {
+ const error = new ProcpsMissingError()
+ expect(error.name).to.equal("ProcpsMissingError")
+ expect(error.message).to.include("command not available")
+
+ // Message should be specific to platform
+ if (process.platform === "win32") {
+ expect(error.message).to.include("tasklist")
+ } else {
+ expect(error.message).to.include("ps")
+ expect(error.message).to.include("procps")
+ }
+ })
+
+ it("should preserve original error", () => {
+ const originalError = new Error("Command failed")
+ const procpsError = new ProcpsMissingError(originalError)
+
+ expect(procpsError.originalError).to.equal(originalError)
+ })
+ })
+})
diff --git a/src/ProcpsChecker.ts b/src/ProcpsChecker.ts
new file mode 100644
index 0000000..85a9fa1
--- /dev/null
+++ b/src/ProcpsChecker.ts
@@ -0,0 +1,52 @@
+import child_process from "node:child_process"
+import { existsSync, readdirSync } from "node:fs"
+import { isWin } from "./Platform"
+
+/**
+ * Error thrown when procps is missing on non-Windows systems
+ */
+export class ProcpsMissingError extends Error {
+ readonly originalError?: Error
+
+ constructor(originalError?: Error) {
+ const message = isWin
+ ? "tasklist command not available"
+ : "ps command not available. Please install procps package (e.g., 'apt-get install procps' on Ubuntu/Debian)"
+
+ super(message)
+ this.name = "ProcpsMissingError"
+
+ if (originalError != null) {
+ this.originalError = originalError
+ }
+ }
+}
+
+/**
+ * Check if the required process listing command is available
+ * @throws {ProcpsMissingError} if the command is not available
+ */
+export function validateProcpsAvailable(): void {
+ // on POSIX systems with a working /proc we can skip ps entirely
+ if (!isWin && existsSync("/proc")) {
+ const entries = readdirSync("/proc")
+ // if we see at least one numeric directory, assume /proc is usable
+ if (entries.some((d) => /^\d+$/.test(d))) {
+ return
+ }
+ // fall through to check `ps` if /proc is empty or unusable
+ }
+
+ try {
+ const command = isWin ? "tasklist" : "ps"
+ const args = isWin ? ["/NH", "/FO", "CSV", "/FI", "PID eq 1"] : ["-p", "1"]
+ const timeout = isWin ? 15_000 : 5_000 // 15s for Windows, 5s elsewhere
+
+ child_process.execFileSync(command, args, {
+ stdio: "pipe",
+ timeout,
+ })
+ } catch (err) {
+ throw new ProcpsMissingError(err instanceof Error ? err : undefined)
+ }
+}
diff --git a/src/Rate.spec.ts b/src/Rate.spec.ts
index 82f7b71..c2c6c1a 100644
--- a/src/Rate.spec.ts
+++ b/src/Rate.spec.ts
@@ -1,20 +1,22 @@
+import FakeTimers from "@sinonjs/fake-timers"
import { minuteMs } from "./BatchClusterOptions"
import { Rate } from "./Rate"
import { expect, times } from "./_chai.spec"
-const tk = require("timekeeper")
-
describe("Rate", () => {
const now = Date.now()
const r = new Rate()
+ let clock: FakeTimers.InstalledClock
beforeEach(() => {
- tk.freeze(now)
- // clear() must be called _after_ freezing time
+ clock = FakeTimers.install({ now: now })
+ // clear() must be called _after_ setting up fake timers
r.clear()
})
- after(() => tk.reset())
+ afterEach(() => {
+ clock.uninstall()
+ })
function expectRate(rate: Rate, epm: number, tol = 0.1) {
expect(rate.eventsPerMs).to.be.withinToleranceOf(epm, tol)
@@ -27,7 +29,7 @@ describe("Rate", () => {
})
it("maintains a rate of 0 after time with no events", () => {
- tk.freeze(now + minuteMs)
+ clock.tick(minuteMs)
expectRate(r, 0)
})
@@ -37,38 +39,39 @@ describe("Rate", () => {
() => {
times(cnt, () => r.onEvent())
expectRate(r, 0)
- tk.freeze(now + 100)
+ clock.tick(100)
expectRate(r, 0)
- tk.freeze(now + r.warmupMs + 1)
+ clock.tick(r.warmupMs - 100 + 1)
expectRate(r, cnt / r.warmupMs)
- tk.freeze(now + 2 * r.warmupMs)
+ clock.tick(r.warmupMs)
expectRate(r, cnt / (2 * r.warmupMs))
- tk.freeze(now + 3 * r.warmupMs)
+ clock.tick(r.warmupMs)
expectRate(r, cnt / (3 * r.warmupMs))
- tk.freeze(now + r.periodMs)
+ clock.tick(r.periodMs - 3 * r.warmupMs)
expectRate(r, 0)
- expect(r.msSinceLastEvent).to.eql(minuteMs)
+ expect(r.msSinceLastEvent).to.be.closeTo(r.periodMs, 5)
},
)
}
- for (const events of [5, 10, 100, 1000]) {
+ for (const events of [4, 32, 256, 1024]) {
it(
"calculates average rate for " + events + " events, and then decays",
() => {
const period = r.periodMs
- times(events, (i) => {
- tk.freeze(now + (period * i) / events)
+ times(events, () => {
+ clock.tick(r.periodMs / events)
r.onEvent()
})
+ const tickMs = r.periodMs / 4
expectRate(r, events / period, 0.3)
- tk.freeze(now + 1.25 * r.periodMs)
+ clock.tick(tickMs)
expectRate(r, 0.75 * (events / period), 0.3)
- tk.freeze(now + 1.5 * r.periodMs)
+ clock.tick(tickMs)
expectRate(r, 0.5 * (events / period), 0.3)
- tk.freeze(now + 1.75 * r.periodMs)
- expectRate(r, 0.25 * (events / period), 0.3)
- tk.freeze(now + 2 * r.periodMs)
+ clock.tick(tickMs)
+ expectRate(r, 0.25 * (events / period), 0.5)
+ clock.tick(tickMs)
expectRate(r, 0)
},
)
diff --git a/src/StreamHandler.spec.ts b/src/StreamHandler.spec.ts
new file mode 100644
index 0000000..0d1230a
--- /dev/null
+++ b/src/StreamHandler.spec.ts
@@ -0,0 +1,441 @@
+import child_process from "node:child_process"
+import events from "node:events"
+import { expect, processFactory } from "./_chai.spec"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { logger } from "./Logger"
+import {
+ StreamContext,
+ StreamHandler,
+ StreamHandlerOptions,
+} from "./StreamHandler"
+import { Task } from "./Task"
+
+describe("StreamHandler", function () {
+ let streamHandler: StreamHandler
+ let emitter: BatchClusterEmitter
+ let mockContext: StreamContext
+ let onErrorCalls: { reason: string; error: Error }[] = []
+ let endCalls: { gracefully: boolean; reason: string }[] = []
+
+ const options: StreamHandlerOptions = {
+ logger,
+ }
+
+ beforeEach(function () {
+ emitter = new events.EventEmitter() as BatchClusterEmitter
+ streamHandler = new StreamHandler(options, emitter)
+
+ onErrorCalls = []
+ endCalls = []
+
+ // Create a mock context that simulates BatchProcess behavior
+ mockContext = {
+ name: "TestProcess(12345)",
+ isEnding: () => false,
+ getCurrentTask: () => undefined,
+ onError: (reason: string, error: Error) => {
+ onErrorCalls.push({ reason, error })
+ },
+ end: (gracefully: boolean, reason: string) => {
+ endCalls.push({ gracefully, reason })
+ },
+ }
+ })
+
+ describe("initial state", function () {
+ it("should initialize correctly", function () {
+ expect(streamHandler).to.not.be.undefined
+
+ const stats = streamHandler.getStats()
+ expect(stats.handlerActive).to.be.true
+ expect(stats.emitterConnected).to.be.true
+ })
+ })
+
+ describe("stream setup", function () {
+ let mockProcess: child_process.ChildProcess
+
+ beforeEach(async function () {
+ // Create a real process for testing stream setup
+ mockProcess = await processFactory()
+ })
+
+ afterEach(function () {
+ if (mockProcess && !mockProcess.killed) {
+ mockProcess.kill()
+ }
+ })
+
+ it("should set up stream listeners on a child process", function () {
+ expect(() => {
+ streamHandler.setupStreamListeners(mockProcess, mockContext)
+ }).to.not.throw()
+
+ // Verify streams exist
+ expect(mockProcess.stdin).to.not.be.null
+ expect(mockProcess.stdout).to.not.be.null
+ expect(mockProcess.stderr).to.not.be.null
+ })
+
+ it("should throw error if stdin is missing", function () {
+ const invalidProcess = { stdin: null } as child_process.ChildProcess
+
+ expect(() => {
+ streamHandler.setupStreamListeners(invalidProcess, mockContext)
+ }).to.throw("Given proc had no stdin")
+ })
+
+ it("should throw error if stdout is missing", function () {
+ const invalidProcess = {
+ stdin: {
+ on: () => {
+ /* mock implementation */
+ },
+ }, // Mock stdin with on method
+ stdout: null,
+ } as any as child_process.ChildProcess
+
+ expect(() => {
+ streamHandler.setupStreamListeners(invalidProcess, mockContext)
+ }).to.throw("Given proc had no stdout")
+ })
+ })
+
+ describe("stdout processing", function () {
+ let mockTask: Task
+ let taskDataEvents: { data: any; task: any; context: any }[] = []
+ let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []
+
+ beforeEach(function () {
+ taskDataEvents = []
+ noTaskDataEvents = []
+
+ // Set up event listeners
+ emitter.on("taskData", (data, task, context) => {
+ taskDataEvents.push({ data, task, context })
+ })
+
+ emitter.on("noTaskData", (stdout, stderr, context) => {
+ noTaskDataEvents.push({ stdout, stderr, context })
+ })
+
+ // Create a mock task
+ mockTask = {
+ pending: true,
+ onStdout: () => {
+ /* mock implementation */
+ },
+ } as unknown as Task
+ })
+
+ it("should process stdout data with active task", function () {
+ mockContext.getCurrentTask = () => mockTask
+ const testData = "test output"
+
+ streamHandler.processStdout(testData, mockContext)
+
+ expect(taskDataEvents).to.have.length(1)
+ expect(taskDataEvents[0]?.data).to.eql(testData)
+ expect(taskDataEvents[0]?.task).to.eql(mockTask)
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should ignore stdout data when process is ending", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => true
+ const testData = "test output"
+
+ streamHandler.processStdout(testData, mockContext)
+
+ expect(taskDataEvents).to.have.length(0)
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should emit noTaskData and end process for stdout without task", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => false
+ const testData = "unexpected output"
+
+ streamHandler.processStdout(testData, mockContext)
+
+ expect(taskDataEvents).to.have.length(0)
+ expect(noTaskDataEvents).to.have.length(1)
+ expect(noTaskDataEvents[0]?.stdout).to.eql(testData)
+ expect(noTaskDataEvents[0]?.stderr).to.be.null
+ expect(endCalls).to.have.length(1)
+ expect(endCalls[0]?.gracefully).to.be.false
+ expect(endCalls[0]?.reason).to.eql("stdout.error")
+ })
+
+ it("should ignore blank stdout data", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => false
+
+ streamHandler.processStdout("", mockContext)
+ streamHandler.processStdout(" ", mockContext)
+ streamHandler.processStdout("\n", mockContext)
+
+ expect(taskDataEvents).to.have.length(0)
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should handle null stdout data", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => false
+
+ streamHandler.processStdout(null as any, mockContext)
+
+ expect(taskDataEvents).to.have.length(0)
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should not process stdout when task is not pending", function () {
+ const nonPendingTask = {
+ pending: false,
+ onStdout: () => {
+ /* mock implementation */
+ },
+ } as unknown as Task
+
+ mockContext.getCurrentTask = () => nonPendingTask
+ mockContext.isEnding = () => false
+ const testData = "test output"
+
+ streamHandler.processStdout(testData, mockContext)
+
+ expect(taskDataEvents).to.have.length(0)
+ expect(noTaskDataEvents).to.have.length(1)
+ expect(endCalls).to.have.length(1)
+ expect(endCalls[0]?.reason).to.eql("stdout.error")
+ })
+ })
+
+ describe("stderr processing", function () {
+ let mockTask: Task
+ let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []
+
+ beforeEach(function () {
+ noTaskDataEvents = []
+
+ // Set up event listeners
+ emitter.on("noTaskData", (stdout, stderr, context) => {
+ noTaskDataEvents.push({ stdout, stderr, context })
+ })
+
+ // Create a mock task
+ mockTask = {
+ pending: true,
+ onStderr: () => {
+ /* mock implementation */
+ },
+ } as unknown as Task
+ })
+
+ it("should process stderr data with active task", function () {
+ mockContext.getCurrentTask = () => mockTask
+ const testData = "error output"
+
+ streamHandler.processStderr(testData, mockContext)
+
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should ignore stderr data when process is ending", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => true
+ const testData = "error output"
+
+ streamHandler.processStderr(testData, mockContext)
+
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should emit noTaskData and end process for stderr without task", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => false
+ const testData = "unexpected error"
+
+ streamHandler.processStderr(testData, mockContext)
+
+ expect(noTaskDataEvents).to.have.length(1)
+ expect(noTaskDataEvents[0]?.stdout).to.be.null
+ expect(noTaskDataEvents[0]?.stderr).to.eql(testData)
+ expect(endCalls).to.have.length(1)
+ expect(endCalls[0]?.gracefully).to.be.false
+ expect(endCalls[0]?.reason).to.eql("stderr")
+ })
+
+ it("should ignore blank stderr data", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => false
+
+ streamHandler.processStderr("", mockContext)
+ streamHandler.processStderr(" ", mockContext)
+ streamHandler.processStderr("\n", mockContext)
+
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should not process stderr when task is not pending", function () {
+ const nonPendingTask = {
+ pending: false,
+ onStderr: () => {
+ /* mock implementation */
+ },
+ } as unknown as Task
+
+ mockContext.getCurrentTask = () => nonPendingTask
+ mockContext.isEnding = () => false
+ const testData = "error output"
+
+ streamHandler.processStderr(testData, mockContext)
+
+ expect(noTaskDataEvents).to.have.length(1)
+ expect(endCalls).to.have.length(1)
+ expect(endCalls[0]?.reason).to.eql("stderr")
+ })
+ })
+
+ describe("utility methods", function () {
+ it("should correctly identify blank data", function () {
+ expect(streamHandler.isBlankData("")).to.be.true
+ expect(streamHandler.isBlankData(" ")).to.be.true
+ expect(streamHandler.isBlankData("\n")).to.be.true
+ expect(streamHandler.isBlankData("\t")).to.be.true
+ expect(streamHandler.isBlankData(null)).to.be.true
+ expect(streamHandler.isBlankData(undefined)).to.be.true
+
+ expect(streamHandler.isBlankData("text")).to.be.false
+ expect(streamHandler.isBlankData(" text ")).to.be.false
+ expect(streamHandler.isBlankData(Buffer.from("data"))).to.be.false
+ })
+
+ it("should provide handler statistics", function () {
+ const stats = streamHandler.getStats()
+
+ expect(stats).to.have.property("handlerActive")
+ expect(stats).to.have.property("emitterConnected")
+ expect(stats.handlerActive).to.be.true
+ expect(stats.emitterConnected).to.be.true
+ })
+ })
+
+ describe("buffer handling", function () {
+ let mockTask: Task
+ let taskDataEvents: { data: any; task: any; context: any }[] = []
+
+ beforeEach(function () {
+ taskDataEvents = []
+
+ emitter.on("taskData", (data, task, context) => {
+ taskDataEvents.push({ data, task, context })
+ })
+
+ mockTask = {
+ pending: true,
+ onStdout: () => {
+ /* mock implementation */
+ },
+ onStderr: () => {
+ /* mock implementation */
+ },
+ } as unknown as Task
+ })
+
+ it("should handle Buffer data in stdout", function () {
+ mockContext.getCurrentTask = () => mockTask
+ const bufferData = Buffer.from("test buffer data")
+
+ streamHandler.processStdout(bufferData, mockContext)
+
+ expect(taskDataEvents).to.have.length(1)
+ expect(taskDataEvents[0]?.data).to.eql(bufferData)
+ })
+
+ it("should handle Buffer data in stderr", function () {
+ mockContext.getCurrentTask = () => mockTask
+ const bufferData = Buffer.from("error buffer data")
+
+ // Should not throw and should process normally
+ expect(() => {
+ streamHandler.processStderr(bufferData, mockContext)
+ }).to.not.throw()
+ })
+ })
+
+ describe("integration scenarios", function () {
+ let mockTask: Task
+ let taskDataEvents: { data: any; task: any; context: any }[] = []
+ let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []
+
+ beforeEach(function () {
+ taskDataEvents = []
+ noTaskDataEvents = []
+
+ emitter.on("taskData", (data, task, context) => {
+ taskDataEvents.push({ data, task, context })
+ })
+
+ emitter.on("noTaskData", (stdout, stderr, context) => {
+ noTaskDataEvents.push({ stdout, stderr, context })
+ })
+
+ mockTask = {
+ pending: true,
+ onStdout: () => {
+ /* mock implementation */
+ },
+ onStderr: () => {
+ /* mock implementation */
+ },
+ } as unknown as Task
+ })
+
+ it("should handle mixed stdout and stderr with active task", function () {
+ mockContext.getCurrentTask = () => mockTask
+
+ streamHandler.processStdout("stdout data", mockContext)
+ streamHandler.processStderr("stderr data", mockContext)
+
+ expect(taskDataEvents).to.have.length(1)
+ expect(taskDataEvents[0]?.data).to.eql("stdout data")
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+
+ it("should handle task completion scenario", function () {
+ // Start with active task
+ mockContext.getCurrentTask = () => mockTask
+ streamHandler.processStdout("initial output", mockContext)
+
+ expect(taskDataEvents).to.have.length(1)
+
+ // Task completes, no current task
+ mockContext.getCurrentTask = () => undefined
+ streamHandler.processStdout("stray output", mockContext)
+
+ expect(noTaskDataEvents).to.have.length(1)
+ expect(endCalls).to.have.length(1)
+ expect(endCalls[0]?.reason).to.eql("stdout.error")
+ })
+
+ it("should handle process ending scenario", function () {
+ mockContext.getCurrentTask = () => undefined
+ mockContext.isEnding = () => true
+
+ streamHandler.processStdout("final output", mockContext)
+ streamHandler.processStderr("final error", mockContext)
+
+ expect(taskDataEvents).to.have.length(0)
+ expect(noTaskDataEvents).to.have.length(0)
+ expect(endCalls).to.have.length(0)
+ })
+ })
+})
diff --git a/src/StreamHandler.ts b/src/StreamHandler.ts
new file mode 100644
index 0000000..40e9fdd
--- /dev/null
+++ b/src/StreamHandler.ts
@@ -0,0 +1,133 @@
+import child_process from "node:child_process"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { Logger } from "./Logger"
+import { map } from "./Object"
+import { blank } from "./String"
+import { Task } from "./Task"
+
+/**
+ * Configuration for stream handling behavior
+ */
+export interface StreamHandlerOptions {
+ readonly logger: () => Logger
+}
+
+/**
+ * Interface for objects that can provide stream context
+ */
+export interface StreamContext {
+ readonly name: string
+ isEnding(): boolean
+ getCurrentTask(): Task | undefined
+ onError: (reason: string, error: Error) => void
+ end: (gracefully: boolean, reason: string) => void
+}
+
+/**
+ * Handles stdout/stderr stream processing for child processes.
+ * Manages stream event listeners, data routing, and error handling.
+ */
+export class StreamHandler {
+ readonly #logger: () => Logger
+
+ constructor(
+ options: StreamHandlerOptions,
+ private readonly emitter: BatchClusterEmitter,
+ ) {
+ this.#logger = options.logger
+ }
+
+ /**
+ * Set up stream event listeners for a child process
+ */
+ setupStreamListeners(
+ proc: child_process.ChildProcess,
+ context: StreamContext,
+ ): void {
+ const stdin = proc.stdin
+ if (stdin == null) throw new Error("Given proc had no stdin")
+ stdin.on("error", (err) => context.onError("stdin.error", err))
+
+ const stdout = proc.stdout
+ if (stdout == null) throw new Error("Given proc had no stdout")
+ stdout.on("error", (err) => context.onError("stdout.error", err))
+ stdout.on("data", (data: string | Buffer) => this.#onStdout(data, context))
+
+ map(proc.stderr, (stderr) => {
+ stderr.on("error", (err) => context.onError("stderr.error", err))
+ stderr.on("data", (data: string | Buffer) =>
+ this.#onStderr(data, context),
+ )
+ })
+ }
+
+ /**
+ * Handle stdout data from a child process
+ */
+ #onStdout(data: string | Buffer, context: StreamContext): void {
+ if (data == null) return
+
+ const task = context.getCurrentTask()
+ if (task != null && task.pending) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
+ this.emitter.emit("taskData", data, task, context as any)
+ task.onStdout(data)
+ } else if (context.isEnding()) {
+ // don't care if we're already being shut down.
+ } else if (!blank(data)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
+ this.emitter.emit("noTaskData", data, null, context as any)
+ context.end(false, "stdout.error")
+ }
+ }
+
+ /**
+ * Handle stderr data from a child process
+ */
+ #onStderr(data: string | Buffer, context: StreamContext): void {
+ if (blank(data)) return
+
+ this.#logger().warn(context.name + ".onStderr(): " + String(data))
+
+ const task = context.getCurrentTask()
+ if (task != null && task.pending) {
+ task.onStderr(data)
+ } else if (!context.isEnding()) {
+ // If we're ending and there isn't a task, don't worry about it.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
+ this.emitter.emit("noTaskData", null, data, context as any)
+ context.end(false, "stderr")
+ }
+ }
+
+ /**
+ * Process stdout data directly (for testing or manual processing)
+ */
+ processStdout(data: string | Buffer, context: StreamContext): void {
+ this.#onStdout(data, context)
+ }
+
+ /**
+ * Process stderr data directly (for testing or manual processing)
+ */
+ processStderr(data: string | Buffer, context: StreamContext): void {
+ this.#onStderr(data, context)
+ }
+
+ /**
+ * Check if data is considered blank/empty
+ */
+ isBlankData(data: string | Buffer | null | undefined): boolean {
+ return blank(data)
+ }
+
+ /**
+ * Get stream handler statistics
+ */
+ getStats() {
+ return {
+ handlerActive: true,
+ emitterConnected: this.emitter != null,
+ }
+ }
+}
diff --git a/src/String.ts b/src/String.ts
index 0732d07..6c9b2d1 100644
--- a/src/String.ts
+++ b/src/String.ts
@@ -1,17 +1,21 @@
-import { isFunction } from "./Object"
-
-export function blank(s: string | Buffer | undefined): boolean {
- return s == null || String(s).trim().length === 0
+export function blank(s: unknown): boolean {
+ return s == null || toS(s).trim().length === 0
}
-export function notBlank(s: string | undefined): s is string {
+export function notBlank(s: unknown): boolean {
return !blank(s)
}
+export function toNotBlank(s: unknown): string | undefined {
+ const result = toS(s).trim()
+ return result.length === 0 ? undefined : result
+}
+
export function ensureSuffix(s: string, suffix: string): string {
return s.endsWith(suffix) ? s : s + suffix
}
-export function toS(s: any): string {
- return s == null ? "" : isFunction(s.toString) ? s.toString() : String(s)
+export function toS(s: unknown): string {
+ /* eslint-disable-next-line @typescript-eslint/no-base-to-string */
+ return s == null ? "" : s.toString()
}
diff --git a/src/Task.ts b/src/Task.ts
index 58a591c..ddd5bc3 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -15,7 +15,7 @@ let _taskId = 1
* instance has a promise that will be resolved or rejected based on the
* result of the task.
*/
-export class Task {
+export class Task {
readonly taskId = _taskId++
#opts?: TaskOptions
#startedAt?: number
@@ -89,13 +89,13 @@ export class Task {
if (passRE != null && passRE.exec(this.#stdout) != null) {
// remove the pass token from stdout:
this.#stdout = this.#stdout.replace(passRE, "")
- this.#resolve(true)
+ void this.#resolve(true)
} else {
const failRE = this.#opts?.failRE
if (failRE != null && failRE.exec(this.#stdout) != null) {
// remove the fail token from stdout:
this.#stdout = this.#stdout.replace(failRE, "")
- this.#resolve(false)
+ void this.#resolve(false)
}
}
}
@@ -106,7 +106,7 @@ export class Task {
if (failRE != null && failRE.exec(this.#stderr) != null) {
// remove the fail token from stderr:
this.#stderr = this.#stderr.replace(failRE, "")
- this.#resolve(false)
+ void this.#resolve(false)
}
}
@@ -146,14 +146,15 @@ export class Task {
try {
const parseResult = await this.parser(this.#stdout, this.#stderr, passed)
if (this.#d.resolve(parseResult)) {
+ // success
} else {
this.#opts?.observer.emit(
"internalError",
new Error(this.toString() + " ._resolved() more than once"),
)
}
- } catch (error: any) {
- this.reject(error)
+ } catch (error: unknown) {
+ this.reject(error instanceof Error ? error : new Error(String(error)))
}
}
}
diff --git a/src/TaskQueueManager.spec.ts b/src/TaskQueueManager.spec.ts
new file mode 100644
index 0000000..2632760
--- /dev/null
+++ b/src/TaskQueueManager.spec.ts
@@ -0,0 +1,263 @@
+import events from "node:events"
+import { expect, parser } from "./_chai.spec"
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { BatchProcess } from "./BatchProcess"
+import { logger } from "./Logger"
+import { Task } from "./Task"
+import { TaskQueueManager } from "./TaskQueueManager"
+
+describe("TaskQueueManager", function () {
+ let queueManager: TaskQueueManager
+ let emitter: BatchClusterEmitter
+ let mockProcess: BatchProcess
+
+ beforeEach(function () {
+ emitter = new events.EventEmitter() as BatchClusterEmitter
+ queueManager = new TaskQueueManager(logger, emitter)
+
+ // Create a mock process that can execute tasks
+ mockProcess = {
+ ready: true,
+ idle: true,
+ pid: 12345,
+ execTask: () => true, // Always succeed
+ } as unknown as BatchProcess
+ })
+
+ describe("initial state", function () {
+ it("should start with empty queue", function () {
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ expect(queueManager.isEmpty).to.be.true
+ expect(queueManager.pendingTasks).to.eql([])
+ })
+
+ it("should return empty queue stats", function () {
+ const stats = queueManager.getQueueStats()
+ expect(stats.pendingTaskCount).to.eql(0)
+ expect(stats.isEmpty).to.be.true
+ })
+ })
+
+ describe("task enqueuing", function () {
+ it("should enqueue tasks when not ended", function () {
+ const task = new Task("test command", parser)
+ const promise = queueManager.enqueueTask(task, false)
+
+ expect(queueManager.pendingTaskCount).to.eql(1)
+ expect(queueManager.isEmpty).to.be.false
+ expect(queueManager.pendingTasks).to.have.length(1)
+ expect(queueManager.pendingTasks[0]).to.eql(task)
+ expect(promise).to.equal(task.promise)
+ })
+
+ it("should reject tasks when ended", function () {
+ const task = new Task("test command", parser)
+ const promise = queueManager.enqueueTask(task, true)
+
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ expect(queueManager.isEmpty).to.be.true
+ expect(promise).to.equal(task.promise)
+ expect(task.pending).to.be.false
+ })
+
+ it("should handle multiple tasks", function () {
+ const task1 = new Task("command 1", parser)
+ const task2 = new Task("command 2", parser)
+ const task3 = new Task("command 3", parser)
+
+ queueManager.enqueueTask(task1, false)
+ queueManager.enqueueTask(task2, false)
+ queueManager.enqueueTask(task3, false)
+
+ expect(queueManager.pendingTaskCount).to.eql(3)
+ expect(queueManager.pendingTasks).to.have.length(3)
+ })
+ })
+
+ describe("task assignment", function () {
+ let task: Task
+
+ beforeEach(function () {
+ task = new Task("test command", parser)
+ queueManager.enqueueTask(task, false)
+ })
+
+ it("should assign task to ready process", function () {
+ const result = queueManager.tryAssignNextTask(mockProcess)
+
+ expect(result).to.be.true
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ expect(queueManager.isEmpty).to.be.true
+ })
+
+ it("should not assign task when no ready process", function () {
+ const result = queueManager.tryAssignNextTask(undefined)
+
+ expect(result).to.be.false
+ expect(queueManager.pendingTaskCount).to.eql(1)
+ expect(queueManager.isEmpty).to.be.false
+ })
+
+ it("should retry when process cannot execute task", function () {
+ const failingProcess = {
+ ...mockProcess,
+ execTask: () => false, // Always fail
+ } as unknown as BatchProcess
+
+ const result = queueManager.tryAssignNextTask(failingProcess)
+
+ expect(result).to.be.false
+ expect(queueManager.pendingTaskCount).to.eql(1) // Task should be re-queued
+ })
+
+ it("should stop retrying after max retries", function () {
+ const failingProcess = {
+ ...mockProcess,
+ execTask: () => false,
+ } as unknown as BatchProcess
+
+ const result = queueManager.tryAssignNextTask(failingProcess, 0)
+
+ expect(result).to.be.false
+ expect(queueManager.pendingTaskCount).to.eql(1) // Task remains when retries exhausted
+ })
+
+ it("should handle empty queue gracefully", function () {
+ // Clear the queue first
+ queueManager.clearAllTasks()
+
+ const result = queueManager.tryAssignNextTask(mockProcess)
+
+ expect(result).to.be.false
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ })
+ })
+
+ describe("queue processing", function () {
+ beforeEach(function () {
+ // Add multiple tasks
+ for (let i = 0; i < 5; i++) {
+ const task = new Task(`command ${i}`, parser)
+ queueManager.enqueueTask(task, false)
+ }
+ })
+
+ it("should process all tasks when process is always ready", function () {
+ const findReadyProcess = () => mockProcess
+ const assignedCount = queueManager.processQueue(findReadyProcess)
+
+ expect(assignedCount).to.eql(5)
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ expect(queueManager.isEmpty).to.be.true
+ })
+
+ it("should stop processing when no ready process available", function () {
+ const findReadyProcess = () => undefined
+ const assignedCount = queueManager.processQueue(findReadyProcess)
+
+ expect(assignedCount).to.eql(0)
+ expect(queueManager.pendingTaskCount).to.eql(5)
+ expect(queueManager.isEmpty).to.be.false
+ })
+
+ it("should partially process queue when process becomes unavailable", function () {
+ let callCount = 0
+ const findReadyProcess = () => {
+ callCount++
+ return callCount <= 3 ? mockProcess : undefined
+ }
+
+ const assignedCount = queueManager.processQueue(findReadyProcess)
+
+ expect(assignedCount).to.eql(3)
+ expect(queueManager.pendingTaskCount).to.eql(2)
+ })
+
+ it("should handle process that fails to execute tasks", function () {
+ const failingProcess = {
+ ...mockProcess,
+ execTask: () => false,
+ } as unknown as BatchProcess
+
+ const findReadyProcess = () => failingProcess
+ const assignedCount = queueManager.processQueue(findReadyProcess)
+
+ expect(assignedCount).to.eql(0)
+ expect(queueManager.pendingTaskCount).to.be.greaterThan(0) // Tasks remain queued
+ })
+ })
+
+ describe("queue management", function () {
+ beforeEach(function () {
+ // Add some tasks
+ for (let i = 0; i < 3; i++) {
+ const task = new Task(`command ${i}`, parser)
+ queueManager.enqueueTask(task, false)
+ }
+ })
+
+ it("should clear all tasks", function () {
+ expect(queueManager.pendingTaskCount).to.eql(3)
+
+ queueManager.clearAllTasks()
+
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ expect(queueManager.isEmpty).to.be.true
+ expect(queueManager.pendingTasks).to.eql([])
+ })
+
+ it("should provide accurate queue statistics", function () {
+ const stats = queueManager.getQueueStats()
+
+ expect(stats.pendingTaskCount).to.eql(3)
+ expect(stats.isEmpty).to.be.false
+ })
+ })
+
+ describe("error handling", function () {
+ it("should handle concurrent access gracefully", function () {
+ // Add a task
+ const task = new Task("test", parser)
+ queueManager.enqueueTask(task, false)
+
+ // First process gets the task
+ const result1 = queueManager.tryAssignNextTask(mockProcess)
+ expect(result1).to.be.true
+
+ // Second attempt on empty queue should return false
+ const result2 = queueManager.tryAssignNextTask(mockProcess)
+ expect(result2).to.be.false
+ expect(queueManager.pendingTaskCount).to.eql(0)
+ })
+ })
+
+ describe("FIFO ordering", function () {
+ it("should process tasks in first-in-first-out order", function () {
+ const executedTasks: Task[] = []
+ const trackingProcess = {
+ ...mockProcess,
+ execTask: (task: Task) => {
+ executedTasks.push(task)
+ return true
+ },
+ } as unknown as BatchProcess
+
+ // Enqueue tasks with identifiable commands
+ const task1 = new Task("first", parser)
+ const task2 = new Task("second", parser)
+ const task3 = new Task("third", parser)
+
+ queueManager.enqueueTask(task1, false)
+ queueManager.enqueueTask(task2, false)
+ queueManager.enqueueTask(task3, false)
+
+ // Process all tasks
+ queueManager.processQueue(() => trackingProcess)
+
+ expect(executedTasks).to.have.length(3)
+ expect(executedTasks[0]?.command).to.eql("first")
+ expect(executedTasks[1]?.command).to.eql("second")
+ expect(executedTasks[2]?.command).to.eql("third")
+ })
+ })
+})
diff --git a/src/TaskQueueManager.ts b/src/TaskQueueManager.ts
new file mode 100644
index 0000000..3412e96
--- /dev/null
+++ b/src/TaskQueueManager.ts
@@ -0,0 +1,141 @@
+import { BatchClusterEmitter } from "./BatchClusterEmitter"
+import { BatchProcess } from "./BatchProcess"
+import { Logger } from "./Logger"
+import { Task } from "./Task"
+
+/**
+ * Manages task queuing, scheduling, and assignment to ready processes.
+ * Handles the task lifecycle from enqueue to assignment.
+ */
+export class TaskQueueManager {
+ readonly #tasks: Task[] = []
+ readonly #logger: () => Logger
+
+ constructor(
+ logger: () => Logger,
+ private readonly emitter?: BatchClusterEmitter,
+ ) {
+ this.#logger = logger
+ }
+
+ /**
+ * Add a task to the queue for processing
+ */
+ enqueueTask(task: Task, ended: boolean): Promise {
+ if (ended) {
+ task.reject(
+ new Error("BatchCluster has ended, cannot enqueue " + task.command),
+ )
+ } else {
+ this.#tasks.push(task as Task)
+ }
+ return task.promise
+ }
+
+ /**
+ * Simple enqueue method (alias for enqueueTask without ended check)
+ */
+ enqueue(task: Task): void {
+ this.#tasks.push(task)
+ }
+
+ /**
+ * Get the number of pending tasks in the queue
+ */
+ get pendingTaskCount(): number {
+ return this.#tasks.length
+ }
+
+ /**
+ * Get all pending tasks (mostly for testing)
+ */
+ get pendingTasks(): readonly Task[] {
+ return this.#tasks
+ }
+
+ /**
+ * Check if the queue is empty
+ */
+ get isEmpty(): boolean {
+ return this.#tasks.length === 0
+ }
+
+ /**
+ * Attempt to assign the next task to a ready process.
+ * Returns true if a task was successfully assigned.
+ */
+ tryAssignNextTask(
+ readyProcess: BatchProcess | undefined,
+ retries = 1,
+ ): boolean {
+ if (this.#tasks.length === 0 || retries < 0) {
+ return false
+ }
+
+ // no procs are idle and healthy :(
+ if (readyProcess == null) {
+ return false
+ }
+
+ const task = this.#tasks.shift()
+ if (task == null) {
+ this.emitter?.emit("internalError", new Error("unexpected null task"))
+ return false
+ }
+
+ const submitted = readyProcess.execTask(task)
+ if (!submitted) {
+ // This isn't an internal error: the proc may have needed to run a health
+ // check. Let's reschedule the task and try again:
+ this.#tasks.push(task)
+ // We don't want to return false here (it'll stop the assignment loop) unless
+ // we actually can't submit the task:
+ return this.tryAssignNextTask(readyProcess, retries - 1)
+ }
+
+ this.#logger().trace(
+ "TaskQueueManager.tryAssignNextTask(): submitted task",
+ {
+ child_pid: readyProcess.pid,
+ task,
+ },
+ )
+
+ return submitted
+ }
+
+ /**
+ * Process all pending tasks by assigning them to ready processes.
+ * Returns the number of tasks successfully assigned.
+ */
+ processQueue(findReadyProcess: () => BatchProcess | undefined): number {
+ let assignedCount = 0
+
+ while (this.#tasks.length > 0) {
+ const readyProcess = findReadyProcess()
+ if (!this.tryAssignNextTask(readyProcess)) {
+ break
+ }
+ assignedCount++
+ }
+
+ return assignedCount
+ }
+
+ /**
+ * Clear all pending tasks (used during shutdown)
+ */
+ clearAllTasks(): void {
+ this.#tasks.length = 0
+ }
+
+ /**
+ * Get statistics about task assignment and queue state
+ */
+ getQueueStats() {
+ return {
+ pendingTaskCount: this.#tasks.length,
+ isEmpty: this.isEmpty,
+ }
+ }
+}
diff --git a/src/Timeout.ts b/src/Timeout.ts
index 318ba65..94a16c9 100644
--- a/src/Timeout.ts
+++ b/src/Timeout.ts
@@ -9,26 +9,30 @@ export async function thenOrTimeout(
// something else in that case?
return timeoutMs <= 1
? p
- : new Promise(async (resolve, reject) => {
+ : new Promise((resolve, reject) => {
let pending = true
- try {
- const t = timers.setTimeout(() => {
- if (pending) {
- pending = false
- resolve(Timeout)
- }
- }, timeoutMs)
- const result = await p
+ const t = timers.setTimeout(() => {
if (pending) {
pending = false
- clearTimeout(t)
- resolve(result)
+ resolve(Timeout)
}
- } catch (err) {
- if (pending) {
- pending = false
- reject(err)
- }
- }
+ }, timeoutMs)
+
+ p.then(
+ (result) => {
+ if (pending) {
+ pending = false
+ clearTimeout(t)
+ resolve(result)
+ }
+ },
+ (err: unknown) => {
+ if (pending) {
+ pending = false
+ clearTimeout(t)
+ reject(err instanceof Error ? err : new Error(String(err)))
+ }
+ },
+ )
})
}
diff --git a/src/WhyNotHealthy.ts b/src/WhyNotHealthy.ts
new file mode 100644
index 0000000..4bdb103
--- /dev/null
+++ b/src/WhyNotHealthy.ts
@@ -0,0 +1,25 @@
+/**
+ * Reasons why a BatchProcess might not be healthy
+ */
+export type WhyNotHealthy =
+ | "broken"
+ | "closed"
+ | "ending"
+ | "ended"
+ | "idle"
+ | "old"
+ | "proc.close"
+ | "proc.disconnect"
+ | "proc.error"
+ | "proc.exit"
+ | "stderr.error"
+ | "stderr"
+ | "stdin.error"
+ | "stdout.error"
+ | "timeout"
+ | "tooMany" // < only sent by BatchCluster when maxProcs is reduced
+ | "startError"
+ | "unhealthy"
+ | "worn"
+
+export type WhyNotReady = WhyNotHealthy | "busy"
diff --git a/src/_chai.spec.ts b/src/_chai.spec.ts
index e761fce..e52fe78 100644
--- a/src/_chai.spec.ts
+++ b/src/_chai.spec.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-require-imports */
try {
require("source-map-support").install()
} catch {
@@ -26,13 +27,11 @@ setLogger(
Log.withTimestamps(
Log.filterLevels(
{
- // tslint:disable: no-unbound-method
trace: console.log,
debug: console.log,
info: console.log,
warn: console.warn,
error: console.error,
- // tslint:enable: no-unbound-method
},
(process.env.LOG as any) ?? "error",
),
diff --git a/src/test-helpers.ts b/src/test-helpers.ts
new file mode 100644
index 0000000..f256031
--- /dev/null
+++ b/src/test-helpers.ts
@@ -0,0 +1 @@
+export const ErrorPrefix = "ERROR: "
diff --git a/src/test.spec.ts b/src/test.spec.ts
index a90d21e..98b89e8 100644
--- a/src/test.spec.ts
+++ b/src/test.spec.ts
@@ -10,8 +10,6 @@ import {
setRngseed,
} from "./_chai.spec"
-/* eslint-disable @typescript-eslint/no-non-null-assertion */
-
describe("test.js", () => {
class Harness {
readonly child: child_process.ChildProcess
@@ -32,18 +30,18 @@ describe("test.js", () => {
}
async end(): Promise {
this.child.stdin!.end(null)
- await until(() => this.running().then((ea) => !ea), 1000)
+ await until(() => this.notRunning(), 1000)
if (await this.running()) {
console.error("Ack, I had to kill child pid " + this.child.pid)
kill(this.child.pid)
}
return
}
- async running(): Promise {
+ running(): boolean {
return pidExists(this.child.pid)
}
- async notRunning(): Promise {
- return this.running().then((ea) => !ea)
+ notRunning(): boolean {
+ return !this.running()
}
async assertStdout(f: (output: string) => void) {
// The OS may take a bit before the PID shows up in the process table:
diff --git a/src/test.ts b/src/test.ts
index fe985e9..afee866 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -40,7 +40,8 @@ function toF(s: string | undefined) {
const failrate = toF(process.env.failrate) ?? 0
const rng =
process.env.rngseed != null
- ? require("seedrandom")(process.env.rngseed)
+ ? // eslint-disable-next-line @typescript-eslint/no-require-imports
+ require("seedrandom")(process.env.rngseed)
: Math.random
async function onLine(line: string): Promise {
@@ -148,5 +149,6 @@ async function onLine(line: string): Promise {
const m = new Mutex()
process.stdin
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
.pipe(require("split2")())
.on("data", (ea: string) => m.serial(() => onLine(ea)))
BatchCluster instances manage 0 or more homogeneous child processes, and +
- Preparing search index...
- The search index is not available
batch-clusterClass BatchCluster
BatchCluster instances manage 0 or more homogeneous child processes, and provide the main interface for enqueuing
Task
s viaenqueueTask
.Given the large number of configuration options, the constructor receives a single options hash. The most important of these are the
-ChildProcessFactory
, which specifies the factory that creates ChildProcess instances, andBatchProcessOptions
, which specifies how child tasks can be verified and shut down.Index
Constructors
Properties
Accessors
Methods
Constructors
constructor
Parameters
opts: Partial<BatchClusterOptions> & BatchProcessOptions & ChildProcessFactory
Returns BatchCluster
Properties
Readonly
emitterReadonly
offType declaration
Type Parameters
E extends keyof BatchClusterEvents
Parameters
eventName: E
listener: ((...args) => void)
Parameters
Rest
...args: Args<BatchClusterEvents[E]>Returns void
Returns this
See
BatchClusterEvents
+Index
Constructors
Properties
Accessors
Methods
Constructors
constructor
Parameters
Returns BatchCluster
Properties
Readonly
emitterReadonly
offType declaration
Type Parameters
Parameters
Parameters
Rest
...args: Args<BatchClusterEvents[E]>Returns void
Returns this
See
BatchClusterEvents
Since
v9.0.0
-See
BatchClusterEvents
-Since
v9.0.0
-Readonly
onType declaration
Type Parameters
E extends keyof BatchClusterEvents
Parameters
eventName: E
listener: ((...args) => void)
Parameters
Rest
...args: Args<BatchClusterEvents[E]>Returns void
Returns this
See
BatchClusterEvents
-See
BatchClusterEvents
-Readonly
optionsAccessors
busy Proc Count
Returns number
the current number of child processes currently servicing tasks
-child End Counts
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
}
Returns {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
}
broken: number
closed: number
ended: number
ending: number
idle: number
old: number
proc.close: number
proc.disconnect: number
proc.error: number
proc.exit: number
start Error: number
stderr: number
stderr.error: number
stdin.error: number
stdout.error: number
timeout: number
too Many: number
unhealthy: number
worn: number
current Tasks
Returns Task<any>[]
the current running Tasks (mostly for testing)
-ended
Returns boolean
internal Error Count
For integration tests:
-Returns number
is Idle
Returns boolean
true if all previously-enqueued tasks have settled
-mean Tasks Per Proc
Returns number
the mean number of tasks completed by child processes
-pending Task Count
Returns number
the number of pending tasks
-pending Tasks
Returns Task<any>[]
the current pending Tasks (mostly for testing)
-proc Count
Returns number
the current number of spawned child processes. Some (or all) may be idle.
-spawned Proc Count
Returns number
the total number of child processes created by this instance
-starting Proc Count
Returns number
Methods
close Child Processes
Shut down any currently-running child processes. New child processes will +
Readonly
onType declaration
Type Parameters
Parameters
Parameters
Rest
...args: Args<BatchClusterEvents[E]>Returns void
Returns this
See
BatchClusterEvents
+Readonly
optionsAccessors
busy Proc Count
Returns number
the current number of child processes currently servicing tasks
+child End Counts
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
}
Returns {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
}
broken: number
closed: number
ended: number
ending: number
idle: number
old: number
proc.close: number
proc.disconnect: number
proc.error: number
proc.exit: number
start Error: number
stderr: number
stderr.error: number
stdin.error: number
stdout.error: number
timeout: number
too Many: number
unhealthy: number
worn: number
current Tasks
Returns Task<any>[]
the current running Tasks (mostly for testing)
+ended
Returns boolean
internal Error Count
For integration tests:
+Returns number
is Idle
Returns boolean
true if all previously-enqueued tasks have settled
+mean Tasks Per Proc
Returns number
the mean number of tasks completed by child processes
+pending Task Count
Returns number
the number of pending tasks
+pending Tasks
Returns Task<any>[]
the current pending Tasks (mostly for testing)
+proc Count
Returns number
the current number of spawned child processes. Some (or all) may be idle.
+spawned Proc Count
Returns number
the total number of child processes created by this instance
+starting Proc Count
Returns number
Methods
close Child Processes
Shut down any currently-running child processes. New child processes will be started automatically to handle new tasks.
-Parameters
gracefully: boolean = true
Returns Promise<void>
count Ended Child Procs
Get ended process counts (used for tests)
-Parameters
why: ChildExitReason
Returns number
end
Shut down this instance, and all child processes.
-Parameters
gracefully: boolean = true
should an attempt be made to finish in-flight tasks, or +
Parameters
Returns Promise<void>
count Ended Child Procs
Get ended process counts (used for tests)
+Parameters
Returns number
end
Shut down this instance, and all child processes.
+Parameters
should an attempt be made to finish in-flight tasks, or should we force-kill child PIDs.
-Returns Deferred<void>
enqueue Task
Submits
-task
for processing by aBatchProcess
instanceType Parameters
T
Parameters
task: Task<T>
Returns Promise<T>
a Promise that is resolved or rejected once the task has been +
Returns Deferred<void>
enqueue Task
Submits
+task
for processing by aBatchProcess
instanceType Parameters
Parameters
Returns Promise<T>
a Promise that is resolved or rejected once the task has been attempted on an idle BatchProcess
-pids
Verify that each BatchProcess PID is actually alive.
+pids
Verify that each BatchProcess PID is actually alive.
Returns number[]
the spawned PIDs that are still in the process table.
-set Max Procs
Reset the maximum number of active child processes to
maxProcs
. Note that +set Max Procs
Reset the maximum number of active child processes to
-maxProcs
. Note that this is handled gracefully: child processes are only reduced as tasks are completed.Parameters
maxProcs: number
Returns void
stats
childEndCounts: {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
};
currentProcCount: number;
ended: boolean;
ending: boolean;
internalErrorCount: number;
maxProcCount: number;
msBeforeNextSpawn: number;
pendingTaskCount: number;
readyProcCount: number;
spawnedProcCount: number;
startErrorRatePerMinute: number;
}
For diagnostics. Contents may change.
-Returns {
childEndCounts: {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
};
currentProcCount: number;
ended: boolean;
ending: boolean;
internalErrorCount: number;
maxProcCount: number;
msBeforeNextSpawn: number;
pendingTaskCount: number;
readyProcCount: number;
spawnedProcCount: number;
startErrorRatePerMinute: number;
}
child End Counts: {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
}
broken: number
closed: number
ended: number
ending: number
idle: number
old: number
proc.close: number
proc.disconnect: number
proc.error: number
proc.exit: number
start Error: number
stderr: number
stderr.error: number
stdin.error: number
stdout.error: number
timeout: number
too Many: number
unhealthy: number
worn: number
current Proc Count: number
ended: boolean
ending: boolean
internal Error Count: number
max Proc Count: number
ms Before Next Spawn: number
pending Task Count: number
ready Proc Count: number
spawned Proc Count: number
start Error Rate Per Minute: number
vacuum Procs
Run maintenance on currently spawned child processes. This method is +
Parameters
Returns void
stats
childEndCounts: {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
};
currentProcCount: number;
ended: boolean;
ending: boolean;
internalErrorCount: number;
maxProcCount: number;
msBeforeNextSpawn: number;
pendingTaskCount: number;
readyProcCount: number;
spawnedProcCount: number;
startErrorRatePerMinute: number;
}
For diagnostics. Contents may change.
+Returns {
childEndCounts: {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
};
currentProcCount: number;
ended: boolean;
ending: boolean;
internalErrorCount: number;
maxProcCount: number;
msBeforeNextSpawn: number;
pendingTaskCount: number;
readyProcCount: number;
spawnedProcCount: number;
startErrorRatePerMinute: number;
}
child End Counts: {
broken: number;
closed: number;
ended: number;
ending: number;
idle: number;
old: number;
proc.close: number;
proc.disconnect: number;
proc.error: number;
proc.exit: number;
startError: number;
stderr: number;
stderr.error: number;
stdin.error: number;
stdout.error: number;
timeout: number;
tooMany: number;
unhealthy: number;
worn: number;
}
broken: number
closed: number
ended: number
ending: number
idle: number
old: number
proc.close: number
proc.disconnect: number
proc.error: number
proc.exit: number
start Error: number
stderr: number
stderr.error: number
stdin.error: number
stdout.error: number
timeout: number
too Many: number
unhealthy: number
worn: number
current Proc Count: number
ended: boolean
ending: boolean
internal Error Count: number
max Proc Count: number
ms Before Next Spawn: number
pending Task Count: number
ready Proc Count: number
spawned Proc Count: number
start Error Rate Per Minute: number
vacuum Procs
Run maintenance on currently spawned child processes. This method is normally invoked automatically as tasks are enqueued and processed.
Only public for tests.
-Returns Promise<void[]>
Settings
Member Visibility
Theme
On This Page
Generated using TypeDoc
Returns Promise<void[]>
Settings
Member Visibility
Theme
On This Page