diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6717548 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,40 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run compile:*)", + "Bash(npm run lint:*)", + "Bash(ls:*)", + "Bash(rg:*)", + "Bash(grep:*)", + "Bash(npm info:*)", + "Bash(sed:*)", + "Bash(npm test)", + "Bash(npx eslint:*)", + "Bash(git add:*)", + "Bash(npm t)", + "Bash(mocha:*)", + "Bash(npx mocha:*)", + "Bash(find:*)", + "Bash(/usr/bin/rg -n \"onStdout|onStderr\" BatchProcess.ts)", + "Bash(timeout 45s npm run test:compile)", + "Bash(timeout:*)", + "Bash(gh run view:*)", + "Bash(mkdir:*)", + "Bash(npm run docs:build:*)", + "Bash(npm test:*)", + "Bash(/usr/bin/rg -n \"TODO|FIXME|XXX|HACK\" src/)", + "Bash(npx npm-check-updates)", + "Bash(gh repo view:*)", + "Bash(gh issue list:*)", + "Bash(gh pr list:*)", + "Bash(gh run list:*)", + "Bash(for i in {1..5})", + "Bash(do echo \"Run $i:\")", + "Bash(break)", + "Bash(done)", + "WebSearch" + ], + "deny": [] + }, + "enableAllProjectMcpServers": false +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index aa740e0..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - env: { - node: true, - }, - extends: [ - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:eslint-plugin-import/recommended", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - project: "tsconfig.json", - sourceType: "module", - }, - plugins: ["@typescript-eslint", "eslint-plugin-import"], - rules: { - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/member-delimiter-style": [ - "warn", - { multiline: { delimiter: "none" } }, - ], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off", - eqeqeq: ["warn", "always", { null: "ignore" }], - "import/no-cycle": "warn", - "import/no-unresolved": "off", - "no-redeclare": "warn", - "no-undef-init": "warn", - "no-unused-expressions": "warn", - }, -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..048017e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference + +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 8 + + # Maintain dependencies for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 8 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..784a8e4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,98 @@ +# This is used by the build badge: +name: Build & Release +env: + CI: 1 + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + version: + description: "Version type (auto-detects from package.json if not specified)" + required: false + type: choice + options: + - "" + - patch + - minor + - major + +jobs: + lint: + runs-on: [ubuntu-latest] + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: "20" + - run: npm ci + - run: npm run lint + + build: + runs-on: ${{ matrix.os }} + + # See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + # See https://github.com/nodejs/release#release-schedule + node-version: [20, 22, 24] + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm test + + publish: + runs-on: ubuntu-24.04 + needs: [lint, build] + if: ${{ github.event_name == 'workflow_dispatch' }} + permissions: + id-token: write # Required for OIDC + contents: write # Required for release-it to create tags/commits + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 # Need full history for release-it + + # setup-node with registry-url is required for OIDC trusted publishing + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: 20 + cache: "npm" + registry-url: "https://registry.npmjs.org" + + - name: Set up SSH signing + uses: photostructure/git-ssh-signing-action@a770c2ff3aea31d9df9f2974ac9d672f2bfe62f3 # v1.1.0 + with: + ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} + git-user-name: ${{ secrets.GIT_USER_NAME }} + git-user-email: ${{ secrets.GIT_USER_EMAIL }} + + - name: Update npm to latest + run: | + echo "Current npm version:" + npm --version + echo "Updating npm to latest..." + npm install -g npm@latest + echo "New npm version:" + npm --version + + - name: Install dependencies + run: npm ci + + # Note: Tests are run by release-it's before:init hook via npm run lint -> pretest + # This avoids running the full test matrix (9+ OS/Node combinations) in the release workflow + # The pretest script (clean + lint + compile) is sufficient for release validation + - name: Release with release-it + run: npm run release -- --ci ${{ github.event.inputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2318761..952dcd4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,12 +7,12 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: [main] schedule: - - cron: '0 16 * * 3' + - cron: "0 16 * * 3" jobs: analyze: @@ -24,43 +24,43 @@ jobs: matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - language: ['javascript'] + language: ["javascript"] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 - # โ„น๏ธ Command-line programs to run using the OS shell. - # ๐Ÿ“š https://git.io/JvXDl + # โ„น๏ธ Command-line programs to run using the OS shell. + # ๐Ÿ“š https://git.io/JvXDl - # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..a3716c4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,58 @@ +name: Docs + +on: + push: + branches: [main] + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Set up Node.js + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + + - name: Install dependencies + run: npm ci + + - name: Generate documentation + run: npm run docs:build + + - name: Setup Pages + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + + - name: Upload artifact + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + with: + path: "build/docs" + + # Deploy job + deploy: + # Add a dependency to the build job + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index 4a202ea..0000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,30 +0,0 @@ -# This is used by the build badge: -name: CI tests -env: - CI: 1 - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ${{ matrix.os }} - - # See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [14.x, 16.x, 18.x] - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: yarn ci - - run: yarn test diff --git a/.gitignore b/.gitignore index 951ea24..6d01c21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ +.dccache .node_repl_history .npm .nyc_output/ *.log coverage/ dist +docs/ node_modules npm-debug.log* -yarn.lock -package-lock.json \ No newline at end of file +yarn.lock \ No newline at end of file diff --git a/.ncurc.json b/.ncurc.json new file mode 100644 index 0000000..ae639fb --- /dev/null +++ b/.ncurc.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json", + "cooldown": 4, + "reject": [ + "@types/chai", + "@types/chai-as-promised", + "chai-as-promised", + "chai-as-promised why: v8 went to ESM", + "chai", + "eslint", + "rimraf", + "rimraf why: https://github.com/isaacs/rimraf/issues/316 (!!)" + ] +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index cce9d3c..55c1943 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,3 @@ { - "semi": false + "plugins": ["prettier-plugin-organize-imports"] } diff --git a/.serve.json b/.serve.json index 1a05945..b308df8 100644 --- a/.serve.json +++ b/.serve.json @@ -1,3 +1,3 @@ { "cleanUrls": false -} \ No newline at end of file +} diff --git a/.typedoc.js b/.typedoc.js deleted file mode 100644 index ca78c6c..0000000 --- a/.typedoc.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - name: "batch-cluster", - out: "./docs/", - readme: "./README.md", - includes: "./src", - gitRevision: "main", // < prevents docs from changing after every commit - exclude: ["**/*test*", "**/*spec*"], - excludePrivate: true, - entryPoints: [ - "./src/BatchCluster.ts", - // "./src/BatchClusterOptions.ts", - // "./src/BatchProcessOptions.ts", - // "./src/Logger.ts", - // "./src/Task.ts", - ], - -} diff --git a/.vscode/launch.json b/.vscode/launch.json index 683625c..d6915ce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,22 +17,16 @@ "name": "Mocha Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { "type": "pwa-node", "request": "launch", "name": "Launch Program", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "program": "${workspaceFolder}/dist/BatchCluster.js", - "outFiles": [ - "${workspaceFolder}/**/*.js" - ] + "outFiles": ["${workspaceFolder}/**/*.js"] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index ea0c059..26968c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,18 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.ignoreWords": [ - "Equalish", - "cygwin", - "debouncer", - "debouncing", - "rngseed" - ] + "typescript.tsdk": "node_modules/typescript/lib", + "cSpell.ignoreWords": [ + "Equalish", + "cygwin", + "debouncer", + "debouncing", + "rngseed" + ], + "cSpell.words": [ + "Millis", + "photostructure", + "Pids", + "Procs", + "sinonjs", + "zombification" + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 67fbbc1..115bedf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,25 +1,21 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 + // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "type": "typescript", "tsconfig": "tsconfig.json", - "problemMatcher": [ - "$tsc" - ], + "problemMatcher": ["$tsc"], "group": "build" }, { "type": "npm", "script": "watch", - "problemMatcher": [ - "$tsc" - ], + "problemMatcher": ["$tsc"], "label": "npm: watch", "detail": "rimraf dist & tsc --watch", "group": "build" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 490c633..bc20ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,21 +17,76 @@ See [Semver](http://semver.org/). - ๐Ÿž Backwards-compatible bug fixes - ๐Ÿ“ฆ Minor packaging changes +- +## v15.0.1 + +"This time, with feeling" + +- ๐Ÿ“ฆ v15.0.0 automated the release to use OIDC ๐Ÿ‘, but the `compile` prerequisite was missed ๐Ÿคฆ, so v15.0.0 has _no code in it_ ๐Ÿชน. + +## v15.0.0 + +- ๐Ÿ’” Deleted the standalone `pids()` function and associated code (including the ProcpsChecker). This function was exported but only used internally by tests. This fixes the [issue #58](https://github.com/photostructure/batch-cluster.js/issues/58) (by deleting the unused code! _the best kind of bugfix_). Thanks for the report, [Zaczero](https://github.com/Zaczero)! + +- ๐Ÿ’” Dropped official support for [Node v23, which is EOL](https://nodejs.org/en/about/previous-releases). + +- ๐Ÿ“ฆ Simplified `prettier` config to accept all defaults -- this added semicolons to every file. + +## v14.0.0 + +- ๐Ÿ’” Dropped official support for Node v14, v16, and v18. Minimum Node.js version is now v20. + +- โœจ Added startup validation for procps availability: `BatchCluster` now throws `ProcpsMissingError` during construction if the required `ps` command (or `tasklist` on Windows) is not available. This provides clear, actionable error messages instead of cryptic runtime failures. Resolves [#13](https://github.com/photostructure/batch-cluster.js/issues/13) and [#39](https://github.com/photostructure/batch-cluster.js/issues/39). + +- ๐Ÿ“ฆ Significant internal refactoring to improve maintainability: + - Extracted process management logic into dedicated classes (`ProcessPoolManager`, `TaskQueueManager`, `ProcessHealthMonitor`, `StreamHandler`, `ProcessTerminator`) + - Implemented strategy pattern for health checking logic + - Improved type safety by replacing `any` with `unknown` throughout the codebase + - Enhanced error handling and process lifecycle management + +## v13.0.0 + +- ๐Ÿ’” Dropped official support for [Node v16, which is EOL](https://nodejs.org/en/blog/announcements/nodejs16-eol/). + +- ๐Ÿ’” Several methods, including BatchCluster#pids() were changed from async to sync (as they were needlessly async). + +- ๐Ÿ“ฆ A number of timeout options can now be validly 0 to disable timeouts: + - `spawnTimeoutMillis` + - `taskTimeoutMillis` + +- ๐Ÿ“ฆ Added eslint `@typescript-eslint/await-thenable` rule and delinted. + +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs + +## v12.1.0 + +- ๐Ÿž `pidExists` now handles `EPERM` properly (previous implementation would + mischaracterize pids as being dead due to insufficient permissions) + +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs + +## v12.0.0 + +- ๐Ÿ’”/โœจ `pidExists` and `killPid` are no longer `async`, as process management + is now performed via `node:process.kill()`, instead of forking `ps` or `tasklist`. + +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs ## v11.0.0 -- ๐Ÿ’” Drop official support for Node 12. +- ๐Ÿ’” Drop official support for Node 12: [EOL was 2022-04-30](https://github.com/nodejs/release#end-of-life-releases) ## v10.4.3 - ๐Ÿž Fix support for zero value of `maxProcAgeMillis` -- ๐Ÿ“ฆ Updated development dependencies and rebuild docs + +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs ## v10.4.2 - ๐Ÿž Fix [`unref` is not a function](https://github.com/photostructure/batch-cluster.js/issues/16) -- ๐Ÿ“ฆ Updated development dependencies and rebuild docs +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs ## v10.4.1 @@ -42,7 +97,7 @@ See [Semver](http://semver.org/). - โœจ If `healthCheckCommand` is set and any task fails, that child process will have a health check run before being put back into rotation. -- ๐Ÿ“ฆ Updated development dependencies and rebuild docs +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs ## v10.3.2 @@ -116,7 +171,7 @@ See [Semver](http://semver.org/). (rather than the prior `Promise.race` call which resulted in a dangling timeout) -- ๐Ÿ“ฆ Updated development dependencies and rebuild docs +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs ## v10.1.0 @@ -133,11 +188,11 @@ See [Semver](http://semver.org/). - ๐Ÿ’” Renamed event s/childExit/childEnd/ - ๐Ÿ’” `childEnd` and `childStart` events receive BatchProcess instances now - ๐Ÿ’” Renamed healthy state s/dead/ended/ -- ๐Ÿ“ฆ make BatchProcess.whyNotHealthy persistent +- ๐Ÿ“ฆ Made BatchProcess.whyNotHealthy persistent - ๐Ÿ“ฆ Added several more WhyNotHealthy values - ๐Ÿ“ฆ Perf: filterInPlace and count use for loops rather than closures -- ๐Ÿ“ฆ add spec to verify .end rejects long-running pending tasks -- ๐Ÿ“ฆ Updated development dependencies and rebuild docs +- ๐Ÿ“ฆ Added spec to verify `.end` rejects long-running pending tasks +- ๐Ÿ“ฆ Updated development dependencies and rebuilt docs ## v9.1.0 @@ -215,7 +270,6 @@ See [Semver](http://semver.org/). ## v7.0.0 - ๐Ÿ’” Several fields were renamed to make things more consistent: - - `BatchCluster.pendingTasks` was renamed to `BatchCluster.pendingTaskCount`. - A new `BatchCluster.pendingTasks` method now matches `BatchCluster.currentTasks`, which both return `Task[]`. - `BatchCluster.busyProcs` was renamed to `busyProcCount`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eeec5b0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +### Build & Development + +- `npm install` - Install dependencies +- `npm run compile` - Compile TypeScript to JavaScript (outputs to dist/) +- `npm run watch` - Watch mode for TypeScript compilation +- `npm run clean` - Clean build artifacts + +### Testing & Quality + +- `npm test` - Run all tests (includes linting and compilation) +- `npm run lint` - Run ESLint on TypeScript source files +- `npm run fmt` - Format code with Prettier +- `mocha dist/**/*.spec.js` - Run specific test files after compilation +- `npx mocha --require ts-node/register src/Object.spec.ts` - Run individual tests like this + +### Documentation + +- `npm run docs` - Generate and serve TypeDoc documentation + +## Architecture Overview + +This library manages clusters of child processes to efficiently handle batch operations through stdin/stdout communication. Key architectural concepts: + +### Core Components + +1. **BatchCluster** (`src/BatchCluster.ts`) - Main entry point that manages a pool of child processes + - Handles process lifecycle, task queuing, and load balancing + - Monitors process health and automatically recycles processes + - Emits events for monitoring and debugging + +2. **BatchProcess** (`src/BatchProcess.ts`) - Wrapper around individual child processes + - Manages communication with a single child process + - Tracks process state, health, and task processing + - Handles process recycling based on task count or runtime + +3. **Task** (`src/Task.ts`) - Represents a unit of work to be processed + - Contains the command to send to child process + - Includes parser for processing responses + - Manages timeouts and completion promises + +### Key Patterns + +- **Parser Interface** - Consumers must implement parsers to handle child process output +- **Deferred Pattern** - Used extensively for promise-based task completion +- **Rate Monitoring** - Tracks error rates to prevent runaway failures +- **Process Recycling** - Automatic process replacement after N tasks or N seconds + +### Testing Approach + +The test suite uses a custom test script (`src/test.ts`) that simulates a batch-mode command-line tool with configurable failure rates. Tests can control: + +- `failrate` - Probability of task failure +- `rngseed` - Seed for deterministic randomness +- `ignoreExit` - Whether to ignore termination signals + +## TypeScript Configuration + +- Strict mode enabled with all strict checks +- Targets ES2019, CommonJS modules +- Outputs to `dist/` with source maps and declarations +- No implicit any, strict null checks, no unchecked indexed access + +## Code Style Guidelines + +- **Null checks**: Always use explicit `x == null` or `x != null` checks. Do not use falsy/truthy checks for nullish values. + - Good: `if (value != null)`, `if (value == null)` + - Bad: `if (value)`, `if (!value)` diff --git a/LICENSE b/LICENSE index 73271f2..0c4295c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2022 Matthew McEachen +Copyright (c) 2017-2025 Matthew McEachen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d2b5c28..e7a6e8a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# batch-cluster +![PhotoStructure batch-cluster logo](https://raw.githubusercontent.com/photostructure/batch-cluster.js/main/doc/logo.svg) **Efficient, concurrent work via batch-mode command-line tools from within Node.js.** [![npm version](https://img.shields.io/npm/v/batch-cluster.svg)](https://www.npmjs.com/package/batch-cluster) -[![Build status](https://github.com/photostructure/batch-cluster.js/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/photostructure/batch-cluster.js/actions/workflows/node.js.yml) +[![Build status](https://github.com/photostructure/batch-cluster.js/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/photostructure/batch-cluster.js/actions/workflows/build.yml) [![GitHub issues](https://img.shields.io/github/issues/photostructure/batch-cluster.js.svg)](https://github.com/photostructure/batch-cluster.js/issues) -[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/photostructure/batch-cluster.js.svg)](https://lgtm.com/projects/g/photostructure/batch-cluster.js/context:javascript) +[![CodeQL](https://github.com/photostructure/batch-cluster.js/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/photostructure/batch-cluster.js/actions/workflows/codeql-analysis.yml) [![Known Vulnerabilities](https://snyk.io/test/github/photostructure/batch-cluster.js/badge.svg?targetFile=package.json)](https://snyk.io/test/github/photostructure/batch-cluster.js?targetFile=package.json) Many command line tools, like @@ -32,11 +32,7 @@ whose source you can examine as an example consumer. ## Installation -Depending on your yarn/npm preference: - ```bash -$ yarn add batch-cluster -# or $ npm install --save batch-cluster ``` @@ -55,7 +51,6 @@ BatchCluster will ensure a given process is only given one task at a time. Note the [constructor options](https://photostructure.github.io/batch-cluster.js/classes/BatchCluster.html#constructor) takes a union type of - - [ChildProcessFactory](https://photostructure.github.io/batch-cluster.js/interfaces/ChildProcessFactory.html) and - [BatchProcessOptions](https://photostructure.github.io/batch-cluster.js/interfaces/BatchProcessOptions.html), @@ -63,7 +58,7 @@ BatchCluster will ensure a given process is only given one task at a time. - [BatchClusterOptions](https://photostructure.github.io/batch-cluster.js/classes/BatchClusterOptions.html), which has defaults that may or may not be relevant to your application. -1. The [default logger](https://photostructure.github.io/batch-cluster.js/interfaces/Logger.html) +1. The [default logger](https://photostructure.github.io/batch-cluster.js/interfaces/Logger.html) writes warning and error messages to `console.warn` and `console.error`. You can change this to your logger by using [setLogger](https://photostructure.github.io/batch-cluster.js/modules.html#setLogger) or by providing a logger to the `BatchCluster` constructor. @@ -82,13 +77,3 @@ See [src/test.ts](https://github.com/photostructure/batch-cluster.js/blob/main/src/test.ts) for an example child process. Note that the script is _designed_ to be flaky on order to test BatchCluster's retry and error handling code. - -## Caution - -The default `BatchClusterOptions.cleanupChildProcs` value of `true` means that BatchCluster will try to use `ps` to ensure Node's view of process state are correct, and that errant -processes are cleaned up. - -If you run this in a docker image based off Alpine or Debian Slim, **this won't work properly unless you install the `procps` package.** - -[See issue #13 for details.](https://github.com/photostructure/batch-cluster.js/issues/13) - diff --git a/SECURITY.md b/SECURITY.md index adfcc68..8d41a47 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Only the latest version of this library is supported. ## Reporting a Vulnerability -If you find a vulnerability, *or even think you have*, please send an email to +If you find a vulnerability, _or even think you have_, please send an email to the author. Each signed git release by `mceachen` contains a monitored email address. diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 0000000..1988e89 Binary files /dev/null and b/doc/logo.png differ diff --git a/doc/logo.svg b/doc/logo.svg new file mode 100644 index 0000000..c1a0d1d --- /dev/null +++ b/doc/logo.svg @@ -0,0 +1,226 @@ + + + + diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e2ac661..0000000 --- a/docs/.nojekyll +++ /dev/null @@ -1 +0,0 @@ -TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/docs/assets/highlight.css b/docs/assets/highlight.css deleted file mode 100644 index 005cd66..0000000 --- a/docs/assets/highlight.css +++ /dev/null @@ -1,36 +0,0 @@ -:root { - --light-hl-0: #000000; - --dark-hl-0: #D4D4D4; - --light-hl-1: #008000; - --dark-hl-1: #6A9955; - --light-code-background: #FFFFFF; - --dark-code-background: #1E1E1E; -} - -@media (prefers-color-scheme: light) { :root { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --code-background: var(--light-code-background); -} } - -@media (prefers-color-scheme: dark) { :root { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --code-background: var(--dark-code-background); -} } - -:root[data-theme='light'] { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --code-background: var(--light-code-background); -} - -:root[data-theme='dark'] { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --code-background: var(--dark-code-background); -} - -.hl-0 { color: var(--hl-0); } -.hl-1 { color: var(--hl-1); } -pre, code { background: var(--code-background); } diff --git a/docs/assets/main.js b/docs/assets/main.js deleted file mode 100644 index c815b33..0000000 --- a/docs/assets/main.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -(()=>{var Qe=Object.create;var ae=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Ce=Object.getOwnPropertyNames;var Oe=Object.getPrototypeOf,Re=Object.prototype.hasOwnProperty;var _e=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Me=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ce(e))!Re.call(t,i)&&i!==n&&ae(t,i,{get:()=>e[i],enumerable:!(r=Pe(e,i))||r.enumerable});return t};var De=(t,e,n)=>(n=t!=null?Qe(Oe(t)):{},Me(e||!t||!t.__esModule?ae(n,"default",{value:t,enumerable:!0}):n,t));var de=_e((ce,he)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var h=t.utils.clone(n)||{};h.position=[a,l],h.index=s.length,s.push(new t.Token(r.slice(a,o),h))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. -`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ou?h+=2:a==u&&(n+=r[l+1]*i[h+1],l+=2,h+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var h=s.str.charAt(0),m=s.str.charAt(1),v;m in s.node.edges?v=s.node.edges[m]:(v=new t.TokenSet,s.node.edges[m]=v),s.str.length==1&&(v.final=!0),i.push({node:v,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof ce=="object"?he.exports=n():e.lunr=n()}(this,function(){return t})})()});var le=[];function j(t,e){le.push({selector:e,constructor:t})}var Y=class{constructor(){this.createComponents(document.body)}createComponents(e){le.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r}),r.dataset.hasInstance=String(!0))})})}};var k=class{constructor(e){this.el=e.el}};var J=class{constructor(){this.listeners={}}addEventListener(e,n){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push(n)}removeEventListener(e,n){if(!(e in this.listeners))return;let r=this.listeners[e];for(let i=0,s=r.length;i{let n=Date.now();return(...r)=>{n+e-Date.now()<0&&(t(...r),n=Date.now())}};var re=class extends J{constructor(){super();this.scrollTop=0;this.lastY=0;this.width=0;this.height=0;this.showToolbar=!0;this.toolbar=document.querySelector(".tsd-page-toolbar"),this.navigation=document.querySelector(".col-menu"),window.addEventListener("scroll",ne(()=>this.onScroll(),10)),window.addEventListener("resize",ne(()=>this.onResize(),10)),this.searchInput=document.querySelector("#tsd-search input"),this.searchInput&&this.searchInput.addEventListener("focus",()=>{this.hideShowToolbar()}),this.onResize(),this.onScroll()}triggerResize(){let n=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(n)}onResize(){this.width=window.innerWidth||0,this.height=window.innerHeight||0;let n=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(n)}onScroll(){this.scrollTop=window.scrollY||0;let n=new CustomEvent("scroll",{detail:{scrollTop:this.scrollTop}});this.dispatchEvent(n),this.hideShowToolbar()}hideShowToolbar(){let n=this.showToolbar;this.showToolbar=this.lastY>=this.scrollTop||this.scrollTop<=0||!!this.searchInput&&this.searchInput===document.activeElement,n!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),this.navigation?.classList.toggle("col-menu--hide")),this.lastY=this.scrollTop}},R=re;R.instance=new re;var X=class extends k{constructor(n){super(n);this.anchors=[];this.index=-1;R.instance.addEventListener("resize",()=>this.onResize()),R.instance.addEventListener("scroll",r=>this.onScroll(r)),this.createAnchors()}createAnchors(){let n=window.location.href;n.indexOf("#")!=-1&&(n=n.substring(0,n.indexOf("#"))),this.el.querySelectorAll("a").forEach(r=>{let i=r.href;if(i.indexOf("#")==-1||i.substring(0,n.length)!=n)return;let s=i.substring(i.indexOf("#")+1),o=document.querySelector("a.tsd-anchor[name="+s+"]"),a=r.parentNode;!o||!a||this.anchors.push({link:a,anchor:o,position:0})}),this.onResize()}onResize(){let n;for(let i=0,s=this.anchors.length;ii.position-s.position);let r=new CustomEvent("scroll",{detail:{scrollTop:R.instance.scrollTop}});this.onScroll(r)}onScroll(n){let r=n.detail.scrollTop+5,i=this.anchors,s=i.length-1,o=this.index;for(;o>-1&&i[o].position>r;)o-=1;for(;o-1&&this.anchors[this.index].link.classList.remove("focus"),this.index=o,this.index>-1&&this.anchors[this.index].link.classList.add("focus"))}};var ue=(t,e=100)=>{let n;return(...r)=>{clearTimeout(n),n=setTimeout(()=>t(r),e)}};var me=De(de());function ve(){let t=document.getElementById("tsd-search");if(!t)return;let e=document.getElementById("search-script");t.classList.add("loading"),e&&(e.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),e.addEventListener("load",()=>{t.classList.remove("loading"),t.classList.add("ready")}),window.searchData&&t.classList.remove("loading"));let n=document.querySelector("#tsd-search input"),r=document.querySelector("#tsd-search .results");if(!n||!r)throw new Error("The input field or the result list wrapper was not found");let i=!1;r.addEventListener("mousedown",()=>i=!0),r.addEventListener("mouseup",()=>{i=!1,t.classList.remove("has-focus")}),n.addEventListener("focus",()=>t.classList.add("has-focus")),n.addEventListener("blur",()=>{i||(i=!1,t.classList.remove("has-focus"))});let s={base:t.dataset.base+"/"};Fe(t,r,n,s)}function Fe(t,e,n,r){n.addEventListener("input",ue(()=>{Ae(t,e,n,r)},200));let i=!1;n.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ve(e,n):s.key=="Escape"?n.blur():s.key=="ArrowUp"?fe(e,-1):s.key==="ArrowDown"?fe(e,1):i=!1}),n.addEventListener("keypress",s=>{i&&s.preventDefault()}),document.body.addEventListener("keydown",s=>{s.altKey||s.ctrlKey||s.metaKey||!n.matches(":focus")&&s.key==="/"&&(n.focus(),s.preventDefault())})}function He(t,e){t.index||window.searchData&&(e.classList.remove("loading"),e.classList.add("ready"),t.data=window.searchData,t.index=me.Index.load(window.searchData.index))}function Ae(t,e,n,r){if(He(r,t),!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s=i?r.index.search(`*${i}*`):[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o${pe(u.parent,i)}.${l}`);let h=document.createElement("li");h.classList.value=u.classes??"";let m=document.createElement("a");m.href=r.base+u.url,m.innerHTML=l,h.append(m),e.appendChild(h)}}function fe(t,e){let n=t.querySelector(".current");if(!n)n=t.querySelector(e==1?"li:first-child":"li:last-child"),n&&n.classList.add("current");else{let r=n;if(e===1)do r=r.nextElementSibling??void 0;while(r instanceof HTMLElement&&r.offsetParent==null);else do r=r.previousElementSibling??void 0;while(r instanceof HTMLElement&&r.offsetParent==null);r&&(n.classList.remove("current"),r.classList.add("current"))}}function Ve(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),e.blur()}}function pe(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ie(t.substring(s,o)),`${ie(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ie(t.substring(s))),i.join("")}var Ne={"&":"&","<":"<",">":">","'":"'",'"':"""};function ie(t){return t.replace(/[&<>"'"]/g,e=>Ne[e])}var F="mousedown",ye="mousemove",B="mouseup",Z={x:0,y:0},ge=!1,se=!1,je=!1,H=!1,xe=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(xe?"is-mobile":"not-mobile");xe&&"ontouchstart"in document.documentElement&&(je=!0,F="touchstart",ye="touchmove",B="touchend");document.addEventListener(F,t=>{se=!0,H=!1;let e=F=="touchstart"?t.targetTouches[0]:t;Z.y=e.pageY||0,Z.x=e.pageX||0});document.addEventListener(ye,t=>{if(!!se&&!H){let e=F=="touchstart"?t.targetTouches[0]:t,n=Z.x-(e.pageX||0),r=Z.y-(e.pageY||0);H=Math.sqrt(n*n+r*r)>10}});document.addEventListener(B,()=>{se=!1});document.addEventListener("click",t=>{ge&&(t.preventDefault(),t.stopImmediatePropagation(),ge=!1)});var K=class extends k{constructor(n){super(n);this.className=this.el.dataset.toggle||"",this.el.addEventListener(B,r=>this.onPointerUp(r)),this.el.addEventListener("click",r=>r.preventDefault()),document.addEventListener(F,r=>this.onDocumentPointerDown(r)),document.addEventListener(B,r=>this.onDocumentPointerUp(r))}setActive(n){if(this.active==n)return;this.active=n,document.documentElement.classList.toggle("has-"+this.className,n),this.el.classList.toggle("active",n);let r=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(r),setTimeout(()=>document.documentElement.classList.remove(r),500)}onPointerUp(n){H||(this.setActive(!0),n.preventDefault())}onDocumentPointerDown(n){if(this.active){if(n.target.closest(".col-menu, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(n){if(!H&&this.active&&n.target.closest(".col-menu")){let r=n.target.closest("a");if(r){let i=window.location.href;i.indexOf("#")!=-1&&(i=i.substring(0,i.indexOf("#"))),r.href.substring(0,i.length)==i&&setTimeout(()=>this.setActive(!1),250)}}}};var oe;try{oe=localStorage}catch{oe={getItem(){return null},setItem(){}}}var Q=oe;var Le=document.head.appendChild(document.createElement("style"));Le.dataset.for="filters";var ee=class extends k{constructor(n){super(n);this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),Le.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } -`}fromLocalStorage(){let n=Q.getItem(this.key);return n?n==="true":this.el.checked}setLocalStorage(n){Q.setItem(this.key,n.toString()),this.value=n,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),document.querySelectorAll(".tsd-index-section").forEach(n=>{n.style.display="block";let r=Array.from(n.querySelectorAll(".tsd-index-link")).every(i=>i.offsetParent==null);n.style.display=r?"none":"block"})}};var te=class extends k{constructor(n){super(n);this.calculateHeights(),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.textContent.replace(/\s+/g,"-").toLowerCase()}`,this.setLocalStorage(this.fromLocalStorage(),!0),this.summary.addEventListener("click",r=>this.toggleVisibility(r)),this.icon.style.transform=this.getIconRotation()}getIconRotation(n=this.el.open){return`rotate(${n?0:-90}deg)`}calculateHeights(){let n=this.el.open,{position:r,left:i}=this.el.style;this.el.style.position="fixed",this.el.style.left="-9999px",this.el.open=!0,this.expandedHeight=this.el.offsetHeight+"px",this.el.open=!1,this.collapsedHeight=this.el.offsetHeight+"px",this.el.open=n,this.el.style.height=n?this.expandedHeight:this.collapsedHeight,this.el.style.position=r,this.el.style.left=i}toggleVisibility(n){n.preventDefault(),this.el.style.overflow="hidden",this.el.open?this.collapse():this.expand()}expand(n=!0){this.el.open=!0,this.animate(this.collapsedHeight,this.expandedHeight,{opening:!0,duration:n?300:0})}collapse(n=!0){this.animate(this.expandedHeight,this.collapsedHeight,{opening:!1,duration:n?300:0})}animate(n,r,{opening:i,duration:s=300}){if(this.animation)return;let o={duration:s,easing:"ease"};this.animation=this.el.animate({height:[n,r]},o),this.icon.animate({transform:[this.icon.style.transform||this.getIconRotation(!i),this.getIconRotation(i)]},o).addEventListener("finish",()=>{this.icon.style.transform=this.getIconRotation(i)}),this.animation.addEventListener("finish",()=>this.animationEnd(i))}animationEnd(n){this.el.open=n,this.animation=void 0,this.el.style.height="auto",this.el.style.overflow="visible",this.setLocalStorage(n)}fromLocalStorage(){let n=Q.getItem(this.key);return n?n==="true":this.el.open}setLocalStorage(n,r=!1){this.fromLocalStorage()===n&&!r||(Q.setItem(this.key,n.toString()),this.el.open=n,this.handleValueChange(r))}handleValueChange(n=!1){this.fromLocalStorage()===this.el.open&&!n||(this.fromLocalStorage()?this.expand(!1):this.collapse(!1))}};function be(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,Ee(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),Ee(t.value)})}function Ee(t){document.documentElement.dataset.theme=t}ve();j(X,".menu-highlight");j(K,"a[data-toggle]");j(te,".tsd-index-accordion");j(ee,".tsd-filter-item input[type=checkbox]");var Se=document.getElementById("theme");Se&&be(Se);var Be=new Y;Object.defineProperty(window,"app",{value:Be});})(); -/*! - * lunr.Builder - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Index - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Pipeline - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Set - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.TokenSet - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Vector - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.stemmer - * Copyright (C) 2020 Oliver Nightingale - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - */ -/*! - * lunr.stopWordFilter - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.tokenizer - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.trimmer - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.utils - * Copyright (C) 2020 Oliver Nightingale - */ -/** - * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 - * Copyright (C) 2020 Oliver Nightingale - * @license MIT - */ diff --git a/docs/assets/search.js b/docs/assets/search.js deleted file mode 100644 index 2d63ef9..0000000 --- a/docs/assets/search.js +++ /dev/null @@ -1 +0,0 @@ -window.searchData = JSON.parse("{\"kinds\":{\"32\":\"Variable\",\"64\":\"Function\",\"128\":\"Class\",\"256\":\"Interface\",\"512\":\"Constructor\",\"1024\":\"Property\",\"2048\":\"Method\",\"65536\":\"Type literal\",\"262144\":\"Accessor\",\"4194304\":\"Type alias\"},\"rows\":[{\"kind\":256,\"name\":\"ChildProcessFactory\",\"url\":\"interfaces/ChildProcessFactory.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":1024,\"name\":\"processFactory\",\"url\":\"interfaces/ChildProcessFactory.html#processFactory\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"ChildProcessFactory\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/ChildProcessFactory.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"ChildProcessFactory\"},{\"kind\":128,\"name\":\"BatchCluster\",\"url\":\"classes/BatchCluster.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/BatchCluster.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":1024,\"name\":\"options\",\"url\":\"classes/BatchCluster.html#options\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":1024,\"name\":\"emitter\",\"url\":\"classes/BatchCluster.html#emitter\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":1024,\"name\":\"on\",\"url\":\"classes/BatchCluster.html#on\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/BatchCluster.html#__type-2\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":1024,\"name\":\"off\",\"url\":\"classes/BatchCluster.html#off\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/BatchCluster.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"ended\",\"url\":\"classes/BatchCluster.html#ended-1\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":2048,\"name\":\"end\",\"url\":\"classes/BatchCluster.html#end\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":2048,\"name\":\"enqueueTask\",\"url\":\"classes/BatchCluster.html#enqueueTask\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"isIdle\",\"url\":\"classes/BatchCluster.html#isIdle\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"pendingTaskCount\",\"url\":\"classes/BatchCluster.html#pendingTaskCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"meanTasksPerProc\",\"url\":\"classes/BatchCluster.html#meanTasksPerProc\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"spawnedProcCount\",\"url\":\"classes/BatchCluster.html#spawnedProcCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"procCount\",\"url\":\"classes/BatchCluster.html#procCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"busyProcCount\",\"url\":\"classes/BatchCluster.html#busyProcCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"startingProcCount\",\"url\":\"classes/BatchCluster.html#startingProcCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"pendingTasks\",\"url\":\"classes/BatchCluster.html#pendingTasks\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"currentTasks\",\"url\":\"classes/BatchCluster.html#currentTasks\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"internalErrorCount\",\"url\":\"classes/BatchCluster.html#internalErrorCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":2048,\"name\":\"pids\",\"url\":\"classes/BatchCluster.html#pids\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":2048,\"name\":\"stats\",\"url\":\"classes/BatchCluster.html#stats\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5\",\"classes\":\"tsd-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats\"},{\"kind\":1024,\"name\":\"pendingTaskCount\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.pendingTaskCount-2\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"currentProcCount\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.currentProcCount\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"readyProcCount\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.readyProcCount\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"maxProcCount\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.maxProcCount\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"internalErrorCount\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.internalErrorCount-2\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"startErrorRatePerMinute\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.startErrorRatePerMinute\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"msBeforeNextSpawn\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.msBeforeNextSpawn\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"spawnedProcCount\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.spawnedProcCount-2\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"childEndCounts\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.childEndCounts-2\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"startError\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.startError-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"timeout\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.timeout-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"broken\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.broken-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"closed\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.closed-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"ending\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.ending-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"ended\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.ended-3\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"idle\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.idle-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"old\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.old-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"proc.close\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.proc_close-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"proc.disconnect\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.proc_disconnect-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"proc.error\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.proc_error-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"proc.exit\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.proc_exit-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"stderr.error\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.stderr_error-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"stderr\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.stderr-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"stdin.error\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.stdin_error-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"stdout.error\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.stdout_error-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"tooMany\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.tooMany-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"unhealthy\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.unhealthy-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"worn\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.__type-6.worn-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type.__type\"},{\"kind\":1024,\"name\":\"ending\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.ending-2\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":1024,\"name\":\"ended\",\"url\":\"classes/BatchCluster.html#stats.stats-1.__type-5.ended-4\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.stats.stats.__type\"},{\"kind\":2048,\"name\":\"countEndedChildProcs\",\"url\":\"classes/BatchCluster.html#countEndedChildProcs\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":262144,\"name\":\"childEndCounts\",\"url\":\"classes/BatchCluster.html#childEndCounts\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4\",\"classes\":\"tsd-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts\"},{\"kind\":1024,\"name\":\"startError\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.startError\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"timeout\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.timeout\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"broken\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.broken\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"closed\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.closed\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"ending\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.ending\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"ended\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.ended\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"idle\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.idle\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"old\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.old\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"proc.close\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.proc_close\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"proc.disconnect\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.proc_disconnect\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"proc.error\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.proc_error\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"proc.exit\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.proc_exit\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"stderr.error\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.stderr_error\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"stderr\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.stderr\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"stdin.error\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.stdin_error\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"stdout.error\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.stdout_error\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"tooMany\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.tooMany\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"unhealthy\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.unhealthy\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":1024,\"name\":\"worn\",\"url\":\"classes/BatchCluster.html#childEndCounts.childEndCounts-1.__type-4.worn\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"BatchCluster.childEndCounts.childEndCounts.__type\"},{\"kind\":2048,\"name\":\"closeChildProcesses\",\"url\":\"classes/BatchCluster.html#closeChildProcesses\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":2048,\"name\":\"setMaxProcs\",\"url\":\"classes/BatchCluster.html#setMaxProcs\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":2048,\"name\":\"vacuumProcs\",\"url\":\"classes/BatchCluster.html#vacuumProcs\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchCluster\"},{\"kind\":128,\"name\":\"BatchClusterOptions\",\"url\":\"classes/BatchClusterOptions.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/BatchClusterOptions.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"maxProcs\",\"url\":\"classes/BatchClusterOptions.html#maxProcs\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"maxProcAgeMillis\",\"url\":\"classes/BatchClusterOptions.html#maxProcAgeMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"onIdleIntervalMillis\",\"url\":\"classes/BatchClusterOptions.html#onIdleIntervalMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"maxReasonableProcessFailuresPerMinute\",\"url\":\"classes/BatchClusterOptions.html#maxReasonableProcessFailuresPerMinute\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"spawnTimeoutMillis\",\"url\":\"classes/BatchClusterOptions.html#spawnTimeoutMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"minDelayBetweenSpawnMillis\",\"url\":\"classes/BatchClusterOptions.html#minDelayBetweenSpawnMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"taskTimeoutMillis\",\"url\":\"classes/BatchClusterOptions.html#taskTimeoutMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"maxTasksPerProcess\",\"url\":\"classes/BatchClusterOptions.html#maxTasksPerProcess\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"endGracefulWaitTimeMillis\",\"url\":\"classes/BatchClusterOptions.html#endGracefulWaitTimeMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"streamFlushMillis\",\"url\":\"classes/BatchClusterOptions.html#streamFlushMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"cleanupChildProcs\",\"url\":\"classes/BatchClusterOptions.html#cleanupChildProcs\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"maxIdleMsPerProcess\",\"url\":\"classes/BatchClusterOptions.html#maxIdleMsPerProcess\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"maxFailedTasksPerProcess\",\"url\":\"classes/BatchClusterOptions.html#maxFailedTasksPerProcess\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"healthCheckIntervalMillis\",\"url\":\"classes/BatchClusterOptions.html#healthCheckIntervalMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"pidCheckIntervalMillis\",\"url\":\"classes/BatchClusterOptions.html#pidCheckIntervalMillis\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":1024,\"name\":\"logger\",\"url\":\"classes/BatchClusterOptions.html#logger\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/BatchClusterOptions.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-class\",\"parent\":\"BatchClusterOptions\"},{\"kind\":128,\"name\":\"BatchProcess\",\"url\":\"classes/BatchProcess.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/BatchProcess.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"name\",\"url\":\"classes/BatchProcess.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"pid\",\"url\":\"classes/BatchProcess.html#pid\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"start\",\"url\":\"classes/BatchProcess.html#start\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"startupTaskId\",\"url\":\"classes/BatchProcess.html#startupTaskId\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"failedTaskCount\",\"url\":\"classes/BatchProcess.html#failedTaskCount\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"proc\",\"url\":\"classes/BatchProcess.html#proc\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":1024,\"name\":\"opts\",\"url\":\"classes/BatchProcess.html#opts\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"currentTask\",\"url\":\"classes/BatchProcess.html#currentTask\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"taskCount\",\"url\":\"classes/BatchProcess.html#taskCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"starting\",\"url\":\"classes/BatchProcess.html#starting\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"ending\",\"url\":\"classes/BatchProcess.html#ending\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"ended\",\"url\":\"classes/BatchProcess.html#ended\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"exited\",\"url\":\"classes/BatchProcess.html#exited\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"whyNotHealthy\",\"url\":\"classes/BatchProcess.html#whyNotHealthy\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"healthy\",\"url\":\"classes/BatchProcess.html#healthy\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"idle\",\"url\":\"classes/BatchProcess.html#idle\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"whyNotReady\",\"url\":\"classes/BatchProcess.html#whyNotReady\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"ready\",\"url\":\"classes/BatchProcess.html#ready\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":262144,\"name\":\"idleMs\",\"url\":\"classes/BatchProcess.html#idleMs\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":2048,\"name\":\"running\",\"url\":\"classes/BatchProcess.html#running\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":2048,\"name\":\"notRunning\",\"url\":\"classes/BatchProcess.html#notRunning\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":2048,\"name\":\"maybeRunHealthcheck\",\"url\":\"classes/BatchProcess.html#maybeRunHealthcheck\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":2048,\"name\":\"execTask\",\"url\":\"classes/BatchProcess.html#execTask\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":2048,\"name\":\"end\",\"url\":\"classes/BatchProcess.html#end\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"BatchProcess\"},{\"kind\":128,\"name\":\"Deferred\",\"url\":\"classes/Deferred.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/Deferred.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":1024,\"name\":\"promise\",\"url\":\"classes/Deferred.html#promise\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":262144,\"name\":\"pending\",\"url\":\"classes/Deferred.html#pending\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":262144,\"name\":\"fulfilled\",\"url\":\"classes/Deferred.html#fulfilled\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":262144,\"name\":\"rejected\",\"url\":\"classes/Deferred.html#rejected\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":262144,\"name\":\"settled\",\"url\":\"classes/Deferred.html#settled\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":2048,\"name\":\"then\",\"url\":\"classes/Deferred.html#then\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":2048,\"name\":\"catch\",\"url\":\"classes/Deferred.html#catch\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":2048,\"name\":\"resolve\",\"url\":\"classes/Deferred.html#resolve\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":2048,\"name\":\"reject\",\"url\":\"classes/Deferred.html#reject\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":2048,\"name\":\"observe\",\"url\":\"classes/Deferred.html#observe\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":2048,\"name\":\"observeQuietly\",\"url\":\"classes/Deferred.html#observeQuietly\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":1024,\"name\":\"[toStringTag]\",\"url\":\"classes/Deferred.html#_toStringTag_\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Deferred\"},{\"kind\":64,\"name\":\"SimpleParser\",\"url\":\"functions/SimpleParser.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":64,\"name\":\"kill\",\"url\":\"functions/kill.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":64,\"name\":\"pidExists\",\"url\":\"functions/pidExists.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":64,\"name\":\"pids\",\"url\":\"functions/pids.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":128,\"name\":\"Rate\",\"url\":\"classes/Rate.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/Rate.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":1024,\"name\":\"periodMs\",\"url\":\"classes/Rate.html#periodMs\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":1024,\"name\":\"warmupMs\",\"url\":\"classes/Rate.html#warmupMs\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":2048,\"name\":\"onEvent\",\"url\":\"classes/Rate.html#onEvent\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":262144,\"name\":\"eventCount\",\"url\":\"classes/Rate.html#eventCount\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":262144,\"name\":\"msSinceLastEvent\",\"url\":\"classes/Rate.html#msSinceLastEvent\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":262144,\"name\":\"msPerEvent\",\"url\":\"classes/Rate.html#msPerEvent\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":262144,\"name\":\"eventsPerMs\",\"url\":\"classes/Rate.html#eventsPerMs\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":262144,\"name\":\"eventsPerSecond\",\"url\":\"classes/Rate.html#eventsPerSecond\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":262144,\"name\":\"eventsPerMinute\",\"url\":\"classes/Rate.html#eventsPerMinute\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":2048,\"name\":\"clear\",\"url\":\"classes/Rate.html#clear\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Rate\"},{\"kind\":128,\"name\":\"Task\",\"url\":\"classes/Task.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/Task.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":1024,\"name\":\"taskId\",\"url\":\"classes/Task.html#taskId\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":1024,\"name\":\"command\",\"url\":\"classes/Task.html#command\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":1024,\"name\":\"parser\",\"url\":\"classes/Task.html#parser\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":262144,\"name\":\"promise\",\"url\":\"classes/Task.html#promise\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":262144,\"name\":\"pending\",\"url\":\"classes/Task.html#pending\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":262144,\"name\":\"state\",\"url\":\"classes/Task.html#state\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":2048,\"name\":\"onStart\",\"url\":\"classes/Task.html#onStart\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":262144,\"name\":\"runtimeMs\",\"url\":\"classes/Task.html#runtimeMs\",\"classes\":\"tsd-kind-accessor tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":2048,\"name\":\"toString\",\"url\":\"classes/Task.html#toString\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":2048,\"name\":\"onStdout\",\"url\":\"classes/Task.html#onStdout\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":2048,\"name\":\"onStderr\",\"url\":\"classes/Task.html#onStderr\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":2048,\"name\":\"reject\",\"url\":\"classes/Task.html#reject\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"Task\"},{\"kind\":4194304,\"name\":\"BatchClusterEmitter\",\"url\":\"types/BatchClusterEmitter.html\",\"classes\":\"tsd-kind-type-alias\"},{\"kind\":256,\"name\":\"BatchClusterEvents\",\"url\":\"interfaces/BatchClusterEvents.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":1024,\"name\":\"childStart\",\"url\":\"interfaces/BatchClusterEvents.html#childStart\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-4\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"childEnd\",\"url\":\"interfaces/BatchClusterEvents.html#childEnd\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-2\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"startError\",\"url\":\"interfaces/BatchClusterEvents.html#startError\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-18\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"internalError\",\"url\":\"interfaces/BatchClusterEvents.html#internalError\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-14\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"fatalError\",\"url\":\"interfaces/BatchClusterEvents.html#fatalError\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-10\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"taskData\",\"url\":\"interfaces/BatchClusterEvents.html#taskData\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-20\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"taskResolved\",\"url\":\"interfaces/BatchClusterEvents.html#taskResolved\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-24\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"taskTimeout\",\"url\":\"interfaces/BatchClusterEvents.html#taskTimeout\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-26\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"taskError\",\"url\":\"interfaces/BatchClusterEvents.html#taskError\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-22\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"noTaskData\",\"url\":\"interfaces/BatchClusterEvents.html#noTaskData\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-16\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"healthCheckError\",\"url\":\"interfaces/BatchClusterEvents.html#healthCheckError\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-12\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"endError\",\"url\":\"interfaces/BatchClusterEvents.html#endError\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-8\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"beforeEnd\",\"url\":\"interfaces/BatchClusterEvents.html#beforeEnd\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":1024,\"name\":\"end\",\"url\":\"interfaces/BatchClusterEvents.html#end\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"interfaces/BatchClusterEvents.html#__type-6\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-interface\",\"parent\":\"BatchClusterEvents\"},{\"kind\":256,\"name\":\"BatchProcessOptions\",\"url\":\"interfaces/BatchProcessOptions.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":1024,\"name\":\"versionCommand\",\"url\":\"interfaces/BatchProcessOptions.html#versionCommand\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchProcessOptions\"},{\"kind\":1024,\"name\":\"healthCheckCommand\",\"url\":\"interfaces/BatchProcessOptions.html#healthCheckCommand\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchProcessOptions\"},{\"kind\":1024,\"name\":\"pass\",\"url\":\"interfaces/BatchProcessOptions.html#pass\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchProcessOptions\"},{\"kind\":1024,\"name\":\"fail\",\"url\":\"interfaces/BatchProcessOptions.html#fail\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchProcessOptions\"},{\"kind\":1024,\"name\":\"exitCommand\",\"url\":\"interfaces/BatchProcessOptions.html#exitCommand\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"BatchProcessOptions\"},{\"kind\":4194304,\"name\":\"ChildExitReason\",\"url\":\"types/ChildExitReason.html\",\"classes\":\"tsd-kind-type-alias\"},{\"kind\":256,\"name\":\"Parser\",\"url\":\"interfaces/Parser.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":4194304,\"name\":\"WhyNotHealthy\",\"url\":\"types/WhyNotHealthy.html\",\"classes\":\"tsd-kind-type-alias\"},{\"kind\":4194304,\"name\":\"WhyNotReady\",\"url\":\"types/WhyNotReady.html\",\"classes\":\"tsd-kind-type-alias\"},{\"kind\":64,\"name\":\"setLogger\",\"url\":\"functions/setLogger.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":64,\"name\":\"logger\",\"url\":\"functions/logger-1.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":256,\"name\":\"Logger\",\"url\":\"interfaces/Logger.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":1024,\"name\":\"trace\",\"url\":\"interfaces/Logger.html#trace\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Logger\"},{\"kind\":1024,\"name\":\"debug\",\"url\":\"interfaces/Logger.html#debug\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Logger\"},{\"kind\":1024,\"name\":\"info\",\"url\":\"interfaces/Logger.html#info\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Logger\"},{\"kind\":1024,\"name\":\"warn\",\"url\":\"interfaces/Logger.html#warn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Logger\"},{\"kind\":1024,\"name\":\"error\",\"url\":\"interfaces/Logger.html#error\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Logger\"},{\"kind\":32,\"name\":\"LogLevels\",\"url\":\"variables/LogLevels.html\",\"classes\":\"tsd-kind-variable\"},{\"kind\":32,\"name\":\"ConsoleLogger\",\"url\":\"variables/ConsoleLogger.html\",\"classes\":\"tsd-kind-variable\"},{\"kind\":32,\"name\":\"NoLogger\",\"url\":\"variables/NoLogger.html\",\"classes\":\"tsd-kind-variable\"},{\"kind\":32,\"name\":\"Log\",\"url\":\"variables/Log.html\",\"classes\":\"tsd-kind-variable\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/Log.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-variable\",\"parent\":\"Log\"},{\"kind\":1024,\"name\":\"withLevels\",\"url\":\"variables/Log.html#__type.withLevels\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"Log.__type\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/Log.html#__type.__type-3\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-literal\",\"parent\":\"Log.__type\"},{\"kind\":1024,\"name\":\"withTimestamps\",\"url\":\"variables/Log.html#__type.withTimestamps\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"Log.__type\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/Log.html#__type.__type-5\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-literal\",\"parent\":\"Log.__type\"},{\"kind\":1024,\"name\":\"filterLevels\",\"url\":\"variables/Log.html#__type.filterLevels\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"Log.__type\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/Log.html#__type.__type-1\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-literal\",\"parent\":\"Log.__type\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,50.413]],[\"comment/0\",[]],[\"name/1\",[1,50.413]],[\"comment/1\",[]],[\"name/2\",[2,22.081]],[\"comment/2\",[]],[\"name/3\",[3,50.413]],[\"comment/3\",[]],[\"name/4\",[4,35.749]],[\"comment/4\",[]],[\"name/5\",[5,50.413]],[\"comment/5\",[]],[\"name/6\",[6,50.413]],[\"comment/6\",[]],[\"name/7\",[7,50.413]],[\"comment/7\",[]],[\"name/8\",[2,22.081]],[\"comment/8\",[]],[\"name/9\",[8,50.413]],[\"comment/9\",[]],[\"name/10\",[2,22.081]],[\"comment/10\",[]],[\"name/11\",[9,37.42]],[\"comment/11\",[]],[\"name/12\",[10,41.94]],[\"comment/12\",[]],[\"name/13\",[11,50.413]],[\"comment/13\",[]],[\"name/14\",[12,50.413]],[\"comment/14\",[]],[\"name/15\",[13,45.304]],[\"comment/15\",[]],[\"name/16\",[14,50.413]],[\"comment/16\",[]],[\"name/17\",[15,45.304]],[\"comment/17\",[]],[\"name/18\",[16,50.413]],[\"comment/18\",[]],[\"name/19\",[17,50.413]],[\"comment/19\",[]],[\"name/20\",[18,50.413]],[\"comment/20\",[]],[\"name/21\",[19,50.413]],[\"comment/21\",[]],[\"name/22\",[20,50.413]],[\"comment/22\",[]],[\"name/23\",[21,45.304]],[\"comment/23\",[]],[\"name/24\",[22,45.304]],[\"comment/24\",[]],[\"name/25\",[23,50.413]],[\"comment/25\",[]],[\"name/26\",[2,22.081]],[\"comment/26\",[]],[\"name/27\",[13,45.304]],[\"comment/27\",[]],[\"name/28\",[24,50.413]],[\"comment/28\",[]],[\"name/29\",[25,50.413]],[\"comment/29\",[]],[\"name/30\",[26,50.413]],[\"comment/30\",[]],[\"name/31\",[21,45.304]],[\"comment/31\",[]],[\"name/32\",[27,50.413]],[\"comment/32\",[]],[\"name/33\",[28,50.413]],[\"comment/33\",[]],[\"name/34\",[15,45.304]],[\"comment/34\",[]],[\"name/35\",[29,45.304]],[\"comment/35\",[]],[\"name/36\",[2,22.081]],[\"comment/36\",[]],[\"name/37\",[30,41.94]],[\"comment/37\",[]],[\"name/38\",[31,45.304]],[\"comment/38\",[]],[\"name/39\",[32,45.304]],[\"comment/39\",[]],[\"name/40\",[33,45.304]],[\"comment/40\",[]],[\"name/41\",[34,39.427]],[\"comment/41\",[]],[\"name/42\",[9,37.42]],[\"comment/42\",[]],[\"name/43\",[35,41.94]],[\"comment/43\",[]],[\"name/44\",[36,45.304]],[\"comment/44\",[]],[\"name/45\",[37,45.304]],[\"comment/45\",[]],[\"name/46\",[38,45.304]],[\"comment/46\",[]],[\"name/47\",[39,45.304]],[\"comment/47\",[]],[\"name/48\",[40,45.304]],[\"comment/48\",[]],[\"name/49\",[41,45.304]],[\"comment/49\",[]],[\"name/50\",[42,45.304]],[\"comment/50\",[]],[\"name/51\",[43,45.304]],[\"comment/51\",[]],[\"name/52\",[44,45.304]],[\"comment/52\",[]],[\"name/53\",[45,45.304]],[\"comment/53\",[]],[\"name/54\",[46,45.304]],[\"comment/54\",[]],[\"name/55\",[47,45.304]],[\"comment/55\",[]],[\"name/56\",[34,39.427]],[\"comment/56\",[]],[\"name/57\",[9,37.42]],[\"comment/57\",[]],[\"name/58\",[48,50.413]],[\"comment/58\",[]],[\"name/59\",[29,45.304]],[\"comment/59\",[]],[\"name/60\",[2,22.081]],[\"comment/60\",[]],[\"name/61\",[30,41.94]],[\"comment/61\",[]],[\"name/62\",[31,45.304]],[\"comment/62\",[]],[\"name/63\",[32,45.304]],[\"comment/63\",[]],[\"name/64\",[33,45.304]],[\"comment/64\",[]],[\"name/65\",[34,39.427]],[\"comment/65\",[]],[\"name/66\",[9,37.42]],[\"comment/66\",[]],[\"name/67\",[35,41.94]],[\"comment/67\",[]],[\"name/68\",[36,45.304]],[\"comment/68\",[]],[\"name/69\",[37,45.304]],[\"comment/69\",[]],[\"name/70\",[38,45.304]],[\"comment/70\",[]],[\"name/71\",[39,45.304]],[\"comment/71\",[]],[\"name/72\",[40,45.304]],[\"comment/72\",[]],[\"name/73\",[41,45.304]],[\"comment/73\",[]],[\"name/74\",[42,45.304]],[\"comment/74\",[]],[\"name/75\",[43,45.304]],[\"comment/75\",[]],[\"name/76\",[44,45.304]],[\"comment/76\",[]],[\"name/77\",[45,45.304]],[\"comment/77\",[]],[\"name/78\",[46,45.304]],[\"comment/78\",[]],[\"name/79\",[47,45.304]],[\"comment/79\",[]],[\"name/80\",[49,50.413]],[\"comment/80\",[]],[\"name/81\",[50,50.413]],[\"comment/81\",[]],[\"name/82\",[51,50.413]],[\"comment/82\",[]],[\"name/83\",[52,50.413]],[\"comment/83\",[]],[\"name/84\",[4,35.749]],[\"comment/84\",[]],[\"name/85\",[53,50.413]],[\"comment/85\",[]],[\"name/86\",[54,50.413]],[\"comment/86\",[]],[\"name/87\",[55,50.413]],[\"comment/87\",[]],[\"name/88\",[56,50.413]],[\"comment/88\",[]],[\"name/89\",[57,50.413]],[\"comment/89\",[]],[\"name/90\",[58,50.413]],[\"comment/90\",[]],[\"name/91\",[59,50.413]],[\"comment/91\",[]],[\"name/92\",[60,50.413]],[\"comment/92\",[]],[\"name/93\",[61,50.413]],[\"comment/93\",[]],[\"name/94\",[62,50.413]],[\"comment/94\",[]],[\"name/95\",[63,50.413]],[\"comment/95\",[]],[\"name/96\",[64,50.413]],[\"comment/96\",[]],[\"name/97\",[65,50.413]],[\"comment/97\",[]],[\"name/98\",[66,50.413]],[\"comment/98\",[]],[\"name/99\",[67,50.413]],[\"comment/99\",[]],[\"name/100\",[68,41.94]],[\"comment/100\",[]],[\"name/101\",[2,22.081]],[\"comment/101\",[]],[\"name/102\",[69,50.413]],[\"comment/102\",[]],[\"name/103\",[4,35.749]],[\"comment/103\",[]],[\"name/104\",[70,50.413]],[\"comment/104\",[]],[\"name/105\",[71,50.413]],[\"comment/105\",[]],[\"name/106\",[72,50.413]],[\"comment/106\",[]],[\"name/107\",[73,50.413]],[\"comment/107\",[]],[\"name/108\",[74,50.413]],[\"comment/108\",[]],[\"name/109\",[75,50.413]],[\"comment/109\",[]],[\"name/110\",[76,50.413]],[\"comment/110\",[]],[\"name/111\",[77,50.413]],[\"comment/111\",[]],[\"name/112\",[78,50.413]],[\"comment/112\",[]],[\"name/113\",[79,50.413]],[\"comment/113\",[]],[\"name/114\",[34,39.427]],[\"comment/114\",[]],[\"name/115\",[9,37.42]],[\"comment/115\",[]],[\"name/116\",[80,50.413]],[\"comment/116\",[]],[\"name/117\",[81,45.304]],[\"comment/117\",[]],[\"name/118\",[82,50.413]],[\"comment/118\",[]],[\"name/119\",[35,41.94]],[\"comment/119\",[]],[\"name/120\",[83,45.304]],[\"comment/120\",[]],[\"name/121\",[84,50.413]],[\"comment/121\",[]],[\"name/122\",[85,50.413]],[\"comment/122\",[]],[\"name/123\",[86,50.413]],[\"comment/123\",[]],[\"name/124\",[87,50.413]],[\"comment/124\",[]],[\"name/125\",[88,50.413]],[\"comment/125\",[]],[\"name/126\",[89,50.413]],[\"comment/126\",[]],[\"name/127\",[10,41.94]],[\"comment/127\",[]],[\"name/128\",[90,50.413]],[\"comment/128\",[]],[\"name/129\",[4,35.749]],[\"comment/129\",[]],[\"name/130\",[91,45.304]],[\"comment/130\",[]],[\"name/131\",[92,45.304]],[\"comment/131\",[]],[\"name/132\",[93,50.413]],[\"comment/132\",[]],[\"name/133\",[94,50.413]],[\"comment/133\",[]],[\"name/134\",[95,50.413]],[\"comment/134\",[]],[\"name/135\",[96,50.413]],[\"comment/135\",[]],[\"name/136\",[97,50.413]],[\"comment/136\",[]],[\"name/137\",[98,50.413]],[\"comment/137\",[]],[\"name/138\",[99,45.304]],[\"comment/138\",[]],[\"name/139\",[100,50.413]],[\"comment/139\",[]],[\"name/140\",[101,50.413]],[\"comment/140\",[]],[\"name/141\",[102,50.413]],[\"comment/141\",[]],[\"name/142\",[103,50.413]],[\"comment/142\",[]],[\"name/143\",[104,50.413]],[\"comment/143\",[]],[\"name/144\",[105,50.413]],[\"comment/144\",[]],[\"name/145\",[22,45.304]],[\"comment/145\",[]],[\"name/146\",[106,50.413]],[\"comment/146\",[]],[\"name/147\",[4,35.749]],[\"comment/147\",[]],[\"name/148\",[107,50.413]],[\"comment/148\",[]],[\"name/149\",[108,50.413]],[\"comment/149\",[]],[\"name/150\",[109,50.413]],[\"comment/150\",[]],[\"name/151\",[110,50.413]],[\"comment/151\",[]],[\"name/152\",[111,50.413]],[\"comment/152\",[]],[\"name/153\",[112,50.413]],[\"comment/153\",[]],[\"name/154\",[113,50.413]],[\"comment/154\",[]],[\"name/155\",[114,50.413]],[\"comment/155\",[]],[\"name/156\",[115,50.413]],[\"comment/156\",[]],[\"name/157\",[116,50.413]],[\"comment/157\",[]],[\"name/158\",[117,50.413]],[\"comment/158\",[]],[\"name/159\",[4,35.749]],[\"comment/159\",[]],[\"name/160\",[118,50.413]],[\"comment/160\",[]],[\"name/161\",[119,50.413]],[\"comment/161\",[]],[\"name/162\",[120,45.304]],[\"comment/162\",[]],[\"name/163\",[91,45.304]],[\"comment/163\",[]],[\"name/164\",[92,45.304]],[\"comment/164\",[]],[\"name/165\",[121,50.413]],[\"comment/165\",[]],[\"name/166\",[122,50.413]],[\"comment/166\",[]],[\"name/167\",[123,50.413]],[\"comment/167\",[]],[\"name/168\",[124,50.413]],[\"comment/168\",[]],[\"name/169\",[125,50.413]],[\"comment/169\",[]],[\"name/170\",[126,50.413]],[\"comment/170\",[]],[\"name/171\",[99,45.304]],[\"comment/171\",[]],[\"name/172\",[127,50.413]],[\"comment/172\",[]],[\"name/173\",[128,50.413]],[\"comment/173\",[]],[\"name/174\",[129,50.413]],[\"comment/174\",[]],[\"name/175\",[2,22.081]],[\"comment/175\",[]],[\"name/176\",[130,50.413]],[\"comment/176\",[]],[\"name/177\",[2,22.081]],[\"comment/177\",[]],[\"name/178\",[30,41.94]],[\"comment/178\",[]],[\"name/179\",[2,22.081]],[\"comment/179\",[]],[\"name/180\",[131,50.413]],[\"comment/180\",[]],[\"name/181\",[2,22.081]],[\"comment/181\",[]],[\"name/182\",[132,50.413]],[\"comment/182\",[]],[\"name/183\",[2,22.081]],[\"comment/183\",[]],[\"name/184\",[133,50.413]],[\"comment/184\",[]],[\"name/185\",[2,22.081]],[\"comment/185\",[]],[\"name/186\",[134,50.413]],[\"comment/186\",[]],[\"name/187\",[2,22.081]],[\"comment/187\",[]],[\"name/188\",[135,50.413]],[\"comment/188\",[]],[\"name/189\",[2,22.081]],[\"comment/189\",[]],[\"name/190\",[136,50.413]],[\"comment/190\",[]],[\"name/191\",[2,22.081]],[\"comment/191\",[]],[\"name/192\",[137,50.413]],[\"comment/192\",[]],[\"name/193\",[2,22.081]],[\"comment/193\",[]],[\"name/194\",[138,50.413]],[\"comment/194\",[]],[\"name/195\",[2,22.081]],[\"comment/195\",[]],[\"name/196\",[139,50.413]],[\"comment/196\",[]],[\"name/197\",[2,22.081]],[\"comment/197\",[]],[\"name/198\",[140,50.413]],[\"comment/198\",[]],[\"name/199\",[2,22.081]],[\"comment/199\",[]],[\"name/200\",[10,41.94]],[\"comment/200\",[]],[\"name/201\",[2,22.081]],[\"comment/201\",[]],[\"name/202\",[141,50.413]],[\"comment/202\",[]],[\"name/203\",[142,50.413]],[\"comment/203\",[]],[\"name/204\",[143,50.413]],[\"comment/204\",[]],[\"name/205\",[144,50.413]],[\"comment/205\",[]],[\"name/206\",[145,50.413]],[\"comment/206\",[]],[\"name/207\",[146,50.413]],[\"comment/207\",[]],[\"name/208\",[147,50.413]],[\"comment/208\",[]],[\"name/209\",[120,45.304]],[\"comment/209\",[]],[\"name/210\",[81,45.304]],[\"comment/210\",[]],[\"name/211\",[83,45.304]],[\"comment/211\",[]],[\"name/212\",[148,50.413]],[\"comment/212\",[]],[\"name/213\",[68,41.94]],[\"comment/213\",[]],[\"name/214\",[68,41.94]],[\"comment/214\",[]],[\"name/215\",[149,50.413]],[\"comment/215\",[]],[\"name/216\",[150,50.413]],[\"comment/216\",[]],[\"name/217\",[151,50.413]],[\"comment/217\",[]],[\"name/218\",[152,50.413]],[\"comment/218\",[]],[\"name/219\",[153,50.413]],[\"comment/219\",[]],[\"name/220\",[154,50.413]],[\"comment/220\",[]],[\"name/221\",[155,50.413]],[\"comment/221\",[]],[\"name/222\",[156,50.413]],[\"comment/222\",[]],[\"name/223\",[157,50.413]],[\"comment/223\",[]],[\"name/224\",[2,22.081]],[\"comment/224\",[]],[\"name/225\",[158,50.413]],[\"comment/225\",[]],[\"name/226\",[2,22.081]],[\"comment/226\",[]],[\"name/227\",[159,50.413]],[\"comment/227\",[]],[\"name/228\",[2,22.081]],[\"comment/228\",[]],[\"name/229\",[160,50.413]],[\"comment/229\",[]],[\"name/230\",[2,22.081]],[\"comment/230\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":2,\"name\":{\"2\":{},\"8\":{},\"10\":{},\"26\":{},\"36\":{},\"60\":{},\"101\":{},\"175\":{},\"177\":{},\"179\":{},\"181\":{},\"183\":{},\"185\":{},\"187\":{},\"189\":{},\"191\":{},\"193\":{},\"195\":{},\"197\":{},\"199\":{},\"201\":{},\"224\":{},\"226\":{},\"228\":{},\"230\":{}},\"comment\":{}}],[\"batchcluster\",{\"_index\":3,\"name\":{\"3\":{}},\"comment\":{}}],[\"batchclusteremitter\",{\"_index\":127,\"name\":{\"172\":{}},\"comment\":{}}],[\"batchclusterevents\",{\"_index\":128,\"name\":{\"173\":{}},\"comment\":{}}],[\"batchclusteroptions\",{\"_index\":52,\"name\":{\"83\":{}},\"comment\":{}}],[\"batchprocess\",{\"_index\":69,\"name\":{\"102\":{}},\"comment\":{}}],[\"batchprocessoptions\",{\"_index\":141,\"name\":{\"202\":{}},\"comment\":{}}],[\"beforeend\",{\"_index\":140,\"name\":{\"198\":{}},\"comment\":{}}],[\"broken\",{\"_index\":32,\"name\":{\"39\":{},\"63\":{}},\"comment\":{}}],[\"busyproccount\",{\"_index\":17,\"name\":{\"19\":{}},\"comment\":{}}],[\"catch\",{\"_index\":97,\"name\":{\"136\":{}},\"comment\":{}}],[\"childend\",{\"_index\":130,\"name\":{\"176\":{}},\"comment\":{}}],[\"childendcounts\",{\"_index\":29,\"name\":{\"35\":{},\"59\":{}},\"comment\":{}}],[\"childexitreason\",{\"_index\":147,\"name\":{\"208\":{}},\"comment\":{}}],[\"childprocessfactory\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"childstart\",{\"_index\":129,\"name\":{\"174\":{}},\"comment\":{}}],[\"cleanupchildprocs\",{\"_index\":63,\"name\":{\"95\":{}},\"comment\":{}}],[\"clear\",{\"_index\":116,\"name\":{\"157\":{}},\"comment\":{}}],[\"closechildprocesses\",{\"_index\":49,\"name\":{\"80\":{}},\"comment\":{}}],[\"closed\",{\"_index\":33,\"name\":{\"40\":{},\"64\":{}},\"comment\":{}}],[\"command\",{\"_index\":119,\"name\":{\"161\":{}},\"comment\":{}}],[\"consolelogger\",{\"_index\":155,\"name\":{\"221\":{}},\"comment\":{}}],[\"constructor\",{\"_index\":4,\"name\":{\"4\":{},\"84\":{},\"103\":{},\"129\":{},\"147\":{},\"159\":{}},\"comment\":{}}],[\"countendedchildprocs\",{\"_index\":48,\"name\":{\"58\":{}},\"comment\":{}}],[\"currentproccount\",{\"_index\":24,\"name\":{\"28\":{}},\"comment\":{}}],[\"currenttask\",{\"_index\":77,\"name\":{\"111\":{}},\"comment\":{}}],[\"currenttasks\",{\"_index\":20,\"name\":{\"22\":{}},\"comment\":{}}],[\"debug\",{\"_index\":150,\"name\":{\"216\":{}},\"comment\":{}}],[\"deferred\",{\"_index\":90,\"name\":{\"128\":{}},\"comment\":{}}],[\"emitter\",{\"_index\":6,\"name\":{\"6\":{}},\"comment\":{}}],[\"end\",{\"_index\":10,\"name\":{\"12\":{},\"127\":{},\"200\":{}},\"comment\":{}}],[\"ended\",{\"_index\":9,\"name\":{\"11\":{},\"42\":{},\"57\":{},\"66\":{},\"115\":{}},\"comment\":{}}],[\"enderror\",{\"_index\":139,\"name\":{\"196\":{}},\"comment\":{}}],[\"endgracefulwaittimemillis\",{\"_index\":61,\"name\":{\"93\":{}},\"comment\":{}}],[\"ending\",{\"_index\":34,\"name\":{\"41\":{},\"56\":{},\"65\":{},\"114\":{}},\"comment\":{}}],[\"enqueuetask\",{\"_index\":11,\"name\":{\"13\":{}},\"comment\":{}}],[\"error\",{\"_index\":153,\"name\":{\"219\":{}},\"comment\":{}}],[\"eventcount\",{\"_index\":110,\"name\":{\"151\":{}},\"comment\":{}}],[\"eventsperminute\",{\"_index\":115,\"name\":{\"156\":{}},\"comment\":{}}],[\"eventsperms\",{\"_index\":113,\"name\":{\"154\":{}},\"comment\":{}}],[\"eventspersecond\",{\"_index\":114,\"name\":{\"155\":{}},\"comment\":{}}],[\"exectask\",{\"_index\":89,\"name\":{\"126\":{}},\"comment\":{}}],[\"exitcommand\",{\"_index\":146,\"name\":{\"207\":{}},\"comment\":{}}],[\"exited\",{\"_index\":80,\"name\":{\"116\":{}},\"comment\":{}}],[\"fail\",{\"_index\":145,\"name\":{\"206\":{}},\"comment\":{}}],[\"failedtaskcount\",{\"_index\":74,\"name\":{\"108\":{}},\"comment\":{}}],[\"fatalerror\",{\"_index\":132,\"name\":{\"182\":{}},\"comment\":{}}],[\"filterlevels\",{\"_index\":160,\"name\":{\"229\":{}},\"comment\":{}}],[\"fulfilled\",{\"_index\":93,\"name\":{\"132\":{}},\"comment\":{}}],[\"healthcheckcommand\",{\"_index\":143,\"name\":{\"204\":{}},\"comment\":{}}],[\"healthcheckerror\",{\"_index\":138,\"name\":{\"194\":{}},\"comment\":{}}],[\"healthcheckintervalmillis\",{\"_index\":66,\"name\":{\"98\":{}},\"comment\":{}}],[\"healthy\",{\"_index\":82,\"name\":{\"118\":{}},\"comment\":{}}],[\"idle\",{\"_index\":35,\"name\":{\"43\":{},\"67\":{},\"119\":{}},\"comment\":{}}],[\"idlems\",{\"_index\":85,\"name\":{\"122\":{}},\"comment\":{}}],[\"info\",{\"_index\":151,\"name\":{\"217\":{}},\"comment\":{}}],[\"internalerror\",{\"_index\":131,\"name\":{\"180\":{}},\"comment\":{}}],[\"internalerrorcount\",{\"_index\":21,\"name\":{\"23\":{},\"31\":{}},\"comment\":{}}],[\"isidle\",{\"_index\":12,\"name\":{\"14\":{}},\"comment\":{}}],[\"kill\",{\"_index\":104,\"name\":{\"143\":{}},\"comment\":{}}],[\"log\",{\"_index\":157,\"name\":{\"223\":{}},\"comment\":{}}],[\"logger\",{\"_index\":68,\"name\":{\"100\":{},\"213\":{},\"214\":{}},\"comment\":{}}],[\"loglevels\",{\"_index\":154,\"name\":{\"220\":{}},\"comment\":{}}],[\"maxfailedtasksperprocess\",{\"_index\":65,\"name\":{\"97\":{}},\"comment\":{}}],[\"maxidlemsperprocess\",{\"_index\":64,\"name\":{\"96\":{}},\"comment\":{}}],[\"maxprocagemillis\",{\"_index\":54,\"name\":{\"86\":{}},\"comment\":{}}],[\"maxproccount\",{\"_index\":26,\"name\":{\"30\":{}},\"comment\":{}}],[\"maxprocs\",{\"_index\":53,\"name\":{\"85\":{}},\"comment\":{}}],[\"maxreasonableprocessfailuresperminute\",{\"_index\":56,\"name\":{\"88\":{}},\"comment\":{}}],[\"maxtasksperprocess\",{\"_index\":60,\"name\":{\"92\":{}},\"comment\":{}}],[\"mayberunhealthcheck\",{\"_index\":88,\"name\":{\"125\":{}},\"comment\":{}}],[\"meantasksperproc\",{\"_index\":14,\"name\":{\"16\":{}},\"comment\":{}}],[\"mindelaybetweenspawnmillis\",{\"_index\":58,\"name\":{\"90\":{}},\"comment\":{}}],[\"msbeforenextspawn\",{\"_index\":28,\"name\":{\"33\":{}},\"comment\":{}}],[\"msperevent\",{\"_index\":112,\"name\":{\"153\":{}},\"comment\":{}}],[\"mssincelastevent\",{\"_index\":111,\"name\":{\"152\":{}},\"comment\":{}}],[\"name\",{\"_index\":70,\"name\":{\"104\":{}},\"comment\":{}}],[\"nologger\",{\"_index\":156,\"name\":{\"222\":{}},\"comment\":{}}],[\"notaskdata\",{\"_index\":137,\"name\":{\"192\":{}},\"comment\":{}}],[\"notrunning\",{\"_index\":87,\"name\":{\"124\":{}},\"comment\":{}}],[\"observe\",{\"_index\":100,\"name\":{\"139\":{}},\"comment\":{}}],[\"observequietly\",{\"_index\":101,\"name\":{\"140\":{}},\"comment\":{}}],[\"off\",{\"_index\":8,\"name\":{\"9\":{}},\"comment\":{}}],[\"old\",{\"_index\":36,\"name\":{\"44\":{},\"68\":{}},\"comment\":{}}],[\"on\",{\"_index\":7,\"name\":{\"7\":{}},\"comment\":{}}],[\"onevent\",{\"_index\":109,\"name\":{\"150\":{}},\"comment\":{}}],[\"onidleintervalmillis\",{\"_index\":55,\"name\":{\"87\":{}},\"comment\":{}}],[\"onstart\",{\"_index\":122,\"name\":{\"166\":{}},\"comment\":{}}],[\"onstderr\",{\"_index\":126,\"name\":{\"170\":{}},\"comment\":{}}],[\"onstdout\",{\"_index\":125,\"name\":{\"169\":{}},\"comment\":{}}],[\"options\",{\"_index\":5,\"name\":{\"5\":{}},\"comment\":{}}],[\"opts\",{\"_index\":76,\"name\":{\"110\":{}},\"comment\":{}}],[\"parser\",{\"_index\":120,\"name\":{\"162\":{},\"209\":{}},\"comment\":{}}],[\"pass\",{\"_index\":144,\"name\":{\"205\":{}},\"comment\":{}}],[\"pending\",{\"_index\":92,\"name\":{\"131\":{},\"164\":{}},\"comment\":{}}],[\"pendingtaskcount\",{\"_index\":13,\"name\":{\"15\":{},\"27\":{}},\"comment\":{}}],[\"pendingtasks\",{\"_index\":19,\"name\":{\"21\":{}},\"comment\":{}}],[\"periodms\",{\"_index\":107,\"name\":{\"148\":{}},\"comment\":{}}],[\"pid\",{\"_index\":71,\"name\":{\"105\":{}},\"comment\":{}}],[\"pidcheckintervalmillis\",{\"_index\":67,\"name\":{\"99\":{}},\"comment\":{}}],[\"pidexists\",{\"_index\":105,\"name\":{\"144\":{}},\"comment\":{}}],[\"pids\",{\"_index\":22,\"name\":{\"24\":{},\"145\":{}},\"comment\":{}}],[\"proc\",{\"_index\":75,\"name\":{\"109\":{}},\"comment\":{}}],[\"proc.close\",{\"_index\":37,\"name\":{\"45\":{},\"69\":{}},\"comment\":{}}],[\"proc.disconnect\",{\"_index\":38,\"name\":{\"46\":{},\"70\":{}},\"comment\":{}}],[\"proc.error\",{\"_index\":39,\"name\":{\"47\":{},\"71\":{}},\"comment\":{}}],[\"proc.exit\",{\"_index\":40,\"name\":{\"48\":{},\"72\":{}},\"comment\":{}}],[\"proccount\",{\"_index\":16,\"name\":{\"18\":{}},\"comment\":{}}],[\"processfactory\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"promise\",{\"_index\":91,\"name\":{\"130\":{},\"163\":{}},\"comment\":{}}],[\"rate\",{\"_index\":106,\"name\":{\"146\":{}},\"comment\":{}}],[\"ready\",{\"_index\":84,\"name\":{\"121\":{}},\"comment\":{}}],[\"readyproccount\",{\"_index\":25,\"name\":{\"29\":{}},\"comment\":{}}],[\"reject\",{\"_index\":99,\"name\":{\"138\":{},\"171\":{}},\"comment\":{}}],[\"rejected\",{\"_index\":94,\"name\":{\"133\":{}},\"comment\":{}}],[\"resolve\",{\"_index\":98,\"name\":{\"137\":{}},\"comment\":{}}],[\"running\",{\"_index\":86,\"name\":{\"123\":{}},\"comment\":{}}],[\"runtimems\",{\"_index\":123,\"name\":{\"167\":{}},\"comment\":{}}],[\"setlogger\",{\"_index\":148,\"name\":{\"212\":{}},\"comment\":{}}],[\"setmaxprocs\",{\"_index\":50,\"name\":{\"81\":{}},\"comment\":{}}],[\"settled\",{\"_index\":95,\"name\":{\"134\":{}},\"comment\":{}}],[\"simpleparser\",{\"_index\":103,\"name\":{\"142\":{}},\"comment\":{}}],[\"spawnedproccount\",{\"_index\":15,\"name\":{\"17\":{},\"34\":{}},\"comment\":{}}],[\"spawntimeoutmillis\",{\"_index\":57,\"name\":{\"89\":{}},\"comment\":{}}],[\"start\",{\"_index\":72,\"name\":{\"106\":{}},\"comment\":{}}],[\"starterror\",{\"_index\":30,\"name\":{\"37\":{},\"61\":{},\"178\":{}},\"comment\":{}}],[\"starterrorrateperminute\",{\"_index\":27,\"name\":{\"32\":{}},\"comment\":{}}],[\"starting\",{\"_index\":79,\"name\":{\"113\":{}},\"comment\":{}}],[\"startingproccount\",{\"_index\":18,\"name\":{\"20\":{}},\"comment\":{}}],[\"startuptaskid\",{\"_index\":73,\"name\":{\"107\":{}},\"comment\":{}}],[\"state\",{\"_index\":121,\"name\":{\"165\":{}},\"comment\":{}}],[\"stats\",{\"_index\":23,\"name\":{\"25\":{}},\"comment\":{}}],[\"stderr\",{\"_index\":42,\"name\":{\"50\":{},\"74\":{}},\"comment\":{}}],[\"stderr.error\",{\"_index\":41,\"name\":{\"49\":{},\"73\":{}},\"comment\":{}}],[\"stdin.error\",{\"_index\":43,\"name\":{\"51\":{},\"75\":{}},\"comment\":{}}],[\"stdout.error\",{\"_index\":44,\"name\":{\"52\":{},\"76\":{}},\"comment\":{}}],[\"streamflushmillis\",{\"_index\":62,\"name\":{\"94\":{}},\"comment\":{}}],[\"task\",{\"_index\":117,\"name\":{\"158\":{}},\"comment\":{}}],[\"taskcount\",{\"_index\":78,\"name\":{\"112\":{}},\"comment\":{}}],[\"taskdata\",{\"_index\":133,\"name\":{\"184\":{}},\"comment\":{}}],[\"taskerror\",{\"_index\":136,\"name\":{\"190\":{}},\"comment\":{}}],[\"taskid\",{\"_index\":118,\"name\":{\"160\":{}},\"comment\":{}}],[\"taskresolved\",{\"_index\":134,\"name\":{\"186\":{}},\"comment\":{}}],[\"tasktimeout\",{\"_index\":135,\"name\":{\"188\":{}},\"comment\":{}}],[\"tasktimeoutmillis\",{\"_index\":59,\"name\":{\"91\":{}},\"comment\":{}}],[\"then\",{\"_index\":96,\"name\":{\"135\":{}},\"comment\":{}}],[\"timeout\",{\"_index\":31,\"name\":{\"38\":{},\"62\":{}},\"comment\":{}}],[\"toomany\",{\"_index\":45,\"name\":{\"53\":{},\"77\":{}},\"comment\":{}}],[\"tostring\",{\"_index\":124,\"name\":{\"168\":{}},\"comment\":{}}],[\"tostringtag\",{\"_index\":102,\"name\":{\"141\":{}},\"comment\":{}}],[\"trace\",{\"_index\":149,\"name\":{\"215\":{}},\"comment\":{}}],[\"unhealthy\",{\"_index\":46,\"name\":{\"54\":{},\"78\":{}},\"comment\":{}}],[\"vacuumprocs\",{\"_index\":51,\"name\":{\"82\":{}},\"comment\":{}}],[\"versioncommand\",{\"_index\":142,\"name\":{\"203\":{}},\"comment\":{}}],[\"warmupms\",{\"_index\":108,\"name\":{\"149\":{}},\"comment\":{}}],[\"warn\",{\"_index\":152,\"name\":{\"218\":{}},\"comment\":{}}],[\"whynothealthy\",{\"_index\":81,\"name\":{\"117\":{},\"210\":{}},\"comment\":{}}],[\"whynotready\",{\"_index\":83,\"name\":{\"120\":{},\"211\":{}},\"comment\":{}}],[\"withlevels\",{\"_index\":158,\"name\":{\"225\":{}},\"comment\":{}}],[\"withtimestamps\",{\"_index\":159,\"name\":{\"227\":{}},\"comment\":{}}],[\"worn\",{\"_index\":47,\"name\":{\"55\":{},\"79\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/docs/assets/style.css b/docs/assets/style.css deleted file mode 100644 index 8f6ed2c..0000000 --- a/docs/assets/style.css +++ /dev/null @@ -1,1224 +0,0 @@ -:root { - /* Light */ - --light-color-background: #f2f4f8; - --light-color-background-secondary: #eff0f1; - --light-color-icon-background: var(--light-color-background); - --light-color-accent: #c5c7c9; - --light-color-text: #222; - --light-color-text-aside: #707070; - --light-color-link: #4da6ff; - --light-color-ts: #db1373; - --light-color-ts-interface: #139d2c; - --light-color-ts-enum: #9c891a; - --light-color-ts-class: #2484e5; - --light-color-ts-function: #572be7; - --light-color-ts-namespace: #b111c9; - --light-color-ts-private: #707070; - --light-color-ts-variable: #4d68ff; - --light-external-icon: url("data:image/svg+xml;utf8,"); - --light-color-scheme: light; - - /* Dark */ - --dark-color-background: #2b2e33; - --dark-color-background-secondary: #1e2024; - --dark-color-icon-background: var(--dark-color-background-secondary); - --dark-color-accent: #9096a2; - --dark-color-text: #f5f5f5; - --dark-color-text-aside: #dddddd; - --dark-color-link: #00aff4; - --dark-color-ts: #ff6492; - --dark-color-ts-interface: #6cff87; - --dark-color-ts-enum: #f4d93e; - --dark-color-ts-class: #61b0ff; - --dark-color-ts-function: #9772ff; - --dark-color-ts-namespace: #e14dff; - --dark-color-ts-private: #e2e2e2; - --dark-color-ts-variable: #4d68ff; - --dark-external-icon: url("data:image/svg+xml;utf8,"); - --dark-color-scheme: dark; -} - -@media (prefers-color-scheme: light) { - :root { - --color-background: var(--light-color-background); - --color-background-secondary: var(--light-color-background-secondary); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - --color-ts: var(--light-color-ts); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-class: var(--light-color-ts-class); - --color-ts-function: var(--light-color-ts-function); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-private: var(--light-color-ts-private); - --color-ts-variable: var(--light-color-ts-variable); - --external-icon: var(--light-external-icon); - --color-scheme: var(--light-color-scheme); - } -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--dark-color-background); - --color-background-secondary: var(--dark-color-background-secondary); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - --color-ts: var(--dark-color-ts); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-private: var(--dark-color-ts-private); - --color-ts-variable: var(--dark-color-ts-variable); - --external-icon: var(--dark-external-icon); - --color-scheme: var(--dark-color-scheme); - } -} - -html { - color-scheme: var(--color-scheme); -} - -body { - margin: 0; -} - -:root[data-theme="light"] { - --color-background: var(--light-color-background); - --color-background-secondary: var(--light-color-background-secondary); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - --color-ts: var(--light-color-ts); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-class: var(--light-color-ts-class); - --color-ts-function: var(--light-color-ts-function); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-private: var(--light-color-ts-private); - --color-ts-variable: var(--light-color-ts-variable); - --external-icon: var(--light-external-icon); - --color-scheme: var(--light-color-scheme); -} - -:root[data-theme="dark"] { - --color-background: var(--dark-color-background); - --color-background-secondary: var(--dark-color-background-secondary); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - --color-ts: var(--dark-color-ts); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-private: var(--dark-color-ts-private); - --color-ts-variable: var(--dark-color-ts-variable); - --external-icon: var(--dark-external-icon); - --color-scheme: var(--dark-color-scheme); -} - -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.2; -} - -h1 { - font-size: 1.875rem; - margin: 0.67rem 0; -} - -h2 { - font-size: 1.5rem; - margin: 0.83rem 0; -} - -h3 { - font-size: 1.25rem; - margin: 1rem 0; -} - -h4 { - font-size: 1.05rem; - margin: 1.33rem 0; -} - -h5 { - font-size: 1rem; - margin: 1.5rem 0; -} - -h6 { - font-size: 0.875rem; - margin: 2.33rem 0; -} - -.uppercase { - text-transform: uppercase; -} - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; -} - -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -.container { - max-width: 1600px; - padding: 0 2rem; -} - -@media (min-width: 640px) { - .container { - padding: 0 4rem; - } -} -@media (min-width: 1200px) { - .container { - padding: 0 8rem; - } -} -@media (min-width: 1600px) { - .container { - padding: 0 12rem; - } -} - -/* Footer */ -.tsd-generator { - border-top: 1px solid var(--color-accent); - padding-top: 1rem; - padding-bottom: 1rem; - max-height: 3.5rem; -} - -.tsd-generator > p { - margin-top: 0; - margin-bottom: 0; - padding: 0 1rem; -} - -.container-main { - display: flex; - justify-content: space-between; - position: relative; - margin: 0 auto; -} - -.col-4, -.col-8 { - box-sizing: border-box; - float: left; - padding: 2rem 1rem; -} - -.col-4 { - flex: 0 0 25%; -} -.col-8 { - flex: 1 0; - flex-wrap: wrap; - padding-left: 0; -} - -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -@keyframes fade-out { - from { - opacity: 1; - visibility: visible; - } - to { - opacity: 0; - } -} -@keyframes fade-in-delayed { - 0% { - opacity: 0; - } - 33% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@keyframes fade-out-delayed { - 0% { - opacity: 1; - visibility: visible; - } - 66% { - opacity: 0; - } - 100% { - opacity: 0; - } -} -@keyframes shift-to-left { - from { - transform: translate(0, 0); - } - to { - transform: translate(-25%, 0); - } -} -@keyframes unshift-to-left { - from { - transform: translate(-25%, 0); - } - to { - transform: translate(0, 0); - } -} -@keyframes pop-in-from-right { - from { - transform: translate(100%, 0); - } - to { - transform: translate(0, 0); - } -} -@keyframes pop-out-to-right { - from { - transform: translate(0, 0); - visibility: visible; - } - to { - transform: translate(100%, 0); - } -} -body { - background: var(--color-background); - font-family: "Segoe UI", sans-serif; - font-size: 16px; - color: var(--color-text); -} - -a { - color: var(--color-link); - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -a.external[target="_blank"] { - background-image: var(--external-icon); - background-position: top 3px right; - background-repeat: no-repeat; - padding-right: 13px; -} - -code, -pre { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - padding: 0.2em; - margin: 0; - font-size: 0.875rem; - border-radius: 0.8em; -} - -pre { - padding: 10px; - border: 0.1em solid var(--color-accent); -} -pre code { - padding: 0; - font-size: 100%; -} - -blockquote { - margin: 1em 0; - padding-left: 1em; - border-left: 4px solid gray; -} - -.tsd-typography { - line-height: 1.333em; -} -.tsd-typography ul { - list-style: square; - padding: 0 0 0 20px; - margin: 0; -} -.tsd-typography h4, -.tsd-typography .tsd-index-panel h3, -.tsd-index-panel .tsd-typography h3, -.tsd-typography h5, -.tsd-typography h6 { - font-size: 1em; - margin: 0; -} -.tsd-typography h5, -.tsd-typography h6 { - font-weight: normal; -} -.tsd-typography p, -.tsd-typography ul, -.tsd-typography ol { - margin: 1em 0; -} - -@media (max-width: 1024px) { - html .col-content { - float: none; - max-width: 100%; - width: 100%; - padding-top: 3rem; - } - html .col-menu { - position: fixed !important; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - z-index: 1024; - top: 0 !important; - bottom: 0 !important; - left: auto !important; - right: 0 !important; - padding: 1.5rem 1.5rem 0 0; - max-width: 25rem; - visibility: hidden; - background-color: var(--color-background); - transform: translate(100%, 0); - } - html .col-menu > *:last-child { - padding-bottom: 20px; - } - html .overlay { - content: ""; - display: block; - position: fixed; - z-index: 1023; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.75); - visibility: hidden; - } - - .to-has-menu .overlay { - animation: fade-in 0.4s; - } - - .to-has-menu :is(header, footer, .col-content) { - animation: shift-to-left 0.4s; - } - - .to-has-menu .col-menu { - animation: pop-in-from-right 0.4s; - } - - .from-has-menu .overlay { - animation: fade-out 0.4s; - } - - .from-has-menu :is(header, footer, .col-content) { - animation: unshift-to-left 0.4s; - } - - .from-has-menu .col-menu { - animation: pop-out-to-right 0.4s; - } - - .has-menu body { - overflow: hidden; - } - .has-menu .overlay { - visibility: visible; - } - .has-menu :is(header, footer, .col-content) { - transform: translate(-25%, 0); - } - .has-menu .col-menu { - visibility: visible; - transform: translate(0, 0); - display: grid; - align-items: center; - grid-template-rows: auto 1fr; - grid-gap: 1.5rem; - max-height: 100vh; - padding: 1rem 2rem; - } - .has-menu .tsd-navigation { - max-height: 100%; - } -} - -.tsd-breadcrumb { - margin: 0; - padding: 0; - color: var(--color-text-aside); -} -.tsd-breadcrumb a { - color: var(--color-text-aside); - text-decoration: none; -} -.tsd-breadcrumb a:hover { - text-decoration: underline; -} -.tsd-breadcrumb li { - display: inline; -} -.tsd-breadcrumb li:after { - content: " / "; -} - -.tsd-comment-tags { - display: flex; - flex-direction: column; -} -dl.tsd-comment-tag-group { - display: flex; - align-items: center; - overflow: hidden; - margin: 0.5em 0; -} -dl.tsd-comment-tag-group dt { - display: flex; - margin-right: 0.5em; - font-size: 0.875em; - font-weight: normal; -} -dl.tsd-comment-tag-group dd { - margin: 0; -} -code.tsd-tag { - padding: 0.25em 0.4em; - border: 0.1em solid var(--color-accent); - margin-right: 0.25em; - font-size: 70%; -} -h1 code.tsd-tag:first-of-type { - margin-left: 0.25em; -} - -dl.tsd-comment-tag-group dd:before, -dl.tsd-comment-tag-group dd:after { - content: " "; -} -dl.tsd-comment-tag-group dd pre, -dl.tsd-comment-tag-group dd:after { - clear: both; -} -dl.tsd-comment-tag-group p { - margin: 0; -} - -.tsd-panel.tsd-comment .lead { - font-size: 1.1em; - line-height: 1.333em; - margin-bottom: 2em; -} -.tsd-panel.tsd-comment .lead:last-child { - margin-bottom: 0; -} - -.tsd-filter-visibility h4 { - font-size: 1rem; - padding-top: 0.75rem; - padding-bottom: 0.5rem; - margin: 0; -} -.tsd-filter-item:not(:last-child) { - margin-bottom: 0.5rem; -} -.tsd-filter-input { - display: flex; - width: fit-content; - width: -moz-fit-content; - align-items: center; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - cursor: pointer; -} -.tsd-filter-input input[type="checkbox"] { - cursor: pointer; - position: absolute; - width: 1.5em; - height: 1.5em; - opacity: 0; -} -.tsd-filter-input input[type="checkbox"]:disabled { - pointer-events: none; -} -.tsd-filter-input svg { - cursor: pointer; - width: 1.5em; - height: 1.5em; - margin-right: 0.5em; - border-radius: 0.33em; - /* Leaving this at full opacity breaks event listeners on Firefox. - Don't remove unless you know what you're doing. */ - opacity: 0.99; -} -.tsd-filter-input input[type="checkbox"]:focus + svg { - transform: scale(0.95); -} -.tsd-filter-input input[type="checkbox"]:focus:not(:focus-visible) + svg { - transform: scale(1); -} -.tsd-checkbox-background { - fill: var(--color-accent); -} -input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { - stroke: var(--color-text); -} -.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { - fill: var(--color-background); - stroke: var(--color-accent); - stroke-width: 0.25rem; -} -.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { - stroke: var(--color-accent); -} - -.tsd-theme-toggle { - padding-top: 0.75rem; -} -.tsd-theme-toggle > h4 { - display: inline; - vertical-align: middle; - margin-right: 0.75rem; -} - -.tsd-hierarchy { - list-style: square; - margin: 0; -} -.tsd-hierarchy .target { - font-weight: bold; -} - -.tsd-panel-group.tsd-index-group { - margin-bottom: 0; -} -.tsd-index-panel .tsd-index-list { - list-style: none; - line-height: 1.333em; - margin: 0; - padding: 0.25rem 0 0 0; - overflow: hidden; - display: grid; - grid-template-columns: repeat(3, 1fr); - column-gap: 1rem; - grid-template-rows: auto; -} -@media (max-width: 1024px) { - .tsd-index-panel .tsd-index-list { - grid-template-columns: repeat(2, 1fr); - } -} -@media (max-width: 768px) { - .tsd-index-panel .tsd-index-list { - grid-template-columns: repeat(1, 1fr); - } -} -.tsd-index-panel .tsd-index-list li { - -webkit-page-break-inside: avoid; - -moz-page-break-inside: avoid; - -ms-page-break-inside: avoid; - -o-page-break-inside: avoid; - page-break-inside: avoid; -} -.tsd-index-panel a, -.tsd-index-panel a.tsd-parent-kind-module { - color: var(--color-ts); -} -.tsd-index-panel a.tsd-parent-kind-interface { - color: var(--color-ts-interface); -} -.tsd-index-panel a.tsd-parent-kind-enum { - color: var(--color-ts-enum); -} -.tsd-index-panel a.tsd-parent-kind-class { - color: var(--color-ts-class); -} -.tsd-index-panel a.tsd-kind-module { - color: var(--color-ts-namespace); -} -.tsd-index-panel a.tsd-kind-interface { - color: var(--color-ts-interface); -} -.tsd-index-panel a.tsd-kind-enum { - color: var(--color-ts-enum); -} -.tsd-index-panel a.tsd-kind-class { - color: var(--color-ts-class); -} -.tsd-index-panel a.tsd-kind-function { - color: var(--color-ts-function); -} -.tsd-index-panel a.tsd-kind-namespace { - color: var(--color-ts-namespace); -} -.tsd-index-panel a.tsd-kind-variable { - color: var(--color-ts-variable); -} -.tsd-index-panel a.tsd-is-private { - color: var(--color-ts-private); -} - -.tsd-flag { - display: inline-block; - padding: 0.25em 0.4em; - border-radius: 4px; - color: var(--color-comment-tag-text); - background-color: var(--color-comment-tag); - text-indent: 0; - font-size: 75%; - line-height: 1; - font-weight: normal; -} - -.tsd-anchor { - position: absolute; - top: -100px; -} - -.tsd-member { - position: relative; -} -.tsd-member .tsd-anchor + h3 { - display: flex; - align-items: center; - margin-top: 0; - margin-bottom: 0; - border-bottom: none; -} -.tsd-member [data-tsd-kind] { - color: var(--color-ts); -} -.tsd-member [data-tsd-kind="Interface"] { - color: var(--color-ts-interface); -} -.tsd-member [data-tsd-kind="Enum"] { - color: var(--color-ts-enum); -} -.tsd-member [data-tsd-kind="Class"] { - color: var(--color-ts-class); -} -.tsd-member [data-tsd-kind="Private"] { - color: var(--color-ts-private); -} - -.tsd-navigation a { - display: block; - margin: 0.4rem 0; - border-left: 2px solid transparent; - color: var(--color-text); - text-decoration: none; - transition: border-left-color 0.1s; -} -.tsd-navigation a:hover { - text-decoration: underline; -} -.tsd-navigation ul { - margin: 0; - padding: 0; - list-style: none; -} -.tsd-navigation li { - padding: 0; -} - -.tsd-navigation.primary .tsd-accordion-details > ul { - margin-top: 0.75rem; -} -.tsd-navigation.primary a { - padding: 0.75rem 0.5rem; - margin: 0; -} -.tsd-navigation.primary ul li a { - margin-left: 0.5rem; -} -.tsd-navigation.primary ul li li a { - margin-left: 1.5rem; -} -.tsd-navigation.primary ul li li li a { - margin-left: 2.5rem; -} -.tsd-navigation.primary ul li li li li a { - margin-left: 3.5rem; -} -.tsd-navigation.primary ul li li li li li a { - margin-left: 4.5rem; -} -.tsd-navigation.primary ul li li li li li li a { - margin-left: 5.5rem; -} -.tsd-navigation.primary li.current > a { - border-left: 0.15rem var(--color-text) solid; -} -.tsd-navigation.primary li.selected > a { - font-weight: bold; - border-left: 0.2rem var(--color-text) solid; -} -.tsd-navigation.primary ul li a:hover { - border-left: 0.2rem var(--color-text-aside) solid; -} -.tsd-navigation.primary li.globals + li > span, -.tsd-navigation.primary li.globals + li > a { - padding-top: 20px; -} - -.tsd-navigation.secondary.tsd-navigation--toolbar-hide { - max-height: calc(100vh - 1rem); - top: 0.5rem; -} -.tsd-navigation.secondary > ul { - display: inline; - padding-right: 0.5rem; - transition: opacity 0.2s; -} -.tsd-navigation.secondary ul li a { - padding-left: 0; -} -.tsd-navigation.secondary ul li li a { - padding-left: 1.1rem; -} -.tsd-navigation.secondary ul li li li a { - padding-left: 2.2rem; -} -.tsd-navigation.secondary ul li li li li a { - padding-left: 3.3rem; -} -.tsd-navigation.secondary ul li li li li li a { - padding-left: 4.4rem; -} -.tsd-navigation.secondary ul li li li li li li a { - padding-left: 5.5rem; -} - -a.tsd-index-link { - margin: 0.25rem 0; - font-size: 1rem; - line-height: 1.25rem; - display: inline-flex; - align-items: center; -} -.tsd-accordion-summary > h1, -.tsd-accordion-summary > h2, -.tsd-accordion-summary > h3, -.tsd-accordion-summary > h4, -.tsd-accordion-summary > h5 { - display: inline-flex; - align-items: center; - vertical-align: middle; - margin-bottom: 0; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} -.tsd-accordion-summary { - display: block; - cursor: pointer; -} -.tsd-accordion-summary > * { - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; -} -.tsd-accordion-summary::-webkit-details-marker { - display: none; -} -.tsd-index-accordion .tsd-accordion-summary svg { - margin-right: 0.25rem; -} -.tsd-index-content > :not(:first-child) { - margin-top: 0.75rem; -} -.tsd-index-heading { - margin-top: 1.5rem; - margin-bottom: 0.75rem; -} - -.tsd-kind-icon { - margin-right: 0.5rem; - width: 1.25rem; - height: 1.25rem; - min-width: 1.25rem; - min-height: 1.25rem; -} -.tsd-kind-icon path { - transform-origin: center; - transform: scale(1.1); -} -.tsd-signature > .tsd-kind-icon { - margin-right: 0.8rem; -} - -@media (min-width: 1024px) { - .col-content { - margin: 2rem auto; - } - - .menu-sticky-wrap { - position: sticky; - height: calc(100vh - 2rem); - top: 4rem; - right: 0; - padding: 0 1.5rem; - padding-top: 1rem; - margin-top: 3rem; - transition: 0.3s ease-in-out; - transition-property: top, padding-top, padding, height; - overflow-y: auto; - } - .col-menu { - border-left: 1px solid var(--color-accent); - } - .col-menu--hide { - top: 1rem; - } - .col-menu .tsd-navigation:not(:last-child) { - padding-bottom: 1.75rem; - } -} - -.tsd-panel { - margin-bottom: 2.5rem; -} -.tsd-panel.tsd-member { - margin-bottom: 4rem; -} -.tsd-panel:empty { - display: none; -} -.tsd-panel > h1, -.tsd-panel > h2, -.tsd-panel > h3 { - margin: 1.5rem -1.5rem 0.75rem -1.5rem; - padding: 0 1.5rem 0.75rem 1.5rem; -} -.tsd-panel > h1.tsd-before-signature, -.tsd-panel > h2.tsd-before-signature, -.tsd-panel > h3.tsd-before-signature { - margin-bottom: 0; - border-bottom: none; -} - -.tsd-panel-group { - margin: 4rem 0; -} -.tsd-panel-group.tsd-index-group { - margin: 2rem 0; -} -.tsd-panel-group.tsd-index-group details { - margin: 2rem 0; -} - -#tsd-search { - transition: background-color 0.2s; -} -#tsd-search .title { - position: relative; - z-index: 2; -} -#tsd-search .field { - position: absolute; - left: 0; - top: 0; - right: 2.5rem; - height: 100%; -} -#tsd-search .field input { - box-sizing: border-box; - position: relative; - top: -50px; - z-index: 1; - width: 100%; - padding: 0 10px; - opacity: 0; - outline: 0; - border: 0; - background: transparent; - color: var(--color-text); -} -#tsd-search .field label { - position: absolute; - overflow: hidden; - right: -40px; -} -#tsd-search .field input, -#tsd-search .title { - transition: opacity 0.2s; -} -#tsd-search .results { - position: absolute; - visibility: hidden; - top: 40px; - width: 100%; - margin: 0; - padding: 0; - list-style: none; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); -} -#tsd-search .results li { - padding: 0 10px; - background-color: var(--color-background); -} -#tsd-search .results li:nth-child(even) { - background-color: var(--color-background-secondary); -} -#tsd-search .results li.state { - display: none; -} -#tsd-search .results li.current, -#tsd-search .results li:hover { - background-color: var(--color-accent); -} -#tsd-search .results a { - display: block; -} -#tsd-search .results a:before { - top: 10px; -} -#tsd-search .results span.parent { - color: var(--color-text-aside); - font-weight: normal; -} -#tsd-search.has-focus { - background-color: var(--color-accent); -} -#tsd-search.has-focus .field input { - top: 0; - opacity: 1; -} -#tsd-search.has-focus .title { - z-index: 0; - opacity: 0; -} -#tsd-search.has-focus .results { - visibility: visible; -} -#tsd-search.loading .results li.state.loading { - display: block; -} -#tsd-search.failure .results li.state.failure { - display: block; -} - -.tsd-signature { - margin: 0 0 1rem 0; - padding: 1rem 0.5rem; - border: 1px solid var(--color-accent); - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: 14px; - overflow-x: auto; -} - -.tsd-signature-symbol { - color: var(--color-text-aside); - font-weight: normal; -} - -.tsd-signature-type { - font-style: italic; - font-weight: normal; -} - -.tsd-signatures { - padding: 0; - margin: 0 0 1em 0; - list-style-type: none; -} -.tsd-signatures .tsd-signature { - margin: 0; - border-color: var(--color-accent); - border-width: 1px 0; - transition: background-color 0.1s; -} -.tsd-description .tsd-signatures .tsd-signature { - border-width: 1px; -} - -ul.tsd-parameter-list, -ul.tsd-type-parameter-list { - list-style: square; - margin: 0; - padding-left: 20px; -} -ul.tsd-parameter-list > li.tsd-parameter-signature, -ul.tsd-type-parameter-list > li.tsd-parameter-signature { - list-style: none; - margin-left: -20px; -} -ul.tsd-parameter-list h5, -ul.tsd-type-parameter-list h5 { - font-size: 16px; - margin: 1em 0 0.5em 0; -} -.tsd-sources { - margin-top: 1rem; - font-size: 0.875em; -} -.tsd-sources a { - color: var(--color-text-aside); - text-decoration: underline; -} -.tsd-sources ul { - list-style: none; - padding: 0; -} - -.tsd-page-toolbar { - position: fixed; - z-index: 1; - top: 0; - left: 0; - width: 100%; - color: var(--color-text); - background: var(--color-background-secondary); - border-bottom: 1px var(--color-accent) solid; - transition: transform 0.3s ease-in-out; -} -.tsd-page-toolbar a { - color: var(--color-text); - text-decoration: none; -} -.tsd-page-toolbar a.title { - font-weight: bold; -} -.tsd-page-toolbar a.title:hover { - text-decoration: underline; -} -.tsd-page-toolbar .tsd-toolbar-contents { - display: flex; - justify-content: space-between; - height: 2.5rem; -} -.tsd-page-toolbar .table-cell { - position: relative; - white-space: nowrap; - line-height: 40px; -} -.tsd-page-toolbar .table-cell:first-child { - width: 100%; -} - -.tsd-page-toolbar--hide { - transform: translateY(-100%); -} - -.tsd-widget { - display: inline-block; - overflow: hidden; - opacity: 0.8; - height: 40px; - transition: opacity 0.1s, background-color 0.2s; - vertical-align: bottom; - cursor: pointer; -} -.tsd-widget:hover { - opacity: 0.9; -} -.tsd-widget.active { - opacity: 1; - background-color: var(--color-accent); -} -.tsd-widget.no-caption { - width: 40px; -} -.tsd-widget.no-caption:before { - margin: 0; -} - -.tsd-widget.options, -.tsd-widget.menu { - display: none; -} -@media (max-width: 1024px) { - .tsd-widget.options, - .tsd-widget.menu { - display: inline-block; - } -} -input[type="checkbox"] + .tsd-widget:before { - background-position: -120px 0; -} -input[type="checkbox"]:checked + .tsd-widget:before { - background-position: -160px 0; -} - -img { - max-width: 100%; -} - -.tsd-anchor-icon { - display: inline-flex; - align-items: center; - margin-left: 0.5rem; - vertical-align: middle; - color: var(--color-text); -} - -.tsd-anchor-icon svg { - width: 1em; - height: 1em; - visibility: hidden; -} - -.tsd-anchor-link:hover > .tsd-anchor-icon svg { - visibility: visible; -} - -.deprecated { - text-decoration: line-through; -} - -* { - scrollbar-width: thin; - scrollbar-color: var(--color-accent) var(--color-icon-background); -} - -*::-webkit-scrollbar { - width: 0.75rem; -} - -*::-webkit-scrollbar-track { - background: var(--color-icon-background); -} - -*::-webkit-scrollbar-thumb { - background-color: var(--color-accent); - border-radius: 999rem; - border: 0.25rem solid var(--color-icon-background); -} diff --git a/docs/assets/widgets.png b/docs/assets/widgets.png deleted file mode 100644 index c738053..0000000 Binary files a/docs/assets/widgets.png and /dev/null differ diff --git a/docs/assets/widgets@2x.png b/docs/assets/widgets@2x.png deleted file mode 100644 index 4bbbd57..0000000 Binary files a/docs/assets/widgets@2x.png and /dev/null differ diff --git a/docs/classes/BatchCluster.html b/docs/classes/BatchCluster.html deleted file mode 100644 index bcfe595..0000000 --- a/docs/classes/BatchCluster.html +++ /dev/null @@ -1,581 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Class BatchCluster

-
-

BatchCluster instances manage 0 or more homogeneous child processes, and -provide the main interface for enqueuing Tasks via enqueueTask.

-

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, and BatchProcessOptions, which specifies how -child tasks can be verified and shut down.

-
-
-

Hierarchy

-
    -
  • BatchCluster
-
-
-
- -
-
-

Constructors

-
- -
-
-

Properties

-
- -
emitter: BatchClusterEmitter = ...
-
- -
off: (<E>(eventName: E, listener: ((...args: Args<BatchClusterEvents[E]>) => void)) => BatchClusterEmitter) = ...
-
-

Type declaration

-
-
- -
on: (<E>(eventName: E, listener: ((...args: Args<BatchClusterEvents[E]>) => void)) => BatchClusterEmitter) = ...
-
-

Type declaration

-
-
- -
options: AllOpts
-
-

Accessors

-
- -
    -
  • get busyProcCount(): number
  • -
  • -
    -

    Returns

    the current number of child processes currently servicing tasks

    -
    -

    Returns number

-
- -
    -
  • get 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 }
  • -
  • -

    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
    • -
    • -
      startError: number
    • -
    • -
      stderr: number
    • -
    • -
      stderr.error: number
    • -
    • -
      stdin.error: number
    • -
    • -
      stdout.error: number
    • -
    • -
      timeout: number
    • -
    • -
      tooMany: number
    • -
    • -
      unhealthy: number
    • -
    • -
      worn: number
-
- -
    -
  • get currentTasks(): Task<any>[]
  • -
  • -
    -

    Returns

    the current running Tasks (mostly for testing)

    -
    -

    Returns Task<any>[]

-
- -
-
- -
    -
  • get internalErrorCount(): number
  • -
  • -

    For integration tests:

    -
    -

    Returns number

-
- -
    -
  • get isIdle(): boolean
  • -
  • -
    -

    Returns

    true if all previously-enqueued tasks have settled

    -
    -

    Returns boolean

-
- -
    -
  • get meanTasksPerProc(): number
  • -
  • -
    -

    Returns

    the mean number of tasks completed by child processes

    -
    -

    Returns number

-
- -
    -
  • get pendingTaskCount(): number
  • -
  • -
    -

    Returns

    the number of pending tasks

    -
    -

    Returns number

-
- -
    -
  • get pendingTasks(): Task<any>[]
  • -
  • -
    -

    Returns

    the current pending Tasks (mostly for testing)

    -
    -

    Returns Task<any>[]

-
- -
    -
  • get procCount(): number
  • -
  • -
    -

    Returns

    the current number of spawned child processes. Some (or all) may be idle.

    -
    -

    Returns number

-
- -
    -
  • get spawnedProcCount(): number
  • -
  • -
    -

    Returns

    the total number of child processes created by this instance

    -
    -

    Returns number

-
- -
-
-

Methods

-
- -
    - -
  • -

    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>

-
- -
-
- -
    - -
  • -

    Shut down this instance, and all child processes.

    -
    -
    -

    Parameters

    -
      -
    • -
      gracefully: boolean = true
      -

      should an attempt be made to finish in-flight tasks, or -should we force-kill child PIDs.

      -
    -

    Returns Deferred<void>

-
- -
    - -
  • -

    Submits task for processing by a BatchProcess instance

    - -

    Returns

    a Promise that is resolved or rejected once the task has been -attempted on an idle BatchProcess

    -
    -
    -

    Type Parameters

    -
      -
    • -

      T

    -
    -

    Parameters

    -
    -

    Returns Promise<T>

-
- -
    - -
  • -

    Verify that each BatchProcess PID is actually alive.

    - -

    Returns

    the spawned PIDs that are still in the process table.

    -
    -

    Returns Promise<number[]>

-
- -
    - -
  • -

    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

-
- -
    - -
  • -

    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 }

    -
      -
    • -
      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 }
      -
        -
      • -
        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
-
- -
    - -
  • -

    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[]>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/classes/BatchClusterOptions.html b/docs/classes/BatchClusterOptions.html deleted file mode 100644 index 8778cff..0000000 --- a/docs/classes/BatchClusterOptions.html +++ /dev/null @@ -1,287 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Class BatchClusterOptions

-
-

These parameter values have somewhat sensible defaults, but can be -overridden for a given BatchCluster.

-
-
-

Hierarchy

-
    -
  • BatchClusterOptions
-
-
-
- -
-
-

Constructors

-
- -
-
-

Properties

-
- -
cleanupChildProcs: boolean = true
-

Should batch-cluster try to clean up after spawned processes that don't -shut down?

-

Only disable this if you have another means of PID cleanup.

-

Defaults to true.

-
-
- -
endGracefulWaitTimeMillis: number = 500
-

When this.end() is called, or Node broadcasts the beforeExit event, -this is the milliseconds spent waiting for currently running tasks to -finish before sending kill signals to child processes.

-

Setting this value to 0 means child processes will immediately receive a -kill signal to shut down. Any pending requests may be interrupted. Must be ->= 0. Defaults to 500ms.

-
-
- -
healthCheckIntervalMillis: number = 0
-

If healthCheckCommand is set, how frequently should we check for -unhealthy child processes?

-

Set this to 0 to disable this feature.

-
-
- -
logger: (() => Logger) = logger
-
-

Type declaration

-
    -
  • -
      -
    • (): Logger
    • -
    • -

      A BatchCluster instance and associated BatchProcess instances will share -this Logger. Defaults to the Logger instance provided to setLogger().

      -
      -

      Returns Logger

-
- -
maxFailedTasksPerProcess: number = 2
-

How many failed tasks should a process be allowed to process before it is -recycled?

-

Set this to 0 to disable this feature.

-
-
- -
maxIdleMsPerProcess: number = 0
-

If a child process is idle for more than this value (in milliseconds), shut -it down to reduce system resource consumption.

-

A value of ~10 seconds to a couple minutes would be reasonable. Set this to -0 to disable this feature.

-
-
- -
maxProcAgeMillis: number = ...
-

Child processes will be recycled when they reach this age.

-

If this value is set to 0, child processes will not "age out".

-

This value must not be less than spawnTimeoutMillis or -taskTimeoutMillis.

-

Defaults to 5 minutes.

-
-
- -
maxProcs: number = 1
-

No more than maxProcs child processes will be run at a given time -to serve pending tasks.

-

Defaults to 1.

-
-
- -
maxReasonableProcessFailuresPerMinute: number = 10
-

If the initial versionCommand fails for new spawned processes more -than this rate, end this BatchCluster and throw an error, because -something is terribly wrong.

-

If this backstop didn't exist, new (failing) child processes would be -created indefinitely.

-

Set to 0 to disable. Defaults to 10.

-
-
- -
maxTasksPerProcess: number = 500
-

Processes will be recycled after processing maxTasksPerProcess tasks. -Depending on the commands and platform, batch mode commands shouldn't -exhibit unduly memory leaks for at least tens if not hundreds of tasks. -Setting this to a low number (like less than 10) will impact performance -markedly, due to OS process start/stop maintenance. Setting this to a very -high number (> 1000) may result in more memory being consumed than -necessary.

-

Must be >= 0. Defaults to 500

-
-
- -
minDelayBetweenSpawnMillis: number = ...
-

If maxProcs > 1, spawning new child processes to process tasks can slow -down initial processing, and create unnecessary processes.

-

Must be >= 0ms. Defaults to 1.5 seconds.

-
-
- -
onIdleIntervalMillis: number = ...
-

This is the minimum interval between calls to BatchCluster.#onIdle, -which runs general janitorial processes like child process management and -task queue validation.

-

Must be > 0. Defaults to 10 seconds.

-
-
- -
pidCheckIntervalMillis: number = ...
-

Verify child processes are still running by checking the OS process table.

-

Set this to 0 to disable this feature.

-
-
- -
spawnTimeoutMillis: number = ...
-

Spawning new child processes and servicing a "version" task must not take -longer than spawnTimeoutMillis before the process is considered failed, -and need to be restarted. Be pessimistic here--windows can regularly take -several seconds to spin up a process, thanks to antivirus shenanigans.

-

Must be >= 100ms. Defaults to 15 seconds.

-
-
- -
streamFlushMillis: number = ...
-

When a task sees a "pass" or "fail" from either stdout or stderr, it needs -to wait for the other stream to finish flushing to ensure the task's Parser -sees the entire relevant stream contents. A larger number may be required -for slower computers to prevent internal errors due to lack of stream -coercion.

-

Note that this puts a hard lower limit on task latency, so don't set this -to a large number: no task will resolve faster than this value (in millis).

-

If you set this value too low, tasks may be erroneously resolved or -rejected (depending on which stream is handled first).

-

Your system may support a smaller value: this is a pessimistic default. If -this is set too low, you'll see noTaskData events.

-

Setting this to 0 makes whatever flushes first--stdout and stderr--and will -most likely result in internal errors (due to stream buffers not being able -to be associated to tasks that were just settled)

-
-
- -
taskTimeoutMillis: number = ...
-

If commands take longer than this, presume the underlying process is dead -and we should fail the task.

-

This should be set to something on the order of seconds.

-

Must be >= 10ms. Defaults to 10 seconds.

-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/classes/BatchProcess.html b/docs/classes/BatchProcess.html deleted file mode 100644 index 0bc6796..0000000 --- a/docs/classes/BatchProcess.html +++ /dev/null @@ -1,386 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Class BatchProcess

-
-

BatchProcess manages the care and feeding of a single child process.

-
-
-

Hierarchy

-
    -
  • BatchProcess
-
-
-
- -
-
-

Constructors

-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      proc: ChildProcess
    • -
    • -
      opts: InternalBatchProcessOptions
    • -
    • -
      onIdle: (() => void)
      -

      to be called when internal state changes (like the current -task is resolved, or the process exits)

      -
      -
        -
      • -
          -
        • (): void
        • -
        • -

          Returns void

    -

    Returns BatchProcess

-
-

Properties

-
- -
failedTaskCount: number = 0
-
- -
name: string
-
- -
opts: InternalBatchProcessOptions
-
- -
pid: number
-
- -
proc: ChildProcess
-
- -
start: number = ...
-
- -
startupTaskId: number
-
-

Accessors

-
- -
-
- -
    -
  • get ended(): boolean
  • -
  • -
    -

    Returns

    true if this.end() has completed running, which includes child -process cleanup. Note that this may return true and the process table may -still include the child pid. Call .running() for an authoritative -(but expensive!) answer.

    -
    -

    Returns boolean

-
- -
    -
  • get ending(): boolean
  • -
  • -
    -

    Returns

    true if this.end() has been requested (which may be due to the -child process exiting)

    -
    -

    Returns boolean

-
- -
    -
  • get exited(): boolean
  • -
  • -
    -

    Returns

    true if the child process has exited and is no longer in the -process table. Note that this may be erroneously false if the process table -hasn't been checked. Call .running() for an authoritative (but -expensive!) answer.

    -
    -

    Returns boolean

-
- -
    -
  • get healthy(): boolean
  • -
  • -
    -

    Returns

    true if the process doesn't need to be recycled.

    -
    -

    Returns boolean

-
- -
    -
  • get idle(): boolean
  • -
  • -
    -

    Returns

    true iff no current task. Does not take into consideration if the -process has ended or should be recycled: see ready.

    -
    -

    Returns boolean

-
- -
-
- -
    -
  • get ready(): boolean
  • -
  • -
    -

    Returns

    true iff this process is both healthy and idle, and ready for a -new task.

    -
    -

    Returns boolean

-
- -
-
- -
-
- -
    -
  • get whyNotHealthy(): null | WhyNotHealthy
  • -
  • -
    -

    Returns

    a string describing why this process should be recycled, or null if -the process passes all health checks. Note that this doesn't include if -we're already busy: see whyNotReady if you need to -know if a process can handle a new task.

    -
    -

    Returns null | WhyNotHealthy

-
- -
    -
  • get whyNotReady(): null | WhyNotReady
  • -
  • -
    -

    Returns

    a string describing why this process cannot currently handle a new -task, or undefined if this process is idle and healthy.

    -
    -

    Returns null | WhyNotReady

-
-

Methods

-
- -
    - -
  • -

    End this child process.

    - -

    Returns

    Promise that will be resolved when the process has completed. -Subsequent calls to end() will ignore the parameters and return the first -endPromise.

    -
    -
    -

    Parameters

    -
      -
    • -
      gracefully: boolean = true
      -

      Wait for any current task to be resolved or rejected -before shutting down the child process.

      -
    • -
    • -
      reason: WhyNotHealthy
      -

      who called end() (used for logging)

      -
    -

    Returns Promise<void>

-
- -
-
- -
-
- -
-
- -
    - -
  • -
    -

    Returns

    true if the child process is in the process table

    -
    -

    Returns Promise<boolean>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/classes/Deferred.html b/docs/classes/Deferred.html deleted file mode 100644 index bc60328..0000000 --- a/docs/classes/Deferred.html +++ /dev/null @@ -1,273 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Class Deferred<T>

-
-

Enables a Promise to be resolved or rejected at a future time, outside of -the context of the Promise construction. Also exposes the pending, -fulfilled, or rejected state of the promise.

-
-
-

Type Parameters

-
    -
  • -

    T

-
-

Hierarchy

-
    -
  • Deferred
-
-

Implements

-
    -
  • PromiseLike<T>
-
-
-
- -
-
-

Constructors

-
-
-

Properties

-
-
-

Accessors

-
-
-

Methods

-
-
-

Constructors

-
- -
-
-

Properties

-
- -
[toStringTag]: "Deferred" = "Deferred"
-
- -
promise: Promise<T>
-
-

Accessors

-
- -
    -
  • get fulfilled(): boolean
  • -
  • -
    -

    Returns

    true iff resolve has been invoked

    -
    -

    Returns boolean

-
- -
    -
  • get pending(): boolean
  • -
  • -
    -

    Returns

    true iff neither resolve nor rejected have been invoked

    -
    -

    Returns boolean

-
- -
    -
  • get rejected(): boolean
  • -
  • -
    -

    Returns

    true iff rejected has been invoked

    -
    -

    Returns boolean

-
- -
    -
  • get settled(): boolean
  • -
  • -
    -

    Returns

    true iff resolve or rejected have been invoked

    -
    -

    Returns boolean

-
-

Methods

-
- -
    - -
  • -
    -

    Type Parameters

    -
      -
    • -

      TResult = never

    -
    -

    Parameters

    -
      -
    • -
      Optional onrejected: null | ((reason: any) => TResult | PromiseLike<TResult>)
    -

    Returns Promise<T | TResult>

-
- -
-
- -
-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      Optional reason: string | Error
    -

    Returns boolean

-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      value: T
    -

    Returns boolean

-
- -
    - -
  • -
    -

    Type Parameters

    -
      -
    • -

      TResult1 = T

    • -
    • -

      TResult2 = never

    -
    -

    Parameters

    -
      -
    • -
      Optional onfulfilled: null | ((value: T) => TResult1 | PromiseLike<TResult1>)
    • -
    • -
      Optional onrejected: null | ((reason: any) => TResult2 | PromiseLike<TResult2>)
    -

    Returns Promise<TResult1 | TResult2>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/classes/Rate.html b/docs/classes/Rate.html deleted file mode 100644 index 7e35571..0000000 --- a/docs/classes/Rate.html +++ /dev/null @@ -1,190 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Class Rate

-
-

Hierarchy

-
    -
  • Rate
-
-
-
- -
-
-

Constructors

-
-
-

Properties

-
-
-

Accessors

-
-
-

Methods

-
-
-

Constructors

-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      periodMs: number = minuteMs
      -

      the length of time to retain event timestamps for computing -rate. Events older than this value will be discarded.

      -
    • -
    • -
      warmupMs: number = secondMs
      -

      return null from #msPerEvent if it's been less -than warmupMs since construction or #clear.

      -
    -

    Returns Rate

-
-

Properties

-
- -
periodMs: number = minuteMs
-
- -
warmupMs: number = secondMs
-
-

Accessors

-
- -
    -
  • get eventCount(): number
  • -
  • -

    Returns number

-
- -
    -
  • get eventsPerMinute(): number
  • -
  • -

    Returns number

-
- -
    -
  • get eventsPerMs(): number
  • -
  • -

    Returns number

-
- -
    -
  • get eventsPerSecond(): number
  • -
  • -

    Returns number

-
- -
    -
  • get msPerEvent(): null | number
  • -
  • -

    Returns null | number

-
- -
    -
  • get msSinceLastEvent(): null | number
  • -
  • -

    Returns null | number

-
-

Methods

-
- -
-
- -
    - -
  • -

    Returns void

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/classes/Task.html b/docs/classes/Task.html deleted file mode 100644 index 87a8faa..0000000 --- a/docs/classes/Task.html +++ /dev/null @@ -1,248 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Class Task<T>

-
-

Tasks embody individual jobs given to the underlying child processes. Each -instance has a promise that will be resolved or rejected based on the -result of the task.

-
-
-

Type Parameters

-
    -
  • -

    T = any

-
-

Hierarchy

-
    -
  • Task
-
-
-
- -
-
-

Constructors

-
-
-

Properties

-
-
-

Accessors

-
-
-

Methods

-
-
-

Constructors

-
- -
    - -
  • -
    -

    Type Parameters

    -
      -
    • -

      T = any

    -
    -

    Parameters

    -
      -
    • -
      command: string
      -

      is the value written to stdin to perform the given -task.

      -
    • -
    • -
      parser: Parser<T>
      -

      is used to parse resulting data from the -underlying process to a typed object.

      -
    -

    Returns Task<T>

-
-

Properties

-
- -
command: string
-
- -
parser: Parser<T>
-
- -
taskId: number = ...
-
-

Accessors

-
- -
    -
  • get pending(): boolean
  • -
  • -

    Returns boolean

-
- -
    -
  • get promise(): Promise<T>
  • -
  • -
    -

    Returns

    the resolution or rejection of this task.

    -
    -

    Returns Promise<T>

-
- -
    -
  • get runtimeMs(): undefined | number
  • -
  • -

    Returns undefined | number

-
- -
    -
  • get state(): string
  • -
  • -

    Returns string

-
-

Methods

-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      opts: TaskOptions
    -

    Returns void

-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      buf: string | Buffer
    -

    Returns void

-
- -
    - -
  • -
    -

    Parameters

    -
      -
    • -
      buf: string | Buffer
    -

    Returns void

-
- -
    - -
  • -
    -

    Returns

    true if the wrapped promise was rejected

    -
    -
    -

    Parameters

    -
      -
    • -
      error: Error
    -

    Returns boolean

-
- -
    - -
  • -

    Returns string

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/functions/SimpleParser.html b/docs/functions/SimpleParser.html deleted file mode 100644 index f2399b1..0000000 --- a/docs/functions/SimpleParser.html +++ /dev/null @@ -1,95 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Function SimpleParser

-
-
    - -
  • -

    Invoked once per task.

    - -

    Throws

    an error if the Parser implementation wants to reject the task. It -is valid to raise Errors if stderr is undefined.

    - -

    See

    BatchProcessOptions

    -
    -
    -

    Parameters

    -
      -
    • -
      stdout: string
      -

      the concatenated stream from stdin, stripped of the PASS -or FAIL tokens from BatchProcessOptions.

      -
    • -
    • -
      stderr: undefined | string
      -

      if defined, includes all text emitted to stderr.

      -
    • -
    • -
      passed: boolean
      -

      true iff the PASS pattern was found in stdout.

      -
    -

    Returns string | Promise<string>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/functions/kill.html b/docs/functions/kill.html deleted file mode 100644 index 7a201e8..0000000 --- a/docs/functions/kill.html +++ /dev/null @@ -1,87 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Function kill

-
-
    - -
  • -

    Send a signal to the given process id.

    - -

    Export

    -
    -

    Parameters

    -
      -
    • -
      pid: undefined | null | number
      -

      the process id. Required.

      -
    • -
    • -
      Optional force: boolean = false
      -

      if true, and the current user has -permissions to send the signal, the pid will be forced to shut down.

      -
    -

    Returns void

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/functions/logger-1.html b/docs/functions/logger-1.html deleted file mode 100644 index 8c6b0e9..0000000 --- a/docs/functions/logger-1.html +++ /dev/null @@ -1,72 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Function logger

-
-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/functions/pidExists.html b/docs/functions/pidExists.html deleted file mode 100644 index 8c6bc2b..0000000 --- a/docs/functions/pidExists.html +++ /dev/null @@ -1,83 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Function pidExists

-
-
    - -
  • -
    -

    Returns

    true if the given process id is in the local -process table. The PID may be paused or a zombie, though.

    -
    -
    -

    Parameters

    -
      -
    • -
      pid: undefined | null | number
      -

      process id. Required.

      -
    -

    Returns Promise<boolean>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/functions/pids.html b/docs/functions/pids.html deleted file mode 100644 index 019e35f..0000000 --- a/docs/functions/pids.html +++ /dev/null @@ -1,76 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Function pids

-
-
    - -
  • -
    -

    Export

    -

    Returns

    all the Process IDs in the process table.

    -
    -

    Returns Promise<number[]>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/functions/setLogger.html b/docs/functions/setLogger.html deleted file mode 100644 index b09a9d2..0000000 --- a/docs/functions/setLogger.html +++ /dev/null @@ -1,77 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Function setLogger

-
-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index e2f8de4..0000000 --- a/docs/index.html +++ /dev/null @@ -1,151 +0,0 @@ -Codestin Search App
-
- -
-
-
-
-

batch-cluster

-
- -

batch-cluster

-
-

Efficient, concurrent work via batch-mode command-line tools from within Node.js.

-

npm version -Build status -GitHub issues -Language grade: JavaScript -Known Vulnerabilities

-

Many command line tools, like -ExifTool, -PowerShell, and -GraphicsMagick, support running in a "batch -mode" that accept a series of discrete commands provided through stdin and -results through stdout. As these tools can be fairly large, spinning them up can -be expensive (especially on Windows).

-

This module allows you to run a series of commands, or Tasks, processed by a -cluster of these processes.

-

This module manages both a queue of pending tasks, feeding processes pending -tasks when they are idle, as well as monitoring the child processes for errors -and crashes. Batch processes are also recycled after processing N tasks or -running for N seconds, in an effort to minimize the impact of any potential -memory leaks.

-

As of version 4, retry logic for tasks is a separate concern from this module.

-

This package powers exiftool-vendored, -whose source you can examine as an example consumer.

- - -

Installation

-
-

Depending on your yarn/npm preference:

-
$ yarn add batch-cluster
# or
$ npm install --save batch-cluster -
- - -

Changelog

-
-

See CHANGELOG.md.

- - -

Usage

-
-

The child process must use stdin and stdout for control/response. -BatchCluster will ensure a given process is only given one task at a time.

-
    -
  1. Create a singleton instance of -BatchCluster.

    -

    Note the constructor -options -takes a union type of

    - -
  2. -
  3. The default logger -writes warning and error messages to console.warn and console.error. You -can change this to your logger by using -setLogger or by providing a logger to the BatchCluster constructor.

    -
  4. -
  5. Implement the Parser -class to parse results from your child process.

    -
  6. -
  7. Construct or extend the -Task -class with the desired command and the parser you built in the previous -step, and submit it to your BatchCluster's -enqueueTask -method.

    -
  8. -
-

See -src/test.ts -for an example child process. Note that the script is designed to be flaky on -order to test BatchCluster's retry and error handling code.

- - -

Caution

-
-

The default BatchClusterOptions.cleanupChildProcs value of true means that BatchCluster will try to use ps to ensure Node's view of process state are correct, and that errant -processes are cleaned up.

-

If you run this in a docker image based off Alpine or Debian Slim, this won't work properly unless you install the procps package.

-

See issue #13 for details.

-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/interfaces/BatchClusterEvents.html b/docs/interfaces/BatchClusterEvents.html deleted file mode 100644 index 9553b9b..0000000 --- a/docs/interfaces/BatchClusterEvents.html +++ /dev/null @@ -1,393 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Interface BatchClusterEvents

-
-

This interface describes the BatchCluster's event names as fields. The type -of the field describes the event data payload.

-

See BatchClusterEmitter for more details.

-
-
-

Hierarchy

-
    -
  • BatchClusterEvents
-
-
-
- -
-
-

Properties

-
- -
beforeEnd: (() => void)
-
-

Type declaration

-
    -
  • -
      -
    • (): void
    • -
    • -

      Emitted when this instance is in the process of ending.

      -
      -

      Returns void

-
- -
childEnd: ((childProcess: BatchProcess, reason: ChildExitReason) => void)
-
-

Type declaration

-
-
- -
childStart: ((childProcess: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (childProcess: BatchProcess): void
    • -
    • -

      Emitted when a child process has started

      -
      -
      -

      Parameters

      -
      -

      Returns void

-
- -
end: (() => void)
-
-

Type declaration

-
    -
  • -
      -
    • (): void
    • -
    • -

      Emitted when this instance has ended. No child processes should remain at -this point.

      -
      -

      Returns void

-
- -
endError: ((error: Error, proc?: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (error: Error, proc?: BatchProcess): void
    • -
    • -

      Emitted when a child process has an error during shutdown

      -
      -
      -

      Parameters

      -
      -

      Returns void

-
- -
fatalError: ((error: Error) => void)
-
-

Type declaration

-
-
- -
healthCheckError: ((error: Error, proc: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (error: Error, proc: BatchProcess): void
    • -
    • -

      Emitted when a process fails health checks

      -
      -
      -

      Parameters

      -
      -

      Returns void

-
- -
internalError: ((error: Error) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (error: Error): void
    • -
    • -

      Emitted when an internal consistency check fails

      -
      -
      -

      Parameters

      -
        -
      • -
        error: Error
      -

      Returns void

-
- -
noTaskData: ((stdoutData: null | string | Buffer, stderrData: null | string | Buffer, proc: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (stdoutData: null | string | Buffer, stderrData: null | string | Buffer, proc: BatchProcess): void
    • -
    • -

      Emitted when child processes write to stdout or stderr without a current -task

      -
      -
      -

      Parameters

      -
        -
      • -
        stdoutData: null | string | Buffer
      • -
      • -
        stderrData: null | string | Buffer
      • -
      • -
        proc: BatchProcess
      -

      Returns void

-
- -
startError: ((error: Error, childProcess?: BatchProcess) => void)
-
-

Type declaration

-
-
- -
taskData: ((data: string | Buffer, task: undefined | Task<any>, proc: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (data: string | Buffer, task: undefined | Task<any>, proc: BatchProcess): void
    • -
    • -

      Emitted when tasks receive data, which may be partial chunks from the task -stream.

      -
      -
      -

      Parameters

      -
      -

      Returns void

-
- -
taskError: ((error: Error, task: Task<any>, proc: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (error: Error, task: Task<any>, proc: BatchProcess): void
    • -
    • -

      Emitted when a task has an error

      -
      -
      -

      Parameters

      -
      -

      Returns void

-
- -
taskResolved: ((task: Task<any>, proc: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
-
- -
taskTimeout: ((timeoutMs: number, task: Task<any>, proc: BatchProcess) => void)
-
-

Type declaration

-
    -
  • -
      -
    • (timeoutMs: number, task: Task<any>, proc: BatchProcess): void
    • -
    • -

      Emitted when a task times out. Note that a taskError event always succeeds these events.

      -
      -
      -

      Parameters

      -
      -

      Returns void

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/interfaces/BatchProcessOptions.html b/docs/interfaces/BatchProcessOptions.html deleted file mode 100644 index 8dc20b7..0000000 --- a/docs/interfaces/BatchProcessOptions.html +++ /dev/null @@ -1,118 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Interface BatchProcessOptions

-
-

BatchProcessOptions have no reasonable defaults, as they are specific to -the API of the command that BatchCluster is spawning.

-

All fields must be set.

-
-
-

Hierarchy

-
    -
  • BatchProcessOptions
-
-
-
- -
-
-

Properties

-
- -
exitCommand?: string
-

Command to end the child batch process. If not provided (or undefined), -stdin will be closed to signal to the child process that it may terminate, -and if it does not shut down within endGracefulWaitTimeMillis, it will be -SIGHUP'ed.

-
-
- -
fail: string | RegExp
-

Expected text to print if a command fails. Cannot be blank. Strings will -be interpreted as a regular expression fragment.

-
-
- -
healthCheckCommand?: string
-

If provided, and healthCheckIntervalMillis is greater than 0, or the -previous task failed, this command will be sent to child processes.

-

If the command outputs to stderr or returns a fail string, the process will -be considered unhealthy and recycled.

-
-
- -
pass: string | RegExp
-

Expected text to print if a command passes. Cannot be blank. Strings will -be interpreted as a regular expression fragment.

-
-
- -
versionCommand: string
-

Low-overhead command to verify the child batch process has started. Will -be invoked immediately after spawn. This command must return before any -tasks will be given to a given process.

-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/interfaces/ChildProcessFactory.html b/docs/interfaces/ChildProcessFactory.html deleted file mode 100644 index 3b5ab37..0000000 --- a/docs/interfaces/ChildProcessFactory.html +++ /dev/null @@ -1,83 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Interface ChildProcessFactory

-
-

These are required parameters for a given BatchCluster.

-
-
-

Hierarchy

-
    -
  • ChildProcessFactory
-
-
-
- -
-
-

Properties

-
-
-

Properties

-
- -
processFactory: (() => ChildProcess | Promise<ChildProcess>)
-
-

Type declaration

-
    -
  • -
      -
    • (): ChildProcess | Promise<ChildProcess>
    • -
    • -

      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.

      -
      -

      Returns ChildProcess | Promise<ChildProcess>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/interfaces/Logger.html b/docs/interfaces/Logger.html deleted file mode 100644 index c6b4abe..0000000 --- a/docs/interfaces/Logger.html +++ /dev/null @@ -1,96 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Interface Logger

-
-

Simple interface for logging.

-
-
-

Hierarchy

-
    -
  • Logger
-
-
-
- -
-
-

Properties

-
-
-

Properties

-
- -
debug: LogFunc
-
- -
error: LogFunc
-
- -
info: LogFunc
-
- -
trace: LogFunc
-
- -
warn: LogFunc
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/interfaces/Parser.html b/docs/interfaces/Parser.html deleted file mode 100644 index 95259d6..0000000 --- a/docs/interfaces/Parser.html +++ /dev/null @@ -1,104 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Interface Parser<T>

-
-

Type Parameters

-
    -
  • -

    T

-
-

Hierarchy

-
    -
  • Parser
-
-
    - -
  • -

    Invoked once per task.

    - -

    Throws

    an error if the Parser implementation wants to reject the task. It -is valid to raise Errors if stderr is undefined.

    - -

    See

    BatchProcessOptions

    -
    -
    -

    Parameters

    -
      -
    • -
      stdout: string
      -

      the concatenated stream from stdin, stripped of the PASS -or FAIL tokens from BatchProcessOptions.

      -
    • -
    • -
      stderr: undefined | string
      -

      if defined, includes all text emitted to stderr.

      -
    • -
    • -
      passed: boolean
      -

      true iff the PASS pattern was found in stdout.

      -
    -

    Returns T | Promise<T>

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html deleted file mode 100644 index 216ba86..0000000 --- a/docs/modules.html +++ /dev/null @@ -1,105 +0,0 @@ -Codestin Search App
-
- -
- -
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/serve.json b/docs/serve.json deleted file mode 100644 index 1a05945..0000000 --- a/docs/serve.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cleanUrls": false -} \ No newline at end of file diff --git a/docs/types/BatchClusterEmitter.html b/docs/types/BatchClusterEmitter.html deleted file mode 100644 index cd89da7..0000000 --- a/docs/types/BatchClusterEmitter.html +++ /dev/null @@ -1,86 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Type alias BatchClusterEmitter

-
BatchClusterEmitter: TypedEventEmitter<BatchClusterEvents>
-

The BatchClusterEmitter signature is built up automatically by the -BatchClusterEvents interface, which ensures .on, .off, and -.emit signatures are all consistent, and include the correct data payloads -for all of BatchCluster's events.

-

This approach has some benefits:

-
    -
  • it ensures that on(), off(), and emit() signatures are all consistent,
  • -
  • supports editor autocomplete, and
  • -
  • offers strong typing,
  • -
-

but has one drawback:

-
    -
  • jsdocs don't list all signatures directly: you have to visit the event -source interface.
  • -
-

See BatchClusterEvents for a the list of events and their payload -signatures

-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/types/ChildExitReason.html b/docs/types/ChildExitReason.html deleted file mode 100644 index 6d85069..0000000 --- a/docs/types/ChildExitReason.html +++ /dev/null @@ -1,68 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Type alias ChildExitReason

-
ChildExitReason: WhyNotHealthy | "tooMany"
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/types/WhyNotHealthy.html b/docs/types/WhyNotHealthy.html deleted file mode 100644 index e206c5b..0000000 --- a/docs/types/WhyNotHealthy.html +++ /dev/null @@ -1,68 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Type alias WhyNotHealthy

-
WhyNotHealthy: "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"
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/types/WhyNotReady.html b/docs/types/WhyNotReady.html deleted file mode 100644 index 6ad7abe..0000000 --- a/docs/types/WhyNotReady.html +++ /dev/null @@ -1,68 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Type alias WhyNotReady

-
WhyNotReady: WhyNotHealthy | "busy"
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/variables/ConsoleLogger.html b/docs/variables/ConsoleLogger.html deleted file mode 100644 index 26e7d8b..0000000 --- a/docs/variables/ConsoleLogger.html +++ /dev/null @@ -1,81 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Variable ConsoleLoggerConst

-
ConsoleLogger: Logger = ...
-

Default Logger implementation.

-
    -
  • debug and info go to util.debuglog("batch-cluster")`.

    -
  • -
  • warn and error go to console.warn and console.error.

    -
  • -
- -

See

-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/variables/Log.html b/docs/variables/Log.html deleted file mode 100644 index 133a1e5..0000000 --- a/docs/variables/Log.html +++ /dev/null @@ -1,112 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Variable LogConst

-
Log: { filterLevels: ((l: Logger, minLogLevel: keyof Logger) => any); withLevels: ((delegate: Logger) => Logger); withTimestamps: ((delegate: Logger) => any) } = ...
-
-

Type declaration

-
    -
  • -
    filterLevels: ((l: Logger, minLogLevel: keyof Logger) => any)
    -
      -
    • -
  • -
  • -
    withLevels: ((delegate: Logger) => Logger)
    -
  • -
  • -
    withTimestamps: ((delegate: Logger) => any)
    -
      -
    • -
        -
      • (delegate: Logger): any
      • -
      • -
        -

        Parameters

        -
        -

        Returns any

-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/variables/LogLevels.html b/docs/variables/LogLevels.html deleted file mode 100644 index 59c272a..0000000 --- a/docs/variables/LogLevels.html +++ /dev/null @@ -1,68 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Variable LogLevelsConst

-
LogLevels: (keyof Logger)[] = ...
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/docs/variables/NoLogger.html b/docs/variables/NoLogger.html deleted file mode 100644 index 05ec956..0000000 --- a/docs/variables/NoLogger.html +++ /dev/null @@ -1,70 +0,0 @@ -Codestin Search App
-
- -
-
-
-
- -

Variable NoLoggerConst

-
NoLogger: Logger = ...
-

Logger that disables all logging.

-
-
-
-

Generated using TypeDoc

-
\ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..61dc251 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,74 @@ +// eslint.config.mjs +import eslint from "@eslint/js"; +import importPlugin from "eslint-plugin-import"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +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 + }, + }, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2e41583 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8503 @@ +{ + "name": "batch-cluster", + "version": "15.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "batch-cluster", + "version": "15.0.1", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.35.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": "^24.3.0", + "@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.32.0", + "globals": "^16.4.0", + "mocha": "^11.7.2", + "npm-check-updates": "^18.2.1", + "npm-run-all": "4.1.5", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", + "release-it": "^19.0.4", + "rimraf": "^5.0.10", + "seedrandom": "^3.0.5", + "serve": "^14.2.5", + "source-map-support": "^0.5.21", + "split2": "^4.2.0", + "ts-node": "^10.9.2", + "typedoc": "^0.28.11", + "typescript": "~5.9.2", + "typescript-eslint": "^8.41.0" + }, + "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.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "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.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "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.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "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.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.11.0.tgz", + "integrity": "sha512-ooCDMAOKv71O7MszbXjSQGcI6K5T6NKlemQZOBHLq7Sv/oXCRfYbZ7UgbzFdl20lSXju6Juds4I3y30R6rHA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.11.0", + "@shikijs/langs": "^3.11.0", + "@shikijs/themes": "^3.11.0", + "@shikijs/types": "^3.11.0", + "@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/@inquirer/checkbox": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.2.tgz", + "integrity": "sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.16.tgz", + "integrity": "sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.0.tgz", + "integrity": "sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.18.tgz", + "integrity": "sha512-yeQN3AXjCm7+Hmq5L6Dm2wEDeBRdAZuyZ4I7tWSSanbxDzqM0KqzoDbKM7p4ebllAYdoQuPJS6N71/3L281i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/external-editor": "^1.0.1", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.18.tgz", + "integrity": "sha512-xUjteYtavH7HwDMzq4Cn2X4Qsh5NozoDHCJTdoXg9HfZ4w3R6mxV1B9tL7DGJX2eq/zqtsFjhm0/RJIMGlh3ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", + "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.2.tgz", + "integrity": "sha512-hqOvBZj/MhQCpHUuD3MVq18SSoDNHy7wEnQ8mtvs71K8OPZVXJinOzcvQna33dNYLYE4LkA9BlhAhK6MJcsVbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.18.tgz", + "integrity": "sha512-7exgBm52WXZRczsydCVftozFTrrwbG5ySE0GqUd2zLNSBXyIucs2Wnm7ZKLe/aUu6NUg9dg7Q80QIHCdZJiY4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.18.tgz", + "integrity": "sha512-zXvzAGxPQTNk/SbT3carAD4Iqi6A2JS2qtcqQjsL22uvD+JfQzUrDEtPjLL7PLn8zlSNyPdY02IiQjzoL9TStA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.4.tgz", + "integrity": "sha512-MuxVZ1en1g5oGamXV3DWP89GEkdD54alcfhHd7InUW5BifAdKQEK9SLFa/5hlWbvuhMPlobF0WAx7Okq988Jxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.2", + "@inquirer/confirm": "^5.1.16", + "@inquirer/editor": "^4.2.18", + "@inquirer/expand": "^4.0.18", + "@inquirer/input": "^4.2.2", + "@inquirer/number": "^3.0.18", + "@inquirer/password": "^4.0.18", + "@inquirer/rawlist": "^4.1.6", + "@inquirer/search": "^3.1.1", + "@inquirer/select": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.6.tgz", + "integrity": "sha512-KOZqa3QNr3f0pMnufzL7K+nweFFCCBs6LCXZzXDrVGTyssjLeudn5ySktZYv1XiSqobyHRYYK0c6QsOxJEhXKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.1.tgz", + "integrity": "sha512-TkMUY+A2p2EYVY3GCTItYGvqT6LiLzHBnqsU1rJbrpXUijFfM6zvUx0R4civofVwFCmJZcKqOVwwWAjplKkhxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.2.tgz", + "integrity": "sha512-nwous24r31M+WyDEHV+qckXkepvihxhnyIaod2MG7eCE6G0Zm/HUF6jgN8GXgf4U7AU6SLseKdanY195cwvU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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/@nodeutils/defaults-deep": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodeutils/defaults-deep/-/defaults-deep-1.1.0.tgz", + "integrity": "sha512-gG44cwQovaOFdSR02jR9IhVRpnDP64VN6JdjYJTfNz4J4fWn7TQnmrf22nSjRqlwlxPcW8PL/L3KbJg3tdwvpg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash": "^4.15.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", + "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", + "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", + "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^6.1.4", + "@octokit/plugin-paginate-rest": "^11.4.2", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.3.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@phun-ky/typeof": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-1.2.8.tgz", + "integrity": "sha512-7J6ca1tK0duM2BgVB+CuFMh3idlIVASOP2QvOCbNWDc6JnvjtKa9nufPoJQQ4xrwBonwgT1TIhRRcEtzdVgWsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.9.0 || >=22.0.0", + "npm": ">=10.8.2" + }, + "funding": { + "url": "https://github.com/phun-ky/typeof?sponsor=1" + } + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.11.0.tgz", + "integrity": "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.11.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.11.0.tgz", + "integrity": "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.11.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.11.0.tgz", + "integrity": "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.11.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.11.0.tgz", + "integrity": "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==", + "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/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/parse-path": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", + "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", + "dev": true, + "license": "MIT" + }, + "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.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "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.41.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", + "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": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", + "dev": true, + "license": "MIT", + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "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.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "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.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "eslint-visitor-keys": "^4.2.1" + }, + "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/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "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.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "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/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "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/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "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/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "dev": true, + "license": "Apache-2.0" + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "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/boxen/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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "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==", + "deprecated": "functionality of this lib is built-in to chai now. see more details here: https://github.com/debitoor/chai-subset/pull/85", + "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/chardet": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "dev": true, + "license": "MIT" + }, + "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/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "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/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/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/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "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/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "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-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "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", + "stop-iteration-iterator": "^1.1.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/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", + "@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.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.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.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "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.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "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.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/eslint/node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "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/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/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/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-up": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", + "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^9.2.0" + } + }, + "node_modules/git-url-parse": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", + "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-up": "^8.1.0" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "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.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.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/inquirer": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.7.0.tgz", + "integrity": "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/prompts": "^7.6.0", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "mute-stream": "^2.0.0", + "run-async": "^4.0.4", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "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-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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-ssh": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "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/is-wsl/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/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/issue-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", + "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, + "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/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "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/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "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/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "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/macos-release": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.4.0.tgz", + "integrity": "sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "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/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.7.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.2.tgz", + "integrity": "sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ==", + "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": "^9.2.0", + "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.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "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/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/new-github-release-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-2.0.0.tgz", + "integrity": "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^2.5.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/new-github-release-url/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/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-check-updates": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.2.1.tgz", + "integrity": "sha512-g1VjhAtGMSFFmmN5fT77aF9Eg9dZ6WG9WAqOv7RmWL2ANfeBZGgi6MxYwcNxwSIp5t7Nky0oNFEwHcG6EHQFKw==", + "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-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "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/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "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/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "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/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-name": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.1.0.tgz", + "integrity": "sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "macos-release": "^3.3.0", + "windows-release": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "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/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-path": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", + "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", + "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-path": "^7.0.0", + "parse-path": "^7.0.0" + }, + "engines": { + "node": ">=14.13.0" + } + }, + "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/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "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/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "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.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "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.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.2.0.tgz", + "integrity": "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/protocols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "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/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/release-it": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.0.4.tgz", + "integrity": "sha512-W9A26FW+l1wy5fDg9BeAknZ19wV+UvHUDOC4D355yIOZF/nHBOIhjDwutKd4pikkjvL7CpKeF+4zLxVP9kmVEw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/webpro" + } + ], + "license": "MIT", + "dependencies": { + "@nodeutils/defaults-deep": "1.1.0", + "@octokit/rest": "21.1.1", + "@phun-ky/typeof": "1.2.8", + "async-retry": "1.3.3", + "c12": "3.1.0", + "ci-info": "^4.3.0", + "eta": "3.5.0", + "git-url-parse": "16.1.0", + "inquirer": "12.7.0", + "issue-parser": "7.0.1", + "lodash.merge": "4.6.2", + "mime-types": "3.0.1", + "new-github-release-url": "2.0.0", + "open": "10.2.0", + "ora": "8.2.0", + "os-name": "6.1.0", + "proxy-agent": "6.5.0", + "semver": "7.7.2", + "tinyglobby": "0.2.14", + "undici": "6.21.3", + "url-join": "5.0.0", + "wildcard-match": "5.1.4", + "yargs-parser": "21.1.1" + }, + "bin": { + "release-it": "bin/release-it.js" + }, + "engines": { + "node": "^20.12.0 || >=22.0.0" + } + }, + "node_modules/release-it/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/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/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "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-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "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.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", + "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.8.1", + "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/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "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/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.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/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "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/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/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "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": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "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.11", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.11.tgz", + "integrity": "sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.9.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.0" + }, + "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 || 5.9.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "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.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.41.0.tgz", + "integrity": "sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.41.0", + "@typescript-eslint/parser": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.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 <6.0.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": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, + "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/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.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/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "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/wildcard-match": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", + "dev": true, + "license": "ISC" + }, + "node_modules/windows-release": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz", + "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/windows-release/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/windows-release/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "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": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "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" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 6db5986..85b9dbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "batch-cluster", - "version": "11.0.0", + "version": "15.0.1", "description": "Manage a cluster of child processes", "main": "dist/BatchCluster.js", "homepage": "https://photostructure.github.io/batch-cluster.js/", @@ -10,64 +10,92 @@ "types": "dist/BatchCluster.d.ts", "repository": { "type": "git", - "url": "https://github.com/photostructure/batch-cluster.js.git" + "url": "git+https://github.com/photostructure/batch-cluster.js.git" + }, + "publishConfig": { + "access": "public", + "provenance": true }, "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": "run-p fmt:*", + "fmt:pkg": "npm pkg fix", + "fmt:prettier": "prettier --write .", + "lint": "eslint src", "compile": "tsc", "watch": "rimraf dist & tsc --watch", - "pretest": "yarn clean && yarn lint && yarn compile", + "pretest": "run-s clean lint 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": "run-s docs:*", + "docs:build": "typedoc", + "docs:serve": "cp .serve.json build/docs/serve.json && touch build/docs/.nojekyll && serve build/docs", + "update": "run-p update:*", + "update:deps": "npm-check-updates --upgrade --install always", + "install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest", + "update:actions": "pinact run -u", + "release": "release-it", + "precommit": "npm i && run-s update docs:build test" }, "release-it": { + "src": { + "tagName": "v%s", + "commitArgs": "-S", + "tagArgs": "-S" + }, "hooks": { "before:init": [ - "yarn install", - "yarn test" + "npm ci", + "npm run clean", + "npm run compile" ] }, "github": { "release": true + }, + "npm": { + "publish": true, + "skipChecks": true } }, + "# release-it.npm.skipChecks": "Required for OIDC Trusted Publishing - bypasses npm auth checks since OIDC handles authentication automatically. See: https://github.com/release-it/release-it/issues/1244 and https://docs.npmjs.com/trusted-publishers#supported-cicd-providers", "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { - "@types/chai": "^4.3.3", - "@types/chai-as-promised": "^7.1.5", - "@types/chai-string": "^1.4.2", - "@types/chai-subset": "^1.3.3", - "@types/mocha": "^9.1.1", - "@types/node": "^18.7.8", - "@typescript-eslint/eslint-plugin": "^5.33.1", - "@typescript-eslint/parser": "^5.33.1", - "chai": "^4.3.6", - "chai-as-promised": "^7.1.1", - "chai-string": "^1.5.0", + "@eslint/js": "^9.35.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": "^24.3.0", + "@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": "^8.22.0", - "eslint-plugin-import": "^2.26.0", - "mocha": "^10.0.0", - "prettier": "^2.7.1", - "rimraf": "^3.0.2", + "eslint": "^9.27.0", + "eslint-plugin-import": "^2.32.0", + "globals": "^16.4.0", + "mocha": "^11.7.2", + "npm-check-updates": "^18.2.1", + "npm-run-all": "4.1.5", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", + "release-it": "^19.0.4", + "rimraf": "^5.0.10", "seedrandom": "^3.0.5", - "serve": "^14.0.1", + "serve": "^14.2.5", "source-map-support": "^0.5.21", - "split2": "^4.1.0", - "timekeeper": "^2.2.0", - "typedoc": "^0.23.10", - "typescript": "~4.7.4" + "split2": "^4.2.0", + "ts-node": "^10.9.2", + "typedoc": "^0.28.11", + "typescript": "~5.9.2", + "typescript-eslint": "^8.41.0" } } diff --git a/src/Args.ts b/src/Args.ts new file mode 100644 index 0000000..ad0e18d --- /dev/null +++ b/src/Args.ts @@ -0,0 +1 @@ +export type Args = T extends (...args: infer A) => void ? A : never; diff --git a/src/Array.spec.ts b/src/Array.spec.ts index ecfe2ef..f18ed59 100644 --- a/src/Array.spec.ts +++ b/src/Array.spec.ts @@ -1,43 +1,43 @@ -import { filterInPlace } from "./Array" -import { expect, times } from "./_chai.spec" +import { filterInPlace } from "./Array"; +import { expect, times } from "./_chai.spec"; describe("Array", () => { describe("filterInPlace()", () => { it("no-ops if filter returns true", () => { - const arr = times(10, (i) => i) - const exp = times(10, (i) => i) - expect(filterInPlace(arr, () => true)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = times(10, (i) => i); + expect(filterInPlace(arr, () => true)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("clears array if filter returns false", () => { - const arr = times(10, (i) => i) - const exp: number[] = [] - expect(filterInPlace(arr, () => false)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp: number[] = []; + expect(filterInPlace(arr, () => false)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for < 5 filter", () => { - const arr = times(10, (i) => i) - const exp = [0, 1, 2, 3, 4] - expect(filterInPlace(arr, (i) => i < 5)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = [0, 1, 2, 3, 4]; + expect(filterInPlace(arr, (i) => i < 5)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for > 5 filter", () => { - const arr = times(10, (i) => i) - const exp = [5, 6, 7, 8, 9] - expect(filterInPlace(arr, (i) => i >= 5)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = [5, 6, 7, 8, 9]; + expect(filterInPlace(arr, (i) => i >= 5)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for even filter", () => { - const arr = times(10, (i) => i) - const exp = [0, 2, 4, 6, 8] - expect(filterInPlace(arr, (i) => i % 2 === 0)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = [0, 2, 4, 6, 8]; + expect(filterInPlace(arr, (i) => i % 2 === 0)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for odd filter", () => { - const arr = times(10, (i) => i) - const exp = [1, 3, 5, 7, 9] - expect(filterInPlace(arr, (i) => i % 2 === 1)).to.eql(exp) - expect(arr).to.eql(exp) - }) - }) -}) + const arr = times(10, (i) => i); + const exp = [1, 3, 5, 7, 9]; + expect(filterInPlace(arr, (i) => i % 2 === 1)).to.eql(exp); + expect(arr).to.eql(exp); + }); + }); +}); diff --git a/src/Array.ts b/src/Array.ts index 9372002..f012d88 100644 --- a/src/Array.ts +++ b/src/Array.ts @@ -3,29 +3,27 @@ * predicate `filter`. */ export function filterInPlace(arr: T[], filter: (t: T) => boolean): T[] { - const len = arr.length - let j = 0 + const len = arr.length; + 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]! + const ea = arr[i]!; if (filter(ea)) { - if (i !== j) arr[j] = ea - j++ + if (i !== j) arr[j] = ea; + j++; } } - arr.length = j - return arr + arr.length = j; + return arr; } export function count( arr: T[], - predicate: (t: T, idx: number) => boolean + predicate: (t: T, idx: number) => boolean, ): number { - let acc = 0 + 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++ + if (predicate(arr[idx]!, idx)) acc++; } - return acc + return acc; } diff --git a/src/Async.ts b/src/Async.ts index 8a1bc8d..367c5d4 100644 --- a/src/Async.ts +++ b/src/Async.ts @@ -1,10 +1,10 @@ -import timers from "timers" +import timers from "node:timers"; export function delay(millis: number, unref = false): Promise { return new Promise((resolve) => { - const t = timers.setTimeout(() => resolve(), millis) - if (unref) t.unref() - }) + const t = timers.setTimeout(() => resolve(), millis); + if (unref) t.unref(); + }); } /** @@ -14,37 +14,17 @@ export function delay(millis: number, unref = false): Promise { export async function until( f: (count: number) => boolean | Promise, timeoutMs: number, - delayMs = 50 + delayMs = 50, ): Promise { - const timeoutAt = Date.now() + timeoutMs - let count = 0 + const timeoutAt = Date.now() + timeoutMs; + let count = 0; while (Date.now() < timeoutAt) { if (await f(count)) { - return true + return true; } else { - count++ - await delay(delayMs) - } - } - 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 + count++; + await delay(delayMs); } } + return false; } diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index db631b4..24802e9 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -1,36 +1,36 @@ -import process from "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 FakeTimers from "@sinonjs/fake-timers"; +import process from "node:process"; import { + childProcs, currentTestPids, expect, flatten, parser, parserErrors, processFactory, - procs, setFailratePct, setIgnoreExit, setNewline, testPids, times, unhandledRejections, -} from "./_chai.spec" - -const isCI = process.env.CI === "1" -const tk = require("timekeeper") +} 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"; function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { - const common = a.filter((ea) => b.includes(ea)) - const minLength = Math.min(a.length, b.length) + const common = a.filter((ea) => b.includes(ea)); + const minLength = Math.min(a.length, b.length); if (common.length < minLength - maxAcceptableDiffs) { expect(a).to.eql( b, @@ -40,209 +40,232 @@ function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { maxAcceptableDiffs, minLength, common_length: common.length, - }) - ) + }), + ); } } describe("BatchCluster", function () { - const ErrorPrefix = "ERROR: " + const ErrorPrefix = "ERROR: "; - const ShutdownTimeoutMs = 12 * secondMs + // Windows CI can be extremely slow to shut down processes, so we need a longer timeout + const ShutdownTimeoutMs = (isWin && isCI ? 30 : 12) * secondMs; function runTasks( bc: BatchCluster, iterations: number, - start = 0 + start = 0, ): Promise[] { return times(iterations, (i) => bc .enqueueTask(new Task("upcase abc " + (i + start), parser)) - .catch((err) => ErrorPrefix + err) - ) + .catch((err) => ErrorPrefix + err), + ); } class Events { - readonly taskData: { cmd: string | undefined; data: string }[] = [] - readonly events: { event: string }[] = [] - readonly startedPids: number[] = [] - readonly exitedPids: number[] = [] - readonly startErrors: Error[] = [] - readonly endErrors: Error[] = [] - readonly fatalErrors: Error[] = [] - readonly taskErrors: Error[] = [] - readonly noTaskData: any[] = [] - readonly healthCheckErrors: Error[] = [] - readonly unhealthyPids: number[] = [] - readonly runtimeMs: number[] = [] + readonly taskData: { cmd: string | undefined; data: string }[] = []; + readonly events: { event: string }[] = []; + readonly startedPids: number[] = []; + readonly exitedPids: number[] = []; + readonly startErrors: Error[] = []; + readonly endErrors: Error[] = []; + readonly fatalErrors: Error[] = []; + readonly taskErrors: Error[] = []; + readonly noTaskData: any[] = []; + readonly healthCheckErrors: Error[] = []; + readonly unhealthyPids: number[] = []; + readonly runtimeMs: number[] = []; } - let events = new Events() - const internalErrors: Error[] = [] + let events = new Events(); + const internalErrors: Error[] = []; function assertExpectedResults(results: string[]) { const dataResults = flatten( - events.taskData.map((ea) => ea.data.split(/[\n\r]+/)) - ) + events.taskData.map((ea) => ea.data.split(/[\n\r]+/)), + ); results.forEach((result, index) => { if (!result.startsWith(ErrorPrefix)) { - expect(result).to.eql("ABC " + index) - expect(dataResults).to.include(result) + expect(result).to.eql("ABC " + index); + expect(dataResults.toString()).to.include(result); } - }) + }); } beforeEach(function () { - events = new Events() - }) + events = new Events(); + }); + + process.on("SIGPIPE", (error) => { + internalErrors.push(new Error("process.on(SIGPIPE): " + String(error))); + }); function postAssertions() { - expect(internalErrors).to.eql([], "internal errors") + expect(internalErrors).to.eql([], "internal errors"); events.runtimeMs.forEach((ea) => expect(ea).to.be.within( 0, 5000, - JSON.stringify({ runtimeMs: events.runtimeMs }) - ) - ) + JSON.stringify({ runtimeMs: events.runtimeMs }), + ), + ); } - const expectedEndEvents = [{ event: "beforeEnd" }, { event: "end" }] + const expectedEndEvents = [{ event: "beforeEnd" }, { event: "end" }]; async function shutdown(bc: BatchCluster) { - if (bc == null) return // we skipped the spec - const endPromise = bc.end(true) + if (bc == null) return; // we skipped the spec + const shutdownStartTime = Date.now(); + const endPromise = bc.end(true); // "ended" should be true immediately, but it may still be waiting for child // processes to exit: - expect(bc.ended).to.eql(true) + expect(bc.ended).to.eql(true); - async function checkShutdown() { + function checkShutdown() { // const isIdle = bc.isIdle // If bc has been told to shut down, it won't ever finish any pending commands. // const pendingCommands = bc.pendingTasks.map((ea) => ea.command) - const runningCommands = bc.currentTasks.map((ea) => ea.command) - const busyProcCount = bc.busyProcCount - const pids = await bc.pids() - const livingPids = await currentTestPids() + const runningCommands = bc.currentTasks.map((ea) => ea.command); + const busyProcCount = bc.busyProcCount; + const pids = bc.pids(); + const livingPids = currentTestPids(); const done = runningCommands.length === 0 && busyProcCount === 0 && pids.length === 0 && - livingPids.length === 0 + livingPids.length === 0; - if (!done) - console.log("shutdown(): waiting for end", { + if (!done) { + const elapsed = Date.now() - shutdownStartTime; + console.log(`shutdown(): waiting for end (${elapsed}ms elapsed)`, { runningCommands, busyProcCount, pids, livingPids, - }) - return done + platform: process.platform, + isCI, + }); + } + return done; } - // Mac CI can be extremely slow to shut down: + // CI environments (especially Windows and Mac) can be extremely slow to shut down: const endOrTimeout = await thenOrTimeout( endPromise.promise.then(() => true), - ShutdownTimeoutMs - ) + ShutdownTimeoutMs, + ); const shutdownOrTimeout = await thenOrTimeout( until(checkShutdown, ShutdownTimeoutMs, 1000), - ShutdownTimeoutMs - ) - expect(endOrTimeout).to.eql(true, ".end() failed") - expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed") + ShutdownTimeoutMs, + ); + + if (isCI && (endOrTimeout !== true || shutdownOrTimeout !== true)) { + console.log( + `Shutdown timeout on CI after ${Date.now() - shutdownStartTime}ms`, + { + endOrTimeout, + shutdownOrTimeout, + platform: process.platform, + ShutdownTimeoutMs, + }, + ); + } + + expect(endOrTimeout).to.eql(true, ".end() failed"); + expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed"); // Calling bc.end() again should be a no-op and return the same Deferred: - expect(bc.end(true).settled).to.eql(true) + expect(bc.end(true).settled).to.eql(true); expect(bc.internalErrorCount).to.eql( 0, JSON.stringify({ internalErrorCount: bc.internalErrorCount, internalErrors, noTaskData: events.noTaskData, - }) - ) - expect(internalErrors).to.eql([], "no expected internal errors") + }), + ); + expect(internalErrors).to.eql([], "no expected internal errors"); expect(events.noTaskData).to.eql( [], "no expected noTaskData events, but got " + - JSON.stringify(events.noTaskData) - ) - return + JSON.stringify(events.noTaskData), + ); + return; } function listen(bc: BatchCluster) { // This is a typings verification, too: bc.on("childStart", (cp) => - map(cp.pid, (ea) => events.startedPids.push(ea)) - ) - bc.on("childEnd", (cp) => map(cp.pid, (ea) => events.exitedPids.push(ea))) - bc.on("startError", (err) => events.startErrors.push(err)) - bc.on("endError", (err) => events.endErrors.push(err)) - bc.on("fatalError", (err) => events.fatalErrors.push(err)) + map(cp.pid, (ea) => events.startedPids.push(ea)), + ); + bc.on("childEnd", (cp) => map(cp.pid, (ea) => events.exitedPids.push(ea))); + bc.on("startError", (err) => events.startErrors.push(err)); + bc.on("endError", (err) => events.endErrors.push(err)); + bc.on("fatalError", (err) => events.fatalErrors.push(err)); bc.on("noTaskData", (stdout, stderr, proc) => { events.noTaskData.push({ stdout: toS(stdout), stderr: toS(stderr), proc_pid: proc?.pid, streamFlushMillis: bc.options.streamFlushMillis, - }) - }) + }); + }); bc.on("internalError", (err) => { - console.error("BatchCluster.spec: internal error: " + err) - internalErrors.push(err) - }) + console.error("BatchCluster.spec: internal error: " + err); + internalErrors.push(err); + }); bc.on("taskData", (data, task: Task | undefined) => events.taskData.push({ cmd: task?.command, data: toS(data), - }) - ) + }), + ); 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!) - }) + const runtimeMs = task.runtimeMs; + expect(runtimeMs).to.not.eql(undefined); + + events.runtimeMs.push(runtimeMs!); + }); bc.on("healthCheckError", (err, proc) => { - events.healthCheckErrors.push(err) - events.unhealthyPids.push(proc.pid) - }) - bc.on("taskError", (err) => events.taskErrors.push(err)) + events.healthCheckErrors.push(err); + events.unhealthyPids.push(proc.pid); + }); + bc.on("taskError", (err) => events.taskErrors.push(err)); for (const event of ["beforeEnd", "end"] as ("beforeEnd" | "end")[]) { - bc.on(event, () => events.events.push({ event })) + bc.on(event, () => events.events.push({ event })); } - return bc + return bc; } - const newlines = ["lf"] + const newlines = ["lf"]; if (isWin) { // Don't need to test crlf except on windows: - newlines.push("crlf") + newlines.push("crlf"); } - it("supports .off()", async () => { - const emitTimes: number[] = [] - const bc = new BatchCluster({ ...DefaultTestOptions, processFactory }) - const listener = () => emitTimes.push(Date.now()) + it("supports .off()", () => { + const emitTimes: number[] = []; + const bc = new BatchCluster({ ...DefaultTestOptions, processFactory }); + const listener = () => emitTimes.push(Date.now()); // pick a random event that doesn't require arguments: - const evt = "beforeEnd" as const - bc.on(evt, listener) - bc.emitter.emit(evt) - expect(emitTimes.length).to.eql(1) - emitTimes.length = 0 - bc.off(evt, listener) - bc.emitter.emit(evt) - expect(emitTimes).to.eql([]) - postAssertions() - }) + const evt = "beforeEnd" as const; + bc.on(evt, listener); + bc.emitter.emit(evt); + expect(emitTimes.length).to.eql(1); + emitTimes.length = 0; + bc.off(evt, listener); + bc.emitter.emit(evt); + expect(emitTimes).to.eql([]); + postAssertions(); + }); for (const newline of newlines) { for (const maxProcs of [1, 4]) { @@ -258,44 +281,44 @@ describe("BatchCluster", function () { minDelayBetweenSpawnMillis, }), function () { - let bc: BatchCluster + let bc: BatchCluster; const opts: any = { ...DefaultTestOptions, maxProcs, minDelayBetweenSpawnMillis, - } + }; if (healthcheck) { - opts.healthCheckIntervalMillis = 250 - opts.healthCheckCommand = "flaky 0.5" // fail half the time (ensure we get a proc end due to "unhealthy") + opts.healthCheckIntervalMillis = 250; + opts.healthCheckCommand = "flaky 0.5"; // fail half the time (ensure we get a proc end due to "unhealthy") } // failrate needs to be high enough to trigger but low enough to allow // retries to succeed. beforeEach(function () { - setNewline(newline as any) - setIgnoreExit(ignoreExit) - bc = listen(new BatchCluster({ ...opts, processFactory })) - procs.length = 0 - }) + setNewline(newline as any); + setIgnoreExit(ignoreExit); + bc = listen(new BatchCluster({ ...opts, processFactory })); + childProcs.length = 0; + }); afterEach(async () => { - await shutdown(bc) - expect(bc.internalErrorCount).to.eql(0) - return - }) + await shutdown(bc); + expect(bc.internalErrorCount).to.eql(0); + return; + }); if (maxProcs > 1) { it("completes work on multiple child processes", async function () { if (isCI) { // don't fight timeouts on GitHub's slower-than-molasses CI boxes: - bc.options.taskTimeoutMillis = 1500 + bc.options.taskTimeoutMillis = 1500; } - this.slow(1) // always show timing + this.slow(1); // always show timing - const pidSet = new Set() - const errors: Error[] = [] + const pidSet = new Set(); + const errors: Error[] = []; for (let i = 0; i < 20; i++) { // run 4 tasks in parallel: @@ -306,126 +329,126 @@ describe("BatchCluster", function () { // DefaultTestOptions.taskTimeoutMillis (250), because we don't // want timeouts. CI failed when this was 100. "sleep 25", - parser - ) - ) + parser, + ), + ), )) { try { - const result = await p - const { pid } = JSON.parse(result) + const result = await p; + const { pid } = JSON.parse(result); if (isNaN(pid)) { throw new Error( - "invalid output: " + JSON.stringify(result) - ) + "invalid output: " + JSON.stringify(result), + ); } else { - pidSet.add(pid) + pidSet.add(pid); } } catch (error) { - errors.push(error as Error) + errors.push(error as Error); } } - if (pidSet.size > 2) break + if (pidSet.size > 2) break; } - const pids = [...pidSet.values()] + const pids = [...pidSet.values()]; // console.dir({ pids, errors }) expect(pids.length).to.be.gt( 2, - "expected more than a couple child processes" - ) + "expected more than a couple child processes", + ); expect(pids.every((ea) => process.pid !== ea)).to.eql( true, "no child pids, " + pids.join(", ") + ", should match this process pid, " + - process.pid - ) + process.pid, + ); expect( - errors.filter((ea) => !String(ea).includes("EUNLUCKY")) - ).to.eql([], "Unexpected errors") - }) + errors.filter((ea) => !String(ea).includes("EUNLUCKY")), + ).to.eql([], "Unexpected errors"); + }); } it("calling .end() when new no-ops", async () => { - await bc.end() - expect(bc.ended).to.eql(true) - expect(bc.isIdle).to.eql(true) - expect((await bc.pids()).length).to.eql(0) - expect(bc.spawnedProcCount).to.eql(0) - expect(events.events).to.eql(expectedEndEvents) - expect(testPids()).to.eql([]) - expect(events.startedPids).to.eql([]) - expect(events.exitedPids).to.eql([]) - postAssertions() - }) + await bc.end(); + expect(bc.ended).to.eql(true); + expect(bc.isIdle).to.eql(true); + expect(bc.pids().length).to.eql(0); + expect(bc.spawnedProcCount).to.eql(0); + expect(events.events).to.eql(expectedEndEvents); + expect(testPids()).to.eql([]); + expect(events.startedPids).to.eql([]); + expect(events.exitedPids).to.eql([]); + postAssertions(); + }); it("calling .end() after running shuts down child procs", async () => { // This just warms up bc to make child procs: const iterations = - maxProcs * (bc.options.maxTasksPerProcess + 1) + maxProcs * (bc.options.maxTasksPerProcess + 1); // we're making exact pid assertions below: don't fight // flakiness. - setFailratePct(0) + setFailratePct(0); - const tasks = await Promise.all(runTasks(bc, iterations)) - assertExpectedResults(tasks) - await shutdown(bc) - console.log(bc.stats()) + const tasks = await Promise.all(runTasks(bc, iterations)); + assertExpectedResults(tasks); + await shutdown(bc); + console.log(bc.stats()); expect(bc.spawnedProcCount).to.be.within( maxProcs, - (iterations + maxProcs) * 3 // because flaky - ) - const pids = testPids() - expect(pids.length).to.be.gte(maxProcs) + (iterations + maxProcs) * 3, // because flaky + ); + const pids = testPids(); + expect(pids.length).to.be.gte(maxProcs); // it's ok to miss a pid due to startup flakiness or cancelled // end tasks. - arrayEqualish(events.startedPids, pids, 1) - arrayEqualish(events.exitedPids, pids, 1) - expect(events.events).to.eql(expectedEndEvents) - postAssertions() - }) + arrayEqualish(events.startedPids, pids, 1); + arrayEqualish(events.exitedPids, pids, 1); + expect(events.events).to.eql(expectedEndEvents); + postAssertions(); + }); it( "runs a given batch process roughly " + opts.maxTasksPerProcess + " before recycling", async function () { - if (isWin && isCI) this.timeout(45 * secondMs) + if (isWin && isCI) this.timeout(45 * secondMs); // make sure we hit an EUNLUCKY: - setFailratePct(60) - let expectedResultCount = 0 - const results = await Promise.all(runTasks(bc, maxProcs)) - expectedResultCount += maxProcs - const pids = await bc.pids() + setFailratePct(60); + let expectedResultCount = 0; + const results = await Promise.all(runTasks(bc, maxProcs)); + expectedResultCount += maxProcs; + const pids = bc.pids(); const iters = Math.floor( - maxProcs * opts.maxTasksPerProcess * 1.5 - ) + maxProcs * opts.maxTasksPerProcess * 1.5, + ); results.push( ...(await Promise.all( - runTasks(bc, iters, expectedResultCount) - )) - ) - console.log(bc.stats()) + runTasks(bc, iters, expectedResultCount), + )), + ); + console.log(bc.stats()); - expectedResultCount += iters - assertExpectedResults(results) - expect(results.length).to.eql(expectedResultCount) + expectedResultCount += iters; + assertExpectedResults(results); + expect(results.length).to.eql(expectedResultCount); // expect some errors: const errorResults = results.filter((ea) => - ea.startsWith(ErrorPrefix) - ) - expect(errorResults).to.not.eql([]) + ea.startsWith(ErrorPrefix), + ); + expect(errorResults).to.not.eql([]); // Expect a reasonable number of new pids. Worst case, we // errored after every start, so there may be more then iters // pids spawned. - expect(procs.length).to.eql(bc.spawnedProcCount) + expect(childProcs.length).to.eql(bc.spawnedProcCount); expect(bc.spawnedProcCount).to.be.within( results.length / opts.maxTasksPerProcess, - results.length * (isWin ? 10 : 5) // because flaky - ) + results.length * (isWin ? 10 : 5), // because flaky + ); // So, at this point, we should have at least _asked_ the // initial child processes to end because they're "worn". @@ -433,53 +456,72 @@ describe("BatchCluster", function () { // Running vacuumProcs will return a promise that will only // resolve when those procs have shut down. - await bc.vacuumProcs() + await bc.vacuumProcs(); // Expect no prior pids to remain, as long as there were before-pids: - if (pids.length > 0) - expect(await bc.pids()).to.not.include.members(pids) + // NOTE: On Windows, PIDs can be reused quickly, so we only check this + // on non-Windows platforms to avoid flakiness + if (pids.length > 0 && !isWin) + expect(bc.pids()).to.not.include.members(pids); expect(bc.meanTasksPerProc).to.be.within( - 0.25, // because flaky - opts.maxTasksPerProcess - ) - expect((await bc.pids()).length).to.be.lte(maxProcs) + 0.15, // because flaky (macOS on GHA resulted in 0.21) + opts.maxTasksPerProcess, + ); + expect(bc.pids().length).to.be.lte(maxProcs); expect((await currentTestPids()).length).to.be.lte( - bc.spawnedProcCount - ) // because flaky + bc.spawnedProcCount, + ); // because flaky - const unhealthy = bc.countEndedChildProcs("unhealthy") + const unhealthy = bc.countEndedChildProcs("unhealthy"); // If it's a short spec and we don't have any worn procs, we // probably don't have any unhealthy procs: if (healthcheck && bc.countEndedChildProcs("worn") > 2) { - expect(unhealthy).to.be.gte(0) + expect(unhealthy).to.be.gte(0); } if (!healthcheck) { - expect(unhealthy).to.eql(0) + expect(unhealthy).to.eql(0); } - await shutdown(bc) + await shutdown(bc); // (no run count assertions) - } - ) + }, + ); it("recovers from invalid commands", async function () { - this.slow(1) + this.slow(1); assertExpectedResults( - await Promise.all(runTasks(bc, maxProcs * 4)) - ) + await Promise.all(runTasks(bc, maxProcs * 4)), + ); const errorResults = await Promise.all( times(maxProcs * 2, () => bc .enqueueTask(new Task("nonsense", parser)) - .catch((err) => err) - ) - ) - filterInPlace( - errorResults, - (ea) => ea != null && !String(ea).includes("EUNLUCKY") - ) + .catch((err: unknown) => err), + ), + ); + 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 && @@ -487,95 +529,97 @@ describe("BatchCluster", function () { ) { // We don't expect these to pass with this config: } else if (maxProcs === 1 && errorResults.length === 0) { - console.warn("(all processes were unlucky)") - return this.skip() + console.warn("(all processes were unlucky)"); + return this.skip(); } else { expect( - errorResults.some((ea) => String(ea).includes("nonsense")) - ).to.eql(true, JSON.stringify(errorResults)) + errorResults.some((ea) => + String(ea).includes("nonsense"), + ), + ).to.eql(true, JSON.stringify(errorResults)); expect( - parserErrors.some((ea) => ea.includes("nonsense")) - ).to.eql(true, JSON.stringify(parserErrors)) + parserErrors.some((ea) => ea.includes("nonsense")), + ).to.eql(true, JSON.stringify(parserErrors)); } - parserErrors.length = 0 + parserErrors.length = 0; // BC should recover: assertExpectedResults( - await Promise.all(runTasks(bc, maxProcs * 4)) - ) + await Promise.all(runTasks(bc, maxProcs * 4)), + ); // (no run count assertions) - return - }) + return; + }); it("times out slow requests", async () => { const task = new Task( "sleep " + (opts.taskTimeoutMillis + 250), // < make sure it times out - parser - ) + parser, + ); await expect( - bc.enqueueTask(task) - ).to.eventually.be.rejectedWith(/timeout|EUNLUCKY/) - postAssertions() - }) + bc.enqueueTask(task), + ).to.eventually.be.rejectedWith(/timeout|EUNLUCKY/); + postAssertions(); + }); it("accepts single and multi-line responses", async () => { - setFailratePct(0) + setFailratePct(0); if (isCI) { // don't fight timeouts on GitHub's slower-than-molasses CI boxes: - bc.options.taskTimeoutMillis = 1500 + bc.options.taskTimeoutMillis = 1500; } - const expected: string[] = [] + const expected: string[] = []; const results = await Promise.all( times(15, (idx) => { // Make a distribution of single, double, and triple line outputs: - const worlds = times(idx % 3, (ea) => "world " + ea) + const worlds = times(idx % 3, (ea) => "world " + ea); expected.push( - [idx + " HELLO", ...worlds].join("\n").toUpperCase() - ) + [idx + " HELLO", ...worlds].join("\n").toUpperCase(), + ); const cmd = ["upcase " + idx + " hello", ...worlds].join( - "
" - ) - return bc.enqueueTask(new Task(cmd, parser)) - }) - ) - expect(results).to.eql(expected) + "
", + ); + return bc.enqueueTask(new Task(cmd, parser)); + }), + ); + expect(results).to.eql(expected); - postAssertions() - }) + postAssertions(); + }); it("rejects a command that results in FAIL", async function () { - const task = new Task("invalid command", parser) - let error: Error | undefined - let result = "" + const task = new Task("invalid command", parser); + let error: Error | undefined; + let result = ""; try { - result = await bc.enqueueTask(task) + result = await bc.enqueueTask(task); } catch (err: any) { - error = err + error = err; } expect(String(error)).to.match( /invalid command|UNLUCKY/, - result - ) - postAssertions() - }) + result, + ); + postAssertions(); + }); it("rejects a command that emits to stderr", async function () { - const task = new Task("stderr omg this should fail", parser) - let error: Error | undefined - let result = "" + const task = new Task("stderr omg this should fail", parser); + let error: Error | undefined; + let result = ""; try { - result = await bc.enqueueTask(task) + result = await bc.enqueueTask(task); } catch (err: any) { - error = err + error = err; } expect(String(error)).to.match( /omg this should fail|UNLUCKY/, - result - ) - postAssertions() - }) - } - ) + result, + ); + postAssertions(); + }); + }, + ); } } } @@ -583,11 +627,11 @@ describe("BatchCluster", function () { } describe("maxProcs", function () { - const iters = 100 - const maxProcs = 10 - const sleepTimeMs = 250 - let bc: BatchCluster - afterEach(() => shutdown(bc)) + const iters = 100; + const maxProcs = 10; + const sleepTimeMs = 250; + let bc: BatchCluster; + afterEach(() => shutdown(bc)); for (const { minDelayBetweenSpawnMillis, expectTaskMin, @@ -611,7 +655,7 @@ describe("BatchCluster", function () { }, ]) { it(JSON.stringify({ minDelayBetweenSpawnMillis }), async () => { - setFailratePct(0) + setFailratePct(0); const opts = { ...DefaultTestOptions, taskTimeoutMillis: 5_000, // < don't test timeouts here @@ -619,52 +663,54 @@ describe("BatchCluster", function () { maxTasksPerProcess: expectedTaskMax + 5, // < don't recycle procs for this test minDelayBetweenSpawnMillis, processFactory, - } - bc = listen(new BatchCluster(opts)) - expect(bc.isIdle).to.eql(true) + }; + bc = listen(new BatchCluster(opts)); + expect(bc.isIdle).to.eql(true); const tasks = await Promise.all( times(iters, async (i) => { - const start = Date.now() - 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 end = Date.now() - return { i, start, end, ...result } - }) - ) - const pid2count = new Map() + const start = Date.now(); + const task = new Task("sleep " + sleepTimeMs, parser); + const resultP = bc.enqueueTask(task); + expect(bc.isIdle).to.eql(false); + const result = JSON.parse(await resultP) as { + pid: number; + } & Record; + const end = Date.now(); + return { i, start, end, ...result }; + }), + ); + const pid2count = new Map(); tasks.forEach((ea) => { - const pid = ea.pid - const count = orElse(pid2count.get(pid), 0) - pid2count.set(pid, count + 1) - }) - expect(bc.isIdle).to.eql(true) + const pid = ea.pid; + const count = pid2count.get(pid) ?? 0; + pid2count.set(pid, count + 1); + }); + expect(bc.isIdle).to.eql(true); console.log({ expectTaskMin, expectedTaskMax, maxProcs, uniqPids: pid2count.size, pid2count, - bcPids: await bc.pids(), - }) + bcPids: bc.pids(), + }); for (const [, count] of pid2count.entries()) { - expect(count).to.be.within(expectTaskMin, expectedTaskMax) + expect(count).to.be.within(expectTaskMin, expectedTaskMax); } - expect(pid2count.size).to.be.within(expectedProcsMin, expectedProcsMax) - }) + expect(pid2count.size).to.be.within(expectedProcsMin, expectedProcsMax); + }); } - }) + }); describe("setMaxProcs", function () { - const maxProcs = 10 - const sleepTimeMs = 250 - let bc: BatchCluster - afterEach(() => shutdown(bc)) + const maxProcs = 10; + const sleepTimeMs = 250; + let bc: BatchCluster; + afterEach(() => shutdown(bc)); it("supports reducing maxProcs", async () => { // don't fight with flakiness here! - setFailratePct(0) + setFailratePct(0); const opts = { ...DefaultTestOptions, minDelayBetweenSpawnMillis: 0, @@ -672,68 +718,68 @@ describe("BatchCluster", function () { maxProcs, maxTasksPerProcess: 100, // < don't recycle procs for this test processFactory, - } - bc = new BatchCluster(opts) - const firstBatchPromises: Promise[] = [] + }; + bc = new BatchCluster(opts); + const firstBatchPromises: Promise[] = []; while (bc.busyProcCount < maxProcs) { firstBatchPromises.push( - bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)) - ) - await delay(25) + bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)), + ); + await delay(25); } - expect(bc.currentTasks.length).to.be.closeTo(maxProcs, 2) - expect(bc.busyProcCount).to.be.closeTo(maxProcs, 2) - expect(bc.procCount).to.be.closeTo(maxProcs, 2) - const maxProcs2 = maxProcs / 2 - bc.setMaxProcs(maxProcs2) + expect(bc.currentTasks.length).to.be.closeTo(maxProcs, 2); + expect(bc.busyProcCount).to.be.closeTo(maxProcs, 2); + expect(bc.procCount).to.be.closeTo(maxProcs, 2); + const maxProcs2 = maxProcs / 2; + bc.setMaxProcs(maxProcs2); const secondBatchPromises = times(maxProcs, () => - bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)) - ) - await Promise.all(firstBatchPromises) - bc.vacuumProcs() + bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)), + ); + await Promise.all(firstBatchPromises); + bc.vacuumProcs(); // We should be dropping BatchProcesses at this point. - expect(bc.busyProcCount).to.be.within(0, maxProcs2) - expect(bc.procCount).to.be.within(0, maxProcs2) + expect(bc.busyProcCount).to.be.within(0, maxProcs2); + expect(bc.procCount).to.be.within(0, maxProcs2); - await Promise.all(secondBatchPromises) + await Promise.all(secondBatchPromises); - expect(bc.busyProcCount).to.eql(0) // because we're done + expect(bc.busyProcCount).to.eql(0); // because we're done // Assert that there were excess procs shut down: - expect(bc.childEndCounts.tooMany).to.be.closeTo(maxProcs - maxProcs2, 2) + expect(bc.childEndCounts.tooMany).to.be.closeTo(maxProcs - maxProcs2, 2); // don't shut down until bc is idle... (otherwise we'll fail due to // "Error: end() called before task completed // ({\"gracefully\":true,\"source\":\"BatchCluster.closeChildProcesses()\"})" - await until(() => bc.isIdle, 5000) + await until(() => bc.isIdle, 5000); - postAssertions() - }) - }) + postAssertions(); + }); + }); describe(".end() cleanup", () => { - const sleepTimeMs = 1000 // must be longer than non-graceful timeout (currently 250) - let bc: BatchCluster - afterEach(() => shutdown(bc)) + const sleepTimeMs = 1000; // must be longer than non-graceful timeout (currently 250) + let bc: BatchCluster; + afterEach(() => shutdown(bc)); 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 () => { - setFailratePct(0) + setFailratePct(0); const opts = { ...DefaultTestOptions, taskTimeoutMillis: sleepTimeMs * 4, // < don't test timeouts here processFactory, - } - bc = new BatchCluster(opts) + }; + bc = new BatchCluster(opts); // Wait for one job to run (so the process spins up and we're ready to go) - await Promise.all(runTasks(bc, 1)) + await Promise.all(runTasks(bc, 1)); expect(stats()).to.eql({ pendingTaskCount: 0, @@ -746,9 +792,9 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - const t = bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)) + const t = bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)); expect(stats()).to.eql({ pendingTaskCount: 1, @@ -761,10 +807,10 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - t.catch((err) => (caught = err)) - await delay(2) + t.catch((err: unknown) => (caught = err)); + await delay(2); expect(stats()).to.eql({ pendingTaskCount: 0, // < yay it's getting processed @@ -777,11 +823,11 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - let caught: any - expect(bc.isIdle).to.eql(false) - await bc.end(false) // not graceful just to shut down faster + let caught: unknown; + expect(bc.isIdle).to.eql(false); + await bc.end(false); // not graceful just to shut down faster expect(stats()).to.eql({ pendingTaskCount: 0, @@ -794,13 +840,15 @@ describe("BatchCluster", function () { childEndCounts: { ending: 1 }, ending: true, ended: true, - }) + }); - expect(bc.isIdle).to.eql(true) - expect(caught?.message).to.include("end() called before task completed") - expect(unhandledRejections).to.eql([]) - }) - }) + expect(bc.isIdle).to.eql(true); + expect((caught as Error)?.message).to.include( + "Process terminated before task completed", + ); + expect(unhandledRejections).to.eql([]); + }); + }); describe("maxProcAgeMillis (cull old children)", function () { const opts = { @@ -810,9 +858,9 @@ describe("BatchCluster", function () { spawnTimeoutMillis: 2000, // maxProcAge must be >= this maxProcAgeMillis: 3000, minDelayBetweenSpawnMillis: 0, - } + }; - let bc: BatchCluster + let bc: BatchCluster; beforeEach( () => @@ -820,32 +868,32 @@ describe("BatchCluster", function () { new BatchCluster({ ...opts, processFactory, - }) - )) - ) + }), + )), + ); - afterEach(() => shutdown(bc)) + afterEach(() => shutdown(bc)); it("culls old child procs", async () => { assertExpectedResults( - await Promise.all(runTasks(bc, opts.maxProcs + 100)) - ) + await Promise.all(runTasks(bc, opts.maxProcs + 100)), + ); // 0 because we might get unlucky. - expect((await bc.pids()).length).to.be.within(0, opts.maxProcs) - await delay(opts.maxProcAgeMillis + 100) - await bc.vacuumProcs() + expect(bc.pids().length).to.be.within(0, opts.maxProcs); + await delay(opts.maxProcAgeMillis + 100); + await bc.vacuumProcs(); console.log({ childEndCounts: bc.childEndCounts, procCount: bc.procCount, maxProcs: opts.maxProcs, - }) - expect(bc.countEndedChildProcs("idle")).to.eql(0) - expect(bc.countEndedChildProcs("old")).to.be.gte(2) + }); + expect(bc.countEndedChildProcs("idle")).to.eql(0); + expect(bc.countEndedChildProcs("old")).to.be.gte(2); // Calling .pids calls .procs(), which culls old procs - expect((await bc.pids()).length).to.be.within(0, opts.maxProcs) - postAssertions() - }) - }) + expect(bc.pids().length).to.be.within(0, opts.maxProcs); + postAssertions(); + }); + }); describe("maxIdleMsPerProcess", function () { const opts = { @@ -853,9 +901,9 @@ describe("BatchCluster", function () { maxProcs: 4, maxIdleMsPerProcess: 1000, maxProcAgeMillis: 30_000, - } + }; - let bc: BatchCluster + let bc: BatchCluster; beforeEach( () => @@ -863,69 +911,78 @@ describe("BatchCluster", function () { new BatchCluster({ ...opts, processFactory, - }) - )) - ) + }), + )), + ); - afterEach(() => shutdown(bc)) + afterEach(() => shutdown(bc)); it("culls idle child procs", async () => { - assertExpectedResults(await Promise.all(runTasks(bc, opts.maxProcs + 10))) + assertExpectedResults( + await Promise.all(runTasks(bc, opts.maxProcs + 10)), + ); // 0 because we might get unlucky. - expect((await bc.pids()).length).to.be.within(0, opts.maxProcs) + expect(bc.pids().length).to.be.within(0, opts.maxProcs); // wait long enough for at least 1 process to be idle and get reaped: - await delay(opts.maxIdleMsPerProcess + 100) - await bc.vacuumProcs() + await delay(opts.maxIdleMsPerProcess + 100); + await bc.vacuumProcs(); console.log({ childEndCounts: bc.childEndCounts, procCount: bc.procCount, maxProcs: opts.maxProcs, - }) - expect(bc.countEndedChildProcs("idle")).to.be.gte(1) - expect(bc.countEndedChildProcs("old")).to.be.lte(1) - expect(bc.countEndedChildProcs("worn")).to.be.lte(2) + }); + expect(bc.countEndedChildProcs("idle")).to.be.gte(1); + expect(bc.countEndedChildProcs("old")).to.be.lte(1); + expect(bc.countEndedChildProcs("worn")).to.be.lte(2); // Calling .pids calls .procs(), which culls old procs - if ((await bc.pids()).length > 0) { - await delay(1000) + if (bc.pids().length > 0) { + await delay(1000); } - expect((await bc.pids()).length).to.eql(0) - postAssertions() - }) - }) + expect(bc.pids().length).to.eql(0); + postAssertions(); + }); + }); describe("maxProcAgeMillis (recycling procs)", () => { - let bc: BatchCluster + let bc: BatchCluster; + let clock: FakeTimers.InstalledClock; + + beforeEach(() => { + clock = FakeTimers.install({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + }); afterEach(() => { - tk.reset() - return shutdown(bc) - }) + clock.uninstall(); + return shutdown(bc); + }); + for (const { maxProcAgeMillis, ctx, exp } of [ { maxProcAgeMillis: 0, ctx: "procs should not be recycled due to old age", exp: (pidsBefore: number[], pidsAfter: number[]) => { - expect(pidsBefore).to.eql(pidsAfter) - expect(bc.countEndedChildProcs("idle")).to.eql(0) - expect(bc.countEndedChildProcs("old")).to.eql(0) + expect(pidsBefore).to.eql(pidsAfter); + expect(bc.countEndedChildProcs("idle")).to.eql(0); + expect(bc.countEndedChildProcs("old")).to.eql(0); }, }, { maxProcAgeMillis: 5000, ctx: "procs should be recycled due to old age", exp: (pidsBefore: number[], pidsAfter: number[]) => { - expect(pidsBefore).to.not.have.members(pidsAfter) - expect(bc.countEndedChildProcs("idle")).to.eql(0) - expect(bc.countEndedChildProcs("old")).to.be.gte(1) + expect(pidsBefore).to.not.have.members(pidsAfter); + expect(bc.countEndedChildProcs("idle")).to.eql(0); + expect(bc.countEndedChildProcs("old")).to.be.gte(1); }, }, ]) { 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) + if (isWin && isCI) return this.skip(); + setFailratePct(0); bc = listen( new BatchCluster({ @@ -934,18 +991,18 @@ describe("BatchCluster", function () { maxProcAgeMillis, spawnTimeoutMillis: Math.max(maxProcAgeMillis, 200), processFactory, - }) - ) - assertExpectedResults(await Promise.all(runTasks(bc, 2))) - const pidsBefore = await bc.pids() - tk.freeze(start + 7000) - assertExpectedResults(await Promise.all(runTasks(bc, 2))) - const pidsAfter = await bc.pids() - console.dir({ maxProcAgeMillis, pidsBefore, pidsAfter }) - exp(pidsBefore, pidsAfter) - postAssertions() - return - }) + }), + ); + assertExpectedResults(await Promise.all(runTasks(bc, 2))); + const pidsBefore = bc.pids(); + clock.tick(7000); + assertExpectedResults(await Promise.all(runTasks(bc, 2))); + const pidsAfter = bc.pids(); + console.dir({ maxProcAgeMillis, pidsBefore, pidsAfter }); + exp(pidsBefore, pidsAfter); + postAssertions(); + return; + }); } - }) -}) + }); +}); diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index d2febfc..3c67016 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -1,66 +1,51 @@ -import child_process from "child_process" -import events from "events" -import process from "process" -import timers from "timers" -import { count, filterInPlace } from "./Array" -import { +import events from "node:events"; +import process from "node:process"; +import timers from "node:timers"; +import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter"; +import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator"; +import type { BatchClusterOptions } from "./BatchClusterOptions"; +import type { BatchClusterStats } from "./BatchClusterStats"; +import type { BatchProcessOptions } from "./BatchProcessOptions"; +import type { ChildProcessFactory } from "./ChildProcessFactory"; +import type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"; +import { Deferred } from "./Deferred"; +import { Logger } from "./Logger"; +import { verifyOptions } from "./OptionsVerifier"; +import { ProcessPoolManager } from "./ProcessPoolManager"; +import { Task } from "./Task"; +import { TaskQueueManager } from "./TaskQueueManager"; + +export { BatchClusterOptions } from "./BatchClusterOptions"; +export { BatchProcess } from "./BatchProcess"; +export { Deferred } from "./Deferred"; +export * from "./Logger"; +export { SimpleParser } from "./Parser"; +export { kill, pidExists } from "./Pids"; +export { Rate } from "./Rate"; +export { Task } from "./Task"; +// Type exports organized by source module +export type { Args } from "./Args"; +export type { BatchClusterEmitter, BatchClusterEvents, ChildEndReason, -} from "./BatchClusterEmitter" -import { - AllOpts, - BatchClusterOptions, - verifyOptions, -} from "./BatchClusterOptions" -import { BatchProcess, WhyNotHealthy, WhyNotReady } from "./BatchProcess" -import { BatchProcessOptions } from "./BatchProcessOptions" -import { Deferred } from "./Deferred" -import { asError } from "./Error" -import { Logger } from "./Logger" -import { Mean } from "./Mean" -import { fromEntries, map } from "./Object" -import { Parser } from "./Parser" -import { Rate } from "./Rate" -import { toS } from "./String" -import { Task } from "./Task" -import { thenOrTimeout, Timeout } from "./Timeout" - -export { BatchClusterOptions } from "./BatchClusterOptions" -export { BatchProcess } from "./BatchProcess" -export { Deferred } from "./Deferred" -export * from "./Logger" -export { SimpleParser } from "./Parser" -export { kill, pidExists, pids } from "./Pids" -export { Rate } from "./Rate" -export { Task } from "./Task" + TypedEventEmitter, +} from "./BatchClusterEmitter"; +export type { WithObserver } from "./BatchClusterOptions"; +export type { BatchClusterStats } from "./BatchClusterStats"; +export type { BatchProcessOptions } from "./BatchProcessOptions"; +export type { ChildProcessFactory } from "./ChildProcessFactory"; +export type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"; +export type { HealthCheckStrategy } from "./HealthCheckStrategy"; +export type { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +export type { LoggerFunction } from "./Logger"; +export type { Parser } from "./Parser"; export type { - BatchClusterEmitter, - BatchClusterEvents, - BatchProcessOptions, - ChildEndReason as ChildExitReason, - Parser, - 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 -} + HealthCheckable, + ProcessHealthMonitor, +} from "./ProcessHealthMonitor"; +export type { TaskOptions } from "./Task"; +export type { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"; /** * BatchCluster instances manage 0 or more homogeneous child processes, and @@ -73,105 +58,76 @@ 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[] = [] - #onIdleRequested = false - #nextSpawnTime = 0 - #lastPidsCheckTime = 0 - readonly #tasks: Task[] = [] - #onIdleInterval: NodeJS.Timer | undefined - readonly #startErrorRate = new Rate() - #spawnedProcs = 0 - #endPromise?: Deferred - #internalErrorCount = 0 - readonly #childEndCounts = new Map() - readonly emitter = new events.EventEmitter() as BatchClusterEmitter + readonly #logger: () => Logger; + readonly options: CombinedBatchProcessOptions; + readonly #processPool: ProcessPoolManager; + readonly #taskQueue: TaskQueueManager; + readonly #eventCoordinator: BatchClusterEventCoordinator; + #onIdleRequested = false; + #onIdleInterval: NodeJS.Timeout | undefined; + #endPromise?: Deferred; + readonly emitter = new events.EventEmitter() as BatchClusterEmitter; constructor( opts: Partial & BatchProcessOptions & - ChildProcessFactory + ChildProcessFactory, ) { - this.options = verifyOptions({ ...opts, observer: this.emitter }) - - 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() - } - }) + this.options = verifyOptions({ ...opts, observer: this.emitter }); + this.#logger = this.options.logger; + + // 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( () => this.#onIdleLater(), - this.options.onIdleIntervalMillis - ) - this.#onIdleInterval.unref() // < don't prevent node from exiting + this.options.onIdleIntervalMillis, + ); + this.#onIdleInterval.unref(); // < don't prevent node from exiting } - this.#logger = this.options.logger + this.#logger = this.options.logger; - process.once("beforeExit", this.#beforeExitListener) - process.once("exit", this.#exitListener) + process.once("beforeExit", this.#beforeExitListener); + process.once("exit", this.#exitListener); } /** * @see BatchClusterEvents */ - readonly on = this.emitter.on.bind(this.emitter) + readonly on = this.emitter.on.bind(this.emitter); /** * @see BatchClusterEvents * @since v9.0.0 */ - readonly off = this.emitter.off.bind(this.emitter) + 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 + return this.#endPromise != null; } /** @@ -181,22 +137,23 @@ export class BatchCluster { */ // NOT ASYNC so state transition happens immediately end(gracefully = true): Deferred { - this.#logger().info("BatchCluster.end()", { gracefully }) + this.#logger().info("BatchCluster.end()", { gracefully }); if (this.#endPromise == null) { - this.emitter.emit("beforeEnd") - map(this.#onIdleInterval, timers.clearInterval) - this.#onIdleInterval = undefined - process.removeListener("beforeExit", this.#beforeExitListener) - process.removeListener("exit", this.#exitListener) + this.emitter.emit("beforeEnd"); + if (this.#onIdleInterval != null) + timers.clearInterval(this.#onIdleInterval); + this.#onIdleInterval = undefined; + process.removeListener("beforeExit", this.#beforeExitListener); + process.removeListener("exit", this.#exitListener); this.#endPromise = new Deferred().observe( this.closeChildProcesses(gracefully).then(() => { - this.emitter.emit("end") - }) - ) + this.emitter.emit("end"); + }), + ); } - return this.#endPromise + return this.#endPromise; } /** @@ -208,96 +165,86 @@ export class BatchCluster { enqueueTask(task: Task): Promise { if (this.ended) { task.reject( - new Error("BatchCluster has ended, cannot enqueue " + task.command) - ) + 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 - this.#onIdleLater() + this.#onIdleLater(); // (BatchProcess will call our #onIdleLater when tasks settle or when they // exit) - return task.promise + return task.promise; } /** * @return true if all previously-enqueued tasks have settled */ get isIdle(): boolean { - return this.pendingTaskCount === 0 && this.busyProcCount === 0 + return this.pendingTaskCount === 0 && this.busyProcCount === 0; } /** * @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 + get pendingTasks(): readonly Task[] { + 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; } /** @@ -305,61 +252,46 @@ export class BatchCluster { * * @return the spawned PIDs that are still in the process table. */ - async pids(): Promise { - const arr: number[] = [] - for (const proc of [...this.#procs]) { - if (proc != null && (await proc.running())) { - arr.push(proc.pid) - } - } - return arr + pids(): number[] { + 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, ended: false === this.#endPromise?.pending, - } + }; } /** * 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); } /** @@ -368,37 +300,26 @@ 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() + this.#onIdleLater(); } readonly #onIdleLater = () => { if (!this.#onIdleRequested) { - this.#onIdleRequested = true - timers.setTimeout(() => this.#onIdle(), 1) + this.#onIdleRequested = true; + timers.setTimeout(() => this.#onIdle(), 1); } - } + }; // NOT ASYNC: updates internal state: #onIdle() { - this.#onIdleRequested = false - this.vacuumProcs() + this.#onIdleRequested = false; + 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(); } /** @@ -409,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(); } /** @@ -438,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 - } - - get #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) - } - - get #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 8f74c4d..d6d07ad 100644 --- a/src/BatchClusterEmitter.ts +++ b/src/BatchClusterEmitter.ts @@ -1,9 +1,9 @@ -import { BatchProcess, WhyNotHealthy } from "./BatchProcess" -import { Task } from "./Task" +import { Args } from "./Args"; +import { BatchProcess } from "./BatchProcess"; +import { Task } from "./Task"; +import { WhyNotHealthy } from "./WhyNotHealthy"; -type Args = T extends (...args: infer A) => void ? A : never - -export type ChildEndReason = WhyNotHealthy | "tooMany" +export type ChildEndReason = WhyNotHealthy | "tooMany"; // Type-safe EventEmitter! Note that this interface is not comprehensive: // EventEmitter has a bunch of other methods, but batch-cluster doesn't use @@ -11,22 +11,22 @@ export type ChildEndReason = WhyNotHealthy | "tooMany" export interface TypedEventEmitter { once( eventName: E, - listener: (...args: Args) => void - ): this + listener: (...args: Args) => void, + ): this; on( eventName: E, - listener: (...args: Args) => void - ): this + listener: (...args: Args) => void, + ): this; off( eventName: E, - listener: (...args: Args) => void - ): this - emit(eventName: E, ...args: Args): boolean + listener: (...args: Args) => void, + ): this; + emit(eventName: E, ...args: Args): boolean; - // eslint-disable-next-line @typescript-eslint/ban-types - listeners(event: E): Function[] + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + listeners(event: E): Function[]; - removeAllListeners(eventName?: keyof T): this + removeAllListeners(eventName?: keyof T): this; } /** @@ -39,30 +39,30 @@ export interface BatchClusterEvents { /** * Emitted when a child process has started */ - childStart: (childProcess: BatchProcess) => void + childStart: (childProcess: BatchProcess) => void; /** * Emitted when a child process has ended */ - childEnd: (childProcess: BatchProcess, reason: ChildEndReason) => void + childEnd: (childProcess: BatchProcess, reason: ChildEndReason) => void; /** * Emitted when a child process fails to spin up and run the {@link BatchProcessOptions.versionCommand} successfully within {@link BatchClusterOptions.spawnTimeoutMillis}. * * @param childProcess will be undefined if the error is from {@link ChildProcessFactory.processFactory} */ - startError: (error: Error, childProcess?: BatchProcess) => void + startError: (error: Error, childProcess?: BatchProcess) => void; /** * Emitted when an internal consistency check fails */ - internalError: (error: Error) => void + internalError: (error: Error) => void; /** * Emitted when `.end()` is called because the error rate has exceeded * {@link BatchClusterOptions.maxReasonableProcessFailuresPerMinute} */ - fatalError: (error: Error) => void + fatalError: (error: Error) => void; /** * Emitted when tasks receive data, which may be partial chunks from the task @@ -70,24 +70,28 @@ export interface BatchClusterEvents { */ taskData: ( data: Buffer | string, - task: Task | undefined, - proc: BatchProcess - ) => void + 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 @@ -96,29 +100,29 @@ export interface BatchClusterEvents { noTaskData: ( stdoutData: string | Buffer | null, stderrData: string | Buffer | null, - proc: BatchProcess - ) => void + proc: BatchProcess, + ) => void; /** * Emitted when a process fails health checks */ - healthCheckError: (error: Error, proc: BatchProcess) => void + healthCheckError: (error: Error, proc: BatchProcess) => void; /** * Emitted when a child process has an error during shutdown */ - endError: (error: Error, proc?: BatchProcess) => void + endError: (error: Error, proc?: BatchProcess) => void; /** * Emitted when this instance is in the process of ending. */ - beforeEnd: () => void + beforeEnd: () => void; /** * Emitted when this instance has ended. No child processes should remain at * this point. */ - end: () => void + end: () => void; } /** @@ -141,4 +145,4 @@ export interface BatchClusterEvents { * See {@link BatchClusterEvents} for a the list of events and their payload * signatures */ -export type BatchClusterEmitter = TypedEventEmitter +export type BatchClusterEmitter = TypedEventEmitter; diff --git a/src/BatchClusterEventCoordinator.spec.ts b/src/BatchClusterEventCoordinator.spec.ts new file mode 100644 index 0000000..ce2fece --- /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..737e531 --- /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 e8111bb..0282860 100644 --- a/src/BatchClusterOptions.spec.ts +++ b/src/BatchClusterOptions.spec.ts @@ -1,65 +1,77 @@ -import { BatchCluster } from "./BatchCluster" -import { verifyOptions } from "./BatchClusterOptions" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { expect, processFactory } from "./_chai.spec" +import { BatchCluster } from "./BatchCluster"; +import { DefaultTestOptions } from "./DefaultTestOptions.spec"; +import { verifyOptions } from "./OptionsVerifier"; +import { expect, processFactory } from "./_chai.spec"; describe("BatchClusterOptions", () => { - let bc: BatchCluster - afterEach(() => bc?.end(false)) + 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", () => { const opts = { ...DefaultTestOptions, maxProcAgeMillis: 0, - } - expect(verifyOptions(opts as any)).to.containSubset(opts) - }) + }; + expect(verifyOptions(opts as any)).to.containSubset(opts); + }); it("requires maxProcAgeMillis to be > spawnTimeoutMillis", () => { - const spawnTimeoutMillis = DefaultTestOptions.taskTimeoutMillis + 1 + const spawnTimeoutMillis = DefaultTestOptions.taskTimeoutMillis + 1; try { bc = new BatchCluster({ processFactory, ...DefaultTestOptions, spawnTimeoutMillis, maxProcAgeMillis: spawnTimeoutMillis - 1, - }) - throw new Error("expected an error due to invalid opts") + }); + throw new Error("expected an error due to invalid opts"); } catch (err) { expect(errToArr(err)).to.eql([ "Error", "BatchCluster was given invalid options", "maxProcAgeMillis must be greater than or equal to " + spawnTimeoutMillis, - `must be greater than the max value of spawnTimeoutMillis (${spawnTimeoutMillis}) and taskTimeoutMillis (${DefaultTestOptions.taskTimeoutMillis})`, - ]) + `the max value of spawnTimeoutMillis (${spawnTimeoutMillis}) and taskTimeoutMillis (${DefaultTestOptions.taskTimeoutMillis})`, + ]); } - }) + }); it("requires maxProcAgeMillis to be > taskTimeoutMillis", () => { - const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1 + const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1; try { bc = new BatchCluster({ processFactory, ...DefaultTestOptions, taskTimeoutMillis, maxProcAgeMillis: taskTimeoutMillis - 1, - }) - throw new Error("expected an error due to invalid opts") + }); + throw new Error("expected an error due to invalid opts"); } catch (err) { expect(errToArr(err)).to.eql([ "Error", "BatchCluster was given invalid options", "maxProcAgeMillis must be greater than or equal to " + taskTimeoutMillis, - `must be greater than the max value of spawnTimeoutMillis (${DefaultTestOptions.spawnTimeoutMillis}) and taskTimeoutMillis (${taskTimeoutMillis})`, - ]) + `the max value of spawnTimeoutMillis (${DefaultTestOptions.spawnTimeoutMillis}) and taskTimeoutMillis (${taskTimeoutMillis})`, + ]); } - }) + }); + + it("allows maxProcAgeMillis to be 0", () => { + const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1; + bc = new BatchCluster({ + processFactory, + ...DefaultTestOptions, + taskTimeoutMillis, + maxProcAgeMillis: 0, + }); + + expect(bc.options.maxProcAgeMillis).to.equal(0); + }); it("reports on invalid opts", () => { try { @@ -69,8 +81,6 @@ describe("BatchClusterOptions", () => { pass: "", fail: "", - spawnTimeoutMillis: 50, - taskTimeoutMillis: 5, maxTasksPerProcess: 0, minDelayBetweenSpawnMillis: -1, @@ -80,8 +90,8 @@ describe("BatchClusterOptions", () => { onIdleIntervalMillis: -1, endGracefulWaitTimeMillis: -1, streamFlushMillis: -1, - }) - throw new Error("expected an error due to invalid opts") + }); + throw new Error("expected an error due to invalid opts"); } catch (err) { expect(errToArr(err)).to.eql([ "Error", @@ -89,19 +99,18 @@ describe("BatchClusterOptions", () => { "versionCommand must not be blank", "pass must not be blank", "fail must not be blank", - "spawnTimeoutMillis must be greater than or equal to 100", - "taskTimeoutMillis must be greater than or equal to 10", "maxTasksPerProcess must be greater than or equal to 1", "maxProcs must be greater than or equal to 1", - "maxProcAgeMillis must be greater than or equal to 50", - "must be greater than the max value of spawnTimeoutMillis (50) and taskTimeoutMillis (5)", + "maxProcAgeMillis must be greater than or equal to 15000", + // DON'T PANIC: this is just a continuation of the previous error message. + "the max value of spawnTimeoutMillis (15000) and taskTimeoutMillis (10000)", "minDelayBetweenSpawnMillis must be greater than or equal to 0", "onIdleIntervalMillis must be greater than or equal to 0", "endGracefulWaitTimeMillis must be greater than or equal to 0", "maxReasonableProcessFailuresPerMinute must be greater than or equal to 0", "streamFlushMillis must be greater than or equal to 0", - ]) + ]); } - }) - }) -}) + }); + }); +}); diff --git a/src/BatchClusterOptions.ts b/src/BatchClusterOptions.ts index 812a2b1..f81ffc8 100644 --- a/src/BatchClusterOptions.ts +++ b/src/BatchClusterOptions.ts @@ -1,13 +1,9 @@ -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" +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { logger, Logger } from "./Logger"; +import { isMac, isWin } from "./Platform"; -export const secondMs = 1000 -export const minuteMs = 60 * secondMs +export const secondMs = 1000; +export const minuteMs = 60 * secondMs; /** * These parameter values have somewhat sensible defaults, but can be @@ -20,28 +16,26 @@ export class BatchClusterOptions { * * Defaults to 1. */ - maxProcs = 1 + maxProcs = 1; /** * Child processes will be recycled when they reach this age. * - * If this value is set to 0, child processes will not "age out". - * * This value must not be less than `spawnTimeoutMillis` or * `taskTimeoutMillis`. * - * Defaults to 5 minutes. + * Defaults to 5 minutes. Set to 0 to disable. */ - maxProcAgeMillis = 5 * minuteMs + maxProcAgeMillis = 5 * minuteMs; /** - * This is the minimum interval between calls to {@link BatchCluster.#onIdle}, - * which runs general janitorial processes like child process management and - * task queue validation. + * This is the minimum interval between calls to BatchCluster's #onIdle + * method, which runs general janitorial processes like child process + * management and task queue validation. * * Must be > 0. Defaults to 10 seconds. */ - onIdleIntervalMillis = 10 * secondMs + onIdleIntervalMillis = 10 * secondMs; /** * If the initial `versionCommand` fails for new spawned processes more @@ -51,9 +45,9 @@ export class BatchClusterOptions { * If this backstop didn't exist, new (failing) child processes would be * created indefinitely. * - * Set to 0 to disable. Defaults to 10. + * Defaults to 10. Set to 0 to disable. */ - maxReasonableProcessFailuresPerMinute = 10 + maxReasonableProcessFailuresPerMinute = 10; /** * Spawning new child processes and servicing a "version" task must not take @@ -61,9 +55,9 @@ export class BatchClusterOptions { * and need to be restarted. Be pessimistic here--windows can regularly take * several seconds to spin up a process, thanks to antivirus shenanigans. * - * Must be >= 100ms. Defaults to 15 seconds. + * Defaults to 15 seconds. Set to 0 to disable. */ - spawnTimeoutMillis = 15 * secondMs + spawnTimeoutMillis = 15 * secondMs; /** * If maxProcs > 1, spawning new child processes to process tasks can slow @@ -71,7 +65,7 @@ export class BatchClusterOptions { * * Must be >= 0ms. Defaults to 1.5 seconds. */ - minDelayBetweenSpawnMillis = 1.5 * secondMs + minDelayBetweenSpawnMillis = 1.5 * secondMs; /** * If commands take longer than this, presume the underlying process is dead @@ -79,9 +73,9 @@ export class BatchClusterOptions { * * This should be set to something on the order of seconds. * - * Must be >= 10ms. Defaults to 10 seconds. + * Defaults to 10 seconds. Set to 0 to disable. */ - taskTimeoutMillis = 10 * secondMs + taskTimeoutMillis = 10 * secondMs; /** * Processes will be recycled after processing `maxTasksPerProcess` tasks. @@ -94,7 +88,7 @@ export class BatchClusterOptions { * * Must be >= 0. Defaults to 500 */ - maxTasksPerProcess = 500 + maxTasksPerProcess = 500; /** * When `this.end()` is called, or Node broadcasts the `beforeExit` event, @@ -105,7 +99,7 @@ export class BatchClusterOptions { * kill signal to shut down. Any pending requests may be interrupted. Must be * >= 0. Defaults to 500ms. */ - endGracefulWaitTimeMillis = 500 + endGracefulWaitTimeMillis = 500; /** * When a task sees a "pass" or "fail" from either stdout or stderr, it needs @@ -129,7 +123,7 @@ export class BatchClusterOptions { */ // These values were found by trial and error using GitHub CI boxes, which // should be the bottom of the barrel, performance-wise, of any computer. - streamFlushMillis = isMac ? 100 : isWin ? 200 : 30 + streamFlushMillis = isMac ? 100 : isWin ? 200 : 30; /** * Should batch-cluster try to clean up after spawned processes that don't @@ -139,7 +133,7 @@ export class BatchClusterOptions { * * Defaults to `true`. */ - cleanupChildProcs = true + cleanupChildProcs = true; /** * If a child process is idle for more than this value (in milliseconds), shut @@ -148,7 +142,7 @@ export class BatchClusterOptions { * A value of ~10 seconds to a couple minutes would be reasonable. Set this to * 0 to disable this feature. */ - maxIdleMsPerProcess = 0 + maxIdleMsPerProcess = 0; /** * How many failed tasks should a process be allowed to process before it is @@ -156,7 +150,7 @@ export class BatchClusterOptions { * * Set this to 0 to disable this feature. */ - maxFailedTasksPerProcess = 2 + maxFailedTasksPerProcess = 2; /** * If `healthCheckCommand` is set, how frequently should we check for @@ -164,105 +158,22 @@ export class BatchClusterOptions { * * Set this to 0 to disable this feature. */ - healthCheckIntervalMillis = 0 + healthCheckIntervalMillis = 0; /** * Verify child processes are still running by checking the OS process table. * * Set this to 0 to disable this feature. */ - pidCheckIntervalMillis = 2 * minuteMs + pidCheckIntervalMillis = 2 * minuteMs; /** * A BatchCluster instance and associated BatchProcess instances will share * this `Logger`. Defaults to the `Logger` instance provided to `setLogger()`. */ - logger: () => Logger = logger + logger: () => Logger = logger; } 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) { - errors.push( - fieldName + - " must be greater than or equal to " + - value + - (why == null ? "" : ": " + why) - ) - } - } - - notBlank("versionCommand") - notBlank("pass") - notBlank("fail") - - gte("spawnTimeoutMillis", 100) - gte("taskTimeoutMillis", 10) - gte("maxTasksPerProcess", 1) - - gte("maxProcs", 1) - - if (opts.maxProcAgeMillis != null && opts.maxProcAgeMillis > 0) { - gte( - "maxProcAgeMillis", - Math.max(result.spawnTimeoutMillis, result.taskTimeoutMillis), - `must be greater than 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) - // 0 disables: - gte("streamFlushMillis", 0) - - if (errors.length > 0) { - throw new Error( - "BatchCluster was given invalid options: " + errors.join("; ") - ) - } - - return result + observer: BatchClusterEmitter; } diff --git a/src/BatchClusterStats.ts b/src/BatchClusterStats.ts new file mode 100644 index 0000000..7257874 --- /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 9ebfb47..0c35c30 100644 --- a/src/BatchProcess.ts +++ b/src/BatchProcess.ts @@ -1,76 +1,76 @@ -import child_process from "child_process" -import timers from "timers" -import { until } from "./Async" -import { Deferred } from "./Deferred" -import { cleanError, tryEach } from "./Error" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { Logger } from "./Logger" -import { map } from "./Object" -import { SimpleParser } from "./Parser" -import { kill, pidExists } from "./Pids" -import { mapNotDestroyed } from "./Stream" -import { blank, 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 child_process from "node:child_process"; +import timers from "node:timers"; +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 { 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 { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"; /** * BatchProcess manages the care and feeding of a single child process. */ export class BatchProcess { - readonly name: string - readonly pid: number - readonly start = Date.now() - #lastHealthCheck = Date.now() - #healthCheckFailures = 0 + readonly name: string; + readonly pid: number; + readonly start = Date.now(); - readonly startupTaskId: number - readonly #logger: () => Logger - #lastJobFinshedAt = Date.now() - #lastJobFailed = false + readonly startupTaskId: number; + readonly #logger: () => Logger; + readonly #terminator: ProcessTerminator; + readonly #healthMonitor: ProcessHealthMonitor; + readonly #streamHandler: StreamHandler; + #lastJobFinishedAt = Date.now(); // Only set to true when `proc.pid` is no longer in the process table. - #starting = true + #starting = true; - // pidExists() returned false - #exited = false + // Deferred that resolves when the process exits (via OS events) + #processExitDeferred = new Deferred(); // override for .whyNotHealthy() - #whyNotHealthy?: WhyNotHealthy + #whyNotHealthy?: WhyNotHealthy; - failedTaskCount = 0 + failedTaskCount = 0; - #taskCount = -1 // don't count the startupTask + #taskCount = -1; // don't count the startupTask /** * Should be undefined if this instance is not currently processing a task. */ - #currentTask: Task | undefined - #currentTaskTimeout: NodeJS.Timer | undefined + #currentTask: Task | undefined; - #endPromise: undefined | Deferred + /** + * 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; /** * @param onIdle to be called when internal state changes (like the current @@ -79,62 +79,71 @@ export class BatchProcess { constructor( readonly proc: child_process.ChildProcess, readonly opts: InternalBatchProcessOptions, - private readonly onIdle: () => void + private readonly onIdle: () => void, + healthMonitor?: ProcessHealthMonitor, ) { - this.name = "BatchProcess(" + proc.pid + ")" - this.#logger = opts.logger + 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() + this.proc.unref(); if (proc.pid == null) { - throw new Error("BatchProcess.constructor: child process pid is null") + throw new Error("BatchProcess.constructor: child process pid is null"); } - 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)) - }) - - const startupTask = new Task(opts.versionCommand, SimpleParser) - this.startupTaskId = startupTask.taskId + this.pid = proc.pid; + + this.proc.on("error", (err) => this.#onError("proc.error", err)); + this.proc.on("close", () => { + this.#processExitDeferred.resolve(); + void this.end(false, "proc.close"); + }); + this.proc.on("exit", () => { + this.#processExitDeferred.resolve(); + void this.end(false, "proc.exit"); + }); + this.proc.on("disconnect", () => { + this.#processExitDeferred.resolve(); + 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; if (!this.execTask(startupTask)) { this.opts.observer.emit( "internalError", - new Error(this.name + " startup task was not submitted") - ) + 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 + this.opts.observer.emit("childStart", this); } get taskCount(): number { - return this.#taskCount + return this.#taskCount; } get starting(): boolean { - return this.#starting + return this.#starting; } /** @@ -142,27 +151,26 @@ export class BatchProcess { * child process exiting) */ get ending(): boolean { - return this.#endPromise != null + return this.#endPromise != null; } /** * @return true if `this.end()` has completed running, which includes child * process cleanup. Note that this may return `true` and the process table may - * still include the child pid. Call {@link .running()} for an authoritative + * still include the child pid. Call {@link BatchProcess#running()} for an authoritative * (but expensive!) answer. */ get ended(): boolean { - return true === this.#endPromise?.settled + return true === this.#endPromise?.settled; } /** - * @return true if the child process has exited and is no longer in the - * process table. Note that this may be erroneously false if the process table - * hasn't been checked. Call {@link .running()} for an authoritative (but - * expensive!) answer. + * @return true if the child process has exited (based on OS events). + * This is now authoritative and inexpensive since it's driven by OS events + * rather than polling. */ get exited(): boolean { - return this.#exited + return this.#processExitDeferred.settled; } /** @@ -172,50 +180,14 @@ 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); } /** * @return true if the process doesn't need to be recycled. */ get healthy(): boolean { - return this.whyNotHealthy == null + return this.whyNotHealthy == null; } /** @@ -223,7 +195,7 @@ export class BatchProcess { * process has ended or should be recycled: see {@link BatchProcess.ready}. */ get idle(): boolean { - return this.#currentTask == null + return this.#currentTask == null; } /** @@ -231,7 +203,7 @@ export class BatchProcess { * task, or `undefined` if this process is idle and healthy. */ get whyNotReady(): WhyNotReady | null { - return !this.idle ? "busy" : this.whyNotHealthy + return !this.idle ? "busy" : this.whyNotHealthy; } /** @@ -239,137 +211,122 @@ export class BatchProcess { * new task. */ get ready(): boolean { - return this.whyNotReady == null + return this.whyNotReady == null; } get idleMs(): number { - return this.idle ? Date.now() - this.#lastJobFinshedAt : -1 + return this.idle ? Date.now() - this.#lastJobFinishedAt : -1; } /** - * @return true if the child process is in the process table + * @return true if the child process is running. + * Now event-driven first with polling fallback. */ - async running(): Promise { - if (this.#exited) return false + running(): boolean { + // If we've been notified via OS events that process exited, trust that immediately + if (this.exited) return false; - const alive = await pidExists(this.pid) + // Only poll as fallback if we haven't been notified yet + // This handles edge cases where events might not fire reliably + const alive = pidExists(this.pid); if (!alive) { - this.#exited = true + this.#processExitDeferred.resolve(); // 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 + return alive; } - notRunning(): Promise { - return this.running().then((ea) => !ea) + notRunning(): boolean { + 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 { - return this.ready ? this.#execTask(task) : false + execTask(task: Task): boolean { + return this.ready ? this.#execTask(task) : false; } - #execTask(task: Task): boolean { - if (this.ending) return false + #execTask(task: Task): boolean { + if (this.ending) return false; - this.#taskCount++ - this.#currentTask = task - const cmd = ensureSuffix(task.command, "\n") - const isStartupTask = task.taskId === this.startupTaskId + this.#taskCount++; + this.#currentTask = task as Task; + const cmd = ensureSuffix(task.command, "\n"); + const isStartupTask = task.taskId === this.startupTaskId; const taskTimeoutMs = isStartupTask ? this.opts.spawnTimeoutMillis - : this.opts.taskTimeoutMillis + : this.opts.taskTimeoutMillis; if (taskTimeoutMs > 0) { // 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), - taskTimeoutMs + this.opts.streamFlushMillis - ) + () => this.#onTimeout(task as Task, taskTimeoutMs), + taskTimeoutMs + this.opts.streamFlushMillis, + ); } // CAREFUL! If you add a .catch or .finally, the pipeline can emit unhandled // 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 + 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() + 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: - this.onIdle() - } - ) + this.onIdle(); + }, + ); try { - task.onStart(this.opts) - const stdin = this.proc?.stdin + task.onStart(this.opts); + const stdin = this.proc?.stdin; if (stdin == null || stdin.destroyed) { - task.reject(new Error("proc.stdin unexpectedly closed")) - return false + task.reject(new Error("proc.stdin unexpectedly closed")); + return false; } else { stdin.write(cmd, (err) => { if (err != null) { - task.reject(err) + task.reject(err); } - }) - return true + }); + return true; } - } catch (err) { + } catch { // child process went away. We should too. - this.end(false, "stdin.error") - return false + void this.end(false, "stdin.error"); + return false; } } @@ -386,181 +343,97 @@ export class BatchProcess { // NOT ASYNC! needs to change state immediately. end(gracefully = true, reason: WhyNotHealthy): Promise { return (this.#endPromise ??= new Deferred().observe( - this.#end(gracefully, (this.#whyNotHealthy ??= reason)) - )).promise + this.#end(gracefully, (this.#whyNotHealthy ??= reason)), + )).promise; } // NOTE: Must only be invoked by this.end(), and only expected to be invoked // once per instance. async #end(gracefully: boolean, reason: WhyNotHealthy) { - 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, - })})` - ) - ) - } - } - - const cmd = map(this.opts.exitCommand, (ea) => ensureSuffix(ea, "\n")) - - // proc cleanup: - tryEach([ - () => mapNotDestroyed(this.proc.stdin, (ea) => ea.end(cmd)), - () => mapNotDestroyed(this.proc.stdout, (ea) => ea.destroy()), - () => mapNotDestroyed(this.proc.stderr, (ea) => ea.destroy()), - () => this.proc.disconnect(), - ]) - - if ( - this.opts.cleanupChildProcs && - gracefully && - this.opts.endGracefulWaitTimeMillis > 0 && - !this.#exited - ) { - // Wait for the end command to take effect: - await this.#awaitNotRunning(this.opts.endGracefulWaitTimeMillis / 2) - // If it's still running, send the pid a signal: - if ((await this.running()) && this.proc.pid != null) - await kill(this.proc.pid) - // Wait for the signal handler to work: - await this.#awaitNotRunning(this.opts.endGracefulWaitTimeMillis / 2) - } - - if ( - this.opts.cleanupChildProcs && - this.proc.pid != null && - (await this.running()) - ) { - this.#logger().warn( - this.name + ".end(): force-killing still-running child." - ) - await kill(this.proc.pid, true) - } - this.opts.observer.emit("childEnd", this, reason) + const lastTask = this.#currentTask; + this.#clearCurrentTask(); + + await this.#terminator.terminate( + this.proc, + this.name, + lastTask, + this.startupTaskId, + gracefully, + this.exited, + () => this.running(), + ); + + // Clean up health monitoring for this process + this.#healthMonitor.cleanupProcess(this.pid); + + 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) + this.opts.observer.emit("taskTimeout", timeoutMs, task, this); + this.#onError("timeout", new Error("waited " + timeoutMs + "ms"), task); } } - #onError(reason: WhyNotHealthy, error: Error, task?: Task) { - if (this.ending) { - // We're ending already, so don't propagate the error. - // This is expected due to race conditions stdin EPIPE and process shutdown. - this.#logger().debug( - this.name + ".onError() post-end (expected and not propagated)", - { - reason, - error, - task, - } - ) - return - } + #onError(reason: WhyNotHealthy, error: Error, task?: Task) { if (task == null) { - task = this.#currentTask + task = this.#currentTask; + } + const cleanedError = new Error(reason + ": " + cleanError(error.message)); + if (error.stack != null) { + // Error stacks, if set, will not be redefined from a rethrow: + cleanedError.stack = cleanError(error.stack); } - const cleanedError = new Error(reason + ": " + cleanError(error.message)) this.#logger().warn(this.name + ".onError()", { reason, task: map(task, (t) => t.command), error: cleanedError, - }) + }); - if (error.stack != null) { - // Error stacks, if set, will not be redefined from a rethrow: - cleanedError.stack = cleanError(error.stack) + if (this.ending) { + // .#end is already disconnecting the error listeners, but in any event, + // we don't really care about errors after we've been told to shut down. + return; } // clear the task before ending so the onExit from end() doesn't retry the task: - this.#clearCurrentTask() - void this.end(false, reason) + this.#clearCurrentTask(); + void this.end(false, reason); if (task != null && this.taskCount === 1) { this.#logger().warn( - this.name + ".onError(): startup task failed: " + cleanedError - ) - this.opts.observer.emit("startError", cleanedError) + this.name + ".onError(): startup task failed: " + String(cleanedError), + ); + this.opts.observer.emit("startError", cleanedError); } if (task != null) { if (task.pending) { - task.reject(cleanedError) + task.reject(cleanedError); } else { this.opts.observer.emit( "internalError", new Error( - `${this.name}.onError(${cleanedError}) cannot reject already-fulfilled task.` - ) - ) + `${this.name}.onError(${cleanedError}) cannot reject already-fulfilled task.`, + ), + ); } } } - #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 - this.#currentTask = undefined - this.#lastJobFinshedAt = Date.now() + if (task != null && task.taskId !== this.#currentTask?.taskId) return; + map(this.#currentTaskTimeout, (ea) => clearTimeout(ea)); + this.#currentTaskTimeout = undefined; + this.#currentTask = undefined; + this.#lastJobFinishedAt = Date.now(); } } diff --git a/src/BatchProcessOptions.ts b/src/BatchProcessOptions.ts index 220eca8..fcc021e 100644 --- a/src/BatchProcessOptions.ts +++ b/src/BatchProcessOptions.ts @@ -10,7 +10,7 @@ export interface BatchProcessOptions { * be invoked immediately after spawn. This command must return before any * tasks will be given to a given process. */ - versionCommand: string + versionCommand: string; /** * If provided, and healthCheckIntervalMillis is greater than 0, or the @@ -19,19 +19,19 @@ export interface BatchProcessOptions { * If the command outputs to stderr or returns a fail string, the process will * be considered unhealthy and recycled. */ - healthCheckCommand?: string | undefined + healthCheckCommand?: string | undefined; /** * Expected text to print if a command passes. Cannot be blank. Strings will * be interpreted as a regular expression fragment. */ - pass: string | RegExp + pass: string | RegExp; /** * Expected text to print if a command fails. Cannot be blank. Strings will * be interpreted as a regular expression fragment. */ - fail: string | RegExp + fail: string | RegExp; /** * Command to end the child batch process. If not provided (or undefined), @@ -39,5 +39,5 @@ export interface BatchProcessOptions { * and if it does not shut down within `endGracefulWaitTimeMillis`, it will be * SIGHUP'ed. */ - exitCommand?: string | undefined + exitCommand?: string | undefined; } diff --git a/src/ChildProcessFactory.ts b/src/ChildProcessFactory.ts new file mode 100644 index 0000000..fdb1ada --- /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..e3cd3e8 --- /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/DefaultTestOptions.spec.ts b/src/DefaultTestOptions.spec.ts index 4fe6eab..63a51e2 100644 --- a/src/DefaultTestOptions.spec.ts +++ b/src/DefaultTestOptions.spec.ts @@ -1,6 +1,6 @@ -import { BatchClusterOptions } from "./BatchClusterOptions" +import { BatchClusterOptions } from "./BatchClusterOptions"; -const bco = new BatchClusterOptions() +const bco = new BatchClusterOptions(); export const DefaultTestOptions = { ...bco, @@ -18,4 +18,4 @@ export const DefaultTestOptions = { // we shouldn't need these overrides... // ...(isCI ? { streamFlushMillis: bco.streamFlushMillis * 3 } : {}), // onIdleIntervalMillis: 1000, -} +}; diff --git a/src/Deferred.spec.ts b/src/Deferred.spec.ts index 39347ef..5c84f4b 100644 --- a/src/Deferred.spec.ts +++ b/src/Deferred.spec.ts @@ -1,67 +1,67 @@ -import { Deferred } from "./Deferred" -import { expect } from "./_chai.spec" +import { Deferred } from "./Deferred"; +import { expect } from "./_chai.spec"; describe("Deferred", () => { it("is born pending", () => { - const d = new Deferred() - expect(d.pending).to.eql(true) - expect(d.fulfilled).to.eql(false) - expect(d.rejected).to.eql(false) - }) + const d = new Deferred(); + expect(d.pending).to.eql(true); + expect(d.fulfilled).to.eql(false); + expect(d.rejected).to.eql(false); + }); it("resolves out of pending", () => { - const d = new Deferred() - const expected = "result" - d.resolve(expected) - expect(d.pending).to.eql(false) - expect(d.fulfilled).to.eql(true) - expect(d.rejected).to.eql(false) - return expect(d).to.become(expected) - }) + const d = new Deferred(); + const expected = "result"; + d.resolve(expected); + expect(d.pending).to.eql(false); + expect(d.fulfilled).to.eql(true); + expect(d.rejected).to.eql(false); + return expect(d).to.become(expected); + }); it("rejects out of pending", () => { - const d = new Deferred() - expect(d.reject("boom")).to.eql(true) - expect(d.pending).to.eql(false) - expect(d.fulfilled).to.eql(false) - expect(d.rejected).to.eql(true) - return expect(d).to.eventually.be.rejectedWith(/boom/) - }) + const d = new Deferred(); + expect(d.reject("boom")).to.eql(true); + expect(d.pending).to.eql(false); + expect(d.fulfilled).to.eql(false); + expect(d.rejected).to.eql(true); + return expect(d).to.eventually.be.rejectedWith(/boom/); + }); it("resolved ignores subsequent resolutions", () => { - const d = new Deferred() - expect(d.resolve(123)).to.eql(true) - expect(d.resolve(456)).to.eql(false) - expect(d.pending).to.eql(false) - expect(d.fulfilled).to.eql(true) - expect(d.rejected).to.eql(false) - return expect(d).to.become(123) - }) + const d = new Deferred(); + expect(d.resolve(123)).to.eql(true); + expect(d.resolve(456)).to.eql(false); + expect(d.pending).to.eql(false); + expect(d.fulfilled).to.eql(true); + expect(d.rejected).to.eql(false); + return expect(d).to.become(123); + }); it("resolved respects subsequent rejections", () => { - const d = new Deferred() - expect(d.resolve(123)).to.eql(true) - expect(d.reject("boom")).to.eql(false) - expect(d.pending).to.eql(false) + const d = new Deferred(); + expect(d.resolve(123)).to.eql(true); + expect(d.reject("boom")).to.eql(false); + expect(d.pending).to.eql(false); // CAUTION: THIS IS WEIRD. The promise is resolved, but something later // wanted to reject, so we assume the rejected state, even though we can't // reach back in the promise chain and un-resolve the promise. - expect(d.fulfilled).to.eql(false) - expect(d.rejected).to.eql(true) - return expect(d).to.become(123) - }) + expect(d.fulfilled).to.eql(false); + expect(d.rejected).to.eql(true); + return expect(d).to.become(123); + }); it("rejected ignores subsequent resolutions", () => { - const d = new Deferred() - expect(d.reject("first boom")).to.eql(true) - expect(d.resolve(456)).to.eql(false) - return expect(d).to.eventually.be.rejectedWith(/first boom/) - }) + const d = new Deferred(); + expect(d.reject("first boom")).to.eql(true); + expect(d.resolve(456)).to.eql(false); + return expect(d).to.eventually.be.rejectedWith(/first boom/); + }); it("rejected ignores subsequent rejections", () => { - const d = new Deferred() - expect(d.reject("first boom")).to.eql(true) - expect(d.reject("second boom")).to.eql(false) - return expect(d).to.eventually.be.rejectedWith(/first boom/) - }) -}) + const d = new Deferred(); + expect(d.reject("first boom")).to.eql(true); + expect(d.reject("second boom")).to.eql(false); + return expect(d).to.eventually.be.rejectedWith(/first boom/); + }); +}); diff --git a/src/Deferred.ts b/src/Deferred.ts index d4fbbff..f62e9f6 100644 --- a/src/Deferred.ts +++ b/src/Deferred.ts @@ -13,116 +13,107 @@ enum State { * `fulfilled`, or `rejected` state of the promise. */ export class Deferred implements PromiseLike { - readonly [Symbol.toStringTag] = "Deferred" - readonly promise: Promise - #resolve!: (value: T | PromiseLike) => void - #reject!: (reason?: any) => void - #state: State = State.pending + readonly [Symbol.toStringTag] = "Deferred"; + readonly promise: Promise; + #resolve!: (value: T | PromiseLike) => void; + #reject!: (reason?: unknown) => void; + #state: State = State.pending; constructor() { this.promise = new Promise((resolve, reject) => { - this.#resolve = resolve - this.#reject = reject - }) + this.#resolve = resolve; + this.#reject = reject; + }); } /** * @return `true` iff neither `resolve` nor `rejected` have been invoked */ get pending(): boolean { - return this.#state === State.pending + return this.#state === State.pending; } /** * @return `true` iff `resolve` has been invoked */ get fulfilled(): boolean { - return this.#state === State.fulfilled + return this.#state === State.fulfilled; } /** * @return `true` iff `rejected` has been invoked */ get rejected(): boolean { - return this.#state === State.rejected + return this.#state === State.rejected; } /** * @return `true` iff `resolve` or `rejected` have been invoked */ get settled(): boolean { - return this.#state !== State.pending + return this.#state !== State.pending; } 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) + 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) + return this.promise.catch(onrejected); } resolve(value: T): boolean { if (this.settled) { - return false + return false; } else { - this.#state = State.fulfilled - this.#resolve(value) - return true + this.#state = State.fulfilled; + this.#resolve(value); + return true; } } reject(reason?: Error | string): boolean { - const wasSettled = this.settled + const wasSettled = this.settled; // This isn't great: the wrapped Promise may be in a different state than // #state: but the caller wanted to reject, so even if it already was // resolved, let's try to respect that. - this.#state = State.rejected + this.#state = State.rejected; if (wasSettled) { - return false + return false; } else { - this.#reject(reason) - return true + this.#reject(reason); + return true; } } observe(p: Promise): this { - void observe(this, p) - return this + void observe(this, p); + return this; } observeQuietly(p: Promise): Deferred { - void observeQuietly(this, p) - return this as any + void observeQuietly(this, p); + return this as Deferred; } } async function observe(d: Deferred, p: Promise) { try { - d.resolve(await p) - } catch (err: any) { - d.reject(err) + d.resolve(await p); + } catch (err: unknown) { + d.reject(err instanceof Error ? err : new Error(String(err))); } } async function observeQuietly(d: Deferred, p: Promise) { try { - d.resolve(await p) + 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..2af1c89 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 @@ -7,21 +7,29 @@ import { blank, toS } from "./String" export function tryEach(arr: (() => void)[]): void { for (const f of arr) { try { - f() - } catch (_) { + f(); + } catch { // } } } -export function cleanError(s: any): string { +export function cleanError(s: unknown): string { return String(s) .trim() - .replace(/^error: /i, "") + .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..8360e53 --- /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/InternalBatchProcessOptions.ts b/src/InternalBatchProcessOptions.ts index 284e34c..deaf07d 100644 --- a/src/InternalBatchProcessOptions.ts +++ b/src/InternalBatchProcessOptions.ts @@ -1,10 +1,10 @@ -import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" -import { BatchProcessOptions } from "./BatchProcessOptions" +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"; +import { BatchProcessOptions } from "./BatchProcessOptions"; export interface InternalBatchProcessOptions extends BatchProcessOptions, BatchClusterOptions, WithObserver { - passRE: RegExp - failRE: RegExp + passRE: RegExp; + failRE: RegExp; } diff --git a/src/Logger.ts b/src/Logger.ts index 32182c0..ea72f7f 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,18 +1,21 @@ -import util from "util" -import { map } from "./Object" -import { notBlank } from "./String" +import util from "node:util"; +import { map } from "./Object"; +import { notBlank } from "./String"; -type LogFunc = (message: string, ...optionalParams: any[]) => void +export type LoggerFunction = ( + message: string, + ...optionalParams: unknown[] +) => void; /** * Simple interface for logging. */ export interface Logger { - trace: LogFunc - debug: LogFunc - info: LogFunc - warn: LogFunc - error: LogFunc + trace: LoggerFunction; + debug: LoggerFunction; + info: LoggerFunction; + warn: LoggerFunction; + error: LoggerFunction; } export const LogLevels: (keyof Logger)[] = [ @@ -21,16 +24,16 @@ export const LogLevels: (keyof Logger)[] = [ "info", "warn", "error", -] +]; -const _debuglog = util.debuglog("batch-cluster") +const _debuglog = util.debuglog("batch-cluster"); -const noop = () => undefined +const noop = () => undefined; /** * Default `Logger` implementation. * - * - `debug` and `info` go to {@link util.debuglog}("batch-cluster")`. + * - `debug` and `info` go to `util.debuglog("batch-cluster")`. * * - `warn` and `error` go to `console.warn` and `console.error`. * @@ -56,12 +59,18 @@ 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); + }, +}); /** * `Logger` that disables all logging. @@ -72,57 +81,60 @@ export const NoLogger: Logger = Object.freeze({ info: noop, warn: noop, 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 + _logger = l; } export function logger(): Logger { - return _logger + return _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) + const prefix = (ea + " ").substring(0, 5) + " | "; + timestamped[ea] = (message?: unknown, ...optionalParams: unknown[]) => { + if (notBlank(String(message))) { + delegate[ea](prefix + String(message), ...optionalParams); } - } - }) - return timestamped + }; + }); + return timestamped; }, 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, - ...optionalParams - ) - )) - ) - return timestamped + new Date().toISOString() + " | " + String(ea), + ...optionalParams, + ), + )), + ); + return timestamped; }, filterLevels: (l: Logger, minLogLevel: keyof Logger) => { - const minLogLevelIndex = LogLevels.indexOf(minLogLevel) - const filtered: any = {} + const minLogLevelIndex = LogLevels.indexOf(minLogLevel); + const filtered: Logger = {} as Logger; LogLevels.forEach( (ea, idx) => - (filtered[ea] = idx < minLogLevelIndex ? noop : l[ea].bind(l)) - ) - return filtered + (filtered[ea] = idx < minLogLevelIndex ? noop : l[ea].bind(l)), + ); + return filtered; }, -} +}; diff --git a/src/Mean.ts b/src/Mean.ts index b87f47a..0ee0204 100644 --- a/src/Mean.ts +++ b/src/Mean.ts @@ -1,36 +1,39 @@ export class Mean { - private _n: number - private _min?: number = undefined - private _max?: number = undefined + private _n: number; + private _min?: number = undefined; + private _max?: number = undefined; - constructor(n = 0, private sum = 0) { - this._n = n + constructor( + n = 0, + private sum = 0, + ) { + this._n = n; } push(x: number): void { - this._n++ - this.sum += x - this._min = this._min == null || this._min > x ? x : this._min - this._max = this._max == null || this._max < x ? x : this._max + this._n++; + this.sum += x; + this._min = this._min == null || this._min > x ? x : this._min; + this._max = this._max == null || this._max < x ? x : this._max; } get n(): number { - return this._n + return this._n; } get min(): number | undefined { - return this._min + return this._min; } get max(): number | undefined { - return this._max + return this._max; } get mean(): number { - return this.sum / this.n + return this.sum / this.n; } clone(): Mean { - return new Mean(this.n, this.sum) + return new Mean(this.n, this.sum); } } diff --git a/src/Mutex.ts b/src/Mutex.ts index a572c14..9085bdc 100644 --- a/src/Mutex.ts +++ b/src/Mutex.ts @@ -1,35 +1,35 @@ -import { filterInPlace } from "./Array" -import { Deferred } from "./Deferred" +import { filterInPlace } from "./Array"; +import { Deferred } from "./Deferred"; /** * Aggregate promises efficiently */ export class Mutex { - private _pushCount = 0 - private readonly _arr: Deferred[] = [] + private _pushCount = 0; + private readonly _arr: Deferred[] = []; private get arr() { - filterInPlace(this._arr, (ea) => ea.pending) - return this._arr + filterInPlace(this._arr, (ea) => ea.pending); + return this._arr; } get pushCount(): number { - return this._pushCount + return this._pushCount; } push(f: () => Promise): Promise { - this._pushCount++ - const p = f() + this._pushCount++; + const p = f(); // Don't cause awaitAll to die if a task rejects: - this.arr.push(new Deferred().observeQuietly(p)) - return p + this.arr.push(new Deferred().observeQuietly(p)); + return p; } /** * Run f() after all prior-enqueued promises have resolved. */ serial(f: () => Promise): Promise { - return this.push(() => this.awaitAll().then(() => f())) + return this.push(() => this.awaitAll().then(() => f())); } /** @@ -37,21 +37,21 @@ export class Mutex { * all pending have resolved. */ runIfIdle(f: () => Promise): undefined | Promise { - return this.pending ? undefined : this.serial(f) + return this.pending ? undefined : this.serial(f); } get pendingCount(): number { // Don't need vacuuming, so we can use this._arr: - return this._arr.reduce((sum, ea) => sum + (ea.pending ? 1 : 0), 0) + return this._arr.reduce((sum, ea) => sum + (ea.pending ? 1 : 0), 0); } get pending(): boolean { - return this.pendingCount > 0 + return this.pendingCount > 0; } get settled(): boolean { // this.arr is a getter that does vacuuming - return this.arr.length === 0 + return this.arr.length === 0; } /** @@ -61,6 +61,6 @@ export class Mutex { awaitAll(): Promise { return this.arr.length === 0 ? Promise.resolve(undefined) - : Promise.all(this.arr.map((ea) => ea.promise)).then(() => undefined) + : Promise.all(this.arr.map((ea) => ea.promise)).then(() => undefined); } } diff --git a/src/Object.spec.ts b/src/Object.spec.ts index 3a0ddfb..2cc614f 100644 --- a/src/Object.spec.ts +++ b/src/Object.spec.ts @@ -1,24 +1,24 @@ -import { expect } from "./_chai.spec" -import { map } from "./Object" +import { map } from "./Object"; +import { expect } from "./_chai.spec"; describe("Object", () => { describe("map()", () => { it("skips if target is null", () => { expect( map(null, () => { - throw new Error("unexpected") - }) - ).to.eql(undefined) - }) + throw new Error("unexpected"); + }), + ).to.eql(undefined); + }); it("skips if target is undefined", () => { expect( map(undefined, () => { - throw new Error("unexpected") - }) - ).to.eql(undefined) - }) + throw new Error("unexpected"); + }), + ).to.eql(undefined); + }); it("passes defined target to f", () => { - expect(map(123, (ea) => String(ea))).to.eql("123") - }) - }) -}) + expect(map(123, (ea) => String(ea))).to.eql("123"); + }); + }); +}); diff --git a/src/Object.ts b/src/Object.ts index 013c399..6067571 100644 --- a/src/Object.ts +++ b/src/Object.ts @@ -4,40 +4,34 @@ */ export function map( obj: T | undefined | null, - f: (t: T) => R + f: (t: T) => R, ): R | undefined { - return obj != null ? f(obj) : undefined + return obj != null ? f(obj) : undefined; } -export function isFunction(obj: any): obj is () => any { - return typeof obj === "function" +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 + o[key] = value; } } - return o + return o; } -export function omit, S extends keyof T>( +export function omit, S extends keyof T>( t: T, ...keysToOmit: S[] ): Omit { - const result = { ...t } + const result = { ...t }; for (const ea of keysToOmit) { - delete result[ea] + delete result[ea]; } - return result + return result; } diff --git a/src/OptionsVerifier.ts b/src/OptionsVerifier.ts new file mode 100644 index 0000000..946caaf --- /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 3fcaf88..b9940ce 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,35 +1,37 @@ -import { notBlank } from "./String" +import { notBlank } from "./String"; /** * Parser implementations convert stdout and stderr from the underlying child * 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, stderr: string | undefined, - passed: boolean + passed: boolean, ) => { - if (!passed) throw new Error("task failed") - if (notBlank(stderr)) throw new Error(stderr) - return stdout -} + if (!passed) throw new Error("task failed"); + if (notBlank(stderr)) throw new Error(stderr); + return stdout; +}; diff --git a/src/Pids.spec.ts b/src/Pids.spec.ts new file mode 100644 index 0000000..4acf579 --- /dev/null +++ b/src/Pids.spec.ts @@ -0,0 +1,199 @@ +import child_process from "node:child_process"; +import process from "node:process"; +import { expect } from "./_chai.spec"; +import { kill, pidExists } from "./Pids"; +import { isWin } from "./Platform"; + +describe("Pids", function () { + describe("pidExists", function () { + it("should return true for current process", function () { + expect(pidExists(process.pid)).to.be.true; + }); + + it("should return false for invalid PIDs", function () { + expect(pidExists(0)).to.be.false; + expect(pidExists(-1)).to.be.false; + expect(pidExists(-999)).to.be.false; + }); + + it("should return false for null and undefined", function () { + expect(pidExists(null as any)).to.be.false; + expect(pidExists(undefined)).to.be.false; + }); + + it("should return false for non-finite numbers", function () { + expect(pidExists(NaN)).to.be.false; + expect(pidExists(Infinity)).to.be.false; + expect(pidExists(-Infinity)).to.be.false; + }); + + it("should return false for very large non-existent PID", function () { + // Use a PID that's extremely unlikely to exist + expect(pidExists(999999999)).to.be.false; + }); + + it("should handle child process PIDs correctly", function () { + const child = child_process.spawn("node", [ + "-e", + "setTimeout(() => {}, 100)", + ]); + + if (child.pid != null) { + expect(pidExists(child.pid)).to.be.true; + + child.kill(); + + // Give process time to terminate + return new Promise((resolve) => { + child.on("exit", () => { + // Process should no longer exist after termination + setTimeout(() => { + expect(pidExists(child.pid!)).to.be.false; + resolve(); + }, 50); + }); + }); + } else { + // If no PID, skip this test + return Promise.resolve(); + } + }); + + if (isWin) { + it("should handle Windows-specific error codes", function () { + // Create a process that terminates quickly to potentially trigger Windows-specific errors + const child = child_process.spawn("cmd", ["/c", "echo test"]); + + if (child.pid != null) { + const originalPid = child.pid; + + return new Promise((resolve) => { + child.on("exit", () => { + // On Windows, attempting to check a recently terminated process + // may throw EINVAL or EACCES instead of ESRCH + setTimeout(() => { + // This should return false regardless of the specific error code + expect(pidExists(originalPid)).to.be.false; + resolve(); + }, 100); + }); + }); + } else { + // If no PID, skip this test + return Promise.resolve(); + } + }); + } + + it("should handle error conditions gracefully", function () { + // Test EPERM error (should return true - process exists but no permission) + const mockKillEPERM = () => { + const err = new Error( + "Operation not permitted", + ) as NodeJS.ErrnoException; + err.code = "EPERM"; + throw err; + }; + expect(pidExists(12345, mockKillEPERM)).to.be.true; + + // Test ESRCH error (should return false - no such process) + const mockKillESRCH = () => { + const err = new Error("No such process") as NodeJS.ErrnoException; + err.code = "ESRCH"; + throw err; + }; + expect(pidExists(12345, mockKillESRCH)).to.be.false; + + if (isWin) { + // Test Windows-specific EINVAL error (should return false) + const mockKillEINVAL = () => { + const err = new Error("Invalid argument") as NodeJS.ErrnoException; + err.code = "EINVAL"; + throw err; + }; + expect(pidExists(12345, mockKillEINVAL)).to.be.false; + + // Test Windows-specific EACCES error (should return false) + const mockKillEACCES = () => { + const err = new Error("Permission denied") as NodeJS.ErrnoException; + err.code = "EACCES"; + throw err; + }; + expect(pidExists(12345, mockKillEACCES)).to.be.false; + } + + // Test unknown error code (should return false) + const mockKillUnknown = () => { + const err = new Error("Unknown error") as NodeJS.ErrnoException; + err.code = "EUNKNOWN"; + throw err; + }; + expect(pidExists(12345, mockKillUnknown)).to.be.false; + }); + }); + + describe("kill", function () { + it("should return false for invalid PIDs", function () { + expect(kill(0)).to.be.false; + expect(kill(-1)).to.be.false; + expect(kill(null as any)).to.be.false; + expect(kill(undefined)).to.be.false; + expect(kill(NaN)).to.be.false; + expect(kill(Infinity)).to.be.false; + }); + + it("should return false for non-existent PID", function () { + expect(kill(999999999)).to.be.false; + }); + + it("should handle ESRCH error gracefully", function () { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalKill = process.kill; + + const mockKill = () => { + const err = new Error("No such process - ESRCH"); + throw err; + }; + process.kill = mockKill; + + expect(kill(12345)).to.be.false; + + process.kill = originalKill; + }); + + it("should re-throw non-ESRCH errors", function () { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalKill = process.kill; + + const mockKill = () => { + const err = new Error("Operation not permitted"); + throw err; + }; + process.kill = mockKill; + + expect(() => kill(12345)).to.throw("Operation not permitted"); + + process.kill = originalKill; + }); + + it("should use SIGKILL when force is true", function () { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalKill = process.kill; + let capturedSignal: string | number | undefined; + + const mockKill = (_pid: number, signal?: string | number): true => { + capturedSignal = signal; + return true; + }; + process.kill = mockKill; + + kill(12345, true); + expect(capturedSignal).to.equal("SIGKILL"); + + kill(12345, false); + expect(capturedSignal).to.be.undefined; + + process.kill = originalKill; + }); + }); +}); diff --git a/src/Pids.ts b/src/Pids.ts index 70b8922..d700f3b 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -1,136 +1,57 @@ -import child_process from "child_process" -import process from "process" -import { map } from "./Object" -import { isWin } from "./Platform" - -function safePid(pid: number) { - if (typeof pid !== "number" || pid < 0) { - throw new Error("invalid pid: " + JSON.stringify(pid)) - } else { - return Math.floor(pid).toString() - } -} - -/* - -Windows 10: - ->tasklist /NH /FO "CSV" /FI "PID eq 15524" -INFO: No tasks are running which match the specified criteria. - ->tasklist /NH /FO "CSV" /FI "PID eq 11968" -"bash.exe","11968","Console","1","5,340 K" - -Linux: - -$ ps -p 20242 - PID TTY TIME CMD -20242 pts/3 00:00:00 bash - -Mac: - -$ ps -p 32183 - PID TTY TIME CMD -32183 ttys001 0:00.10 /bin/bash -l - -*/ +import { isWin } from "./Platform"; /** * @param {number} pid process id. Required. - * @returns {Promise} true if the given process id is in the local - * process table. The PID may be paused or a zombie, though. + * @param {Function} killFn optional kill function, defaults to process.kill + * @returns boolean true if the given process id is in the local process + * table. The PID may be paused or a zombie, though. */ -export function pidExists(pid: number | null | undefined): Promise { - if (pid == null) return Promise.resolve(false) - const needle = safePid(pid) - const cmd = isWin ? "tasklist" : "ps" - const args = isWin - ? // NoHeader, FOrmat CSV, FIlter on pid: - [ - ["/NH", "/FO", "CSV", "/FI", "PID eq " + needle], - { - windowsHide: true, - }, - ] - : // linux has "quick" mode (-q) but mac doesn't. We add the ",1" to avoid ps - // returning exit code 1, which generates an extraneous Error. - [["-p", needle + ",1"]] - return new Promise((resolve) => { - child_process.execFile( - cmd, - ...(args as any), - (error: Error | null, stdout: string) => { - const result = - error == null && - new RegExp( - isWin ? '"' + needle + '"' : "^\\s*" + needle + "\\b", - // The posix regex pattern needs multiline support: - "m" - ).exec(String(stdout).trim()) != null - resolve(result) +export function pidExists( + pid: number | undefined, + killFn?: (pid: number, signal?: string | number) => boolean, +): boolean { + if (pid == null || !isFinite(pid) || pid <= 0) return false; + try { + // 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 (killFn ?? process.kill)(pid, 0); + } catch (err: unknown) { + const errorCode = (err as NodeJS.ErrnoException)?.code; + + // EPERM means we don't have permission to signal the process, but it exists + if (errorCode === "EPERM") return true; + + // ESRCH means "no such process" - the process doesn't exist or has terminated + if (errorCode === "ESRCH") return false; + + // On Windows, additional error codes can indicate process termination issues + if (isWin) { + // EINVAL: Invalid signal argument (process may be terminating) + // EACCES: Access denied (process may be in terminating state) + if (errorCode === "EINVAL" || errorCode === "EACCES") { + return false; } - ) - }) -} - -const winRe = /^".+?","(\d+)"/ -const posixRe = /^\s*(\d+)/ + } -/** - * @export - * @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[] - ) - } - ) - }) + // For any other error, assume the pid is gone + return false; + } } /** * Send a signal to the given process id. * - * @export - * @param {number} pid the process id. Required. - * @param {boolean} [force=false] if true, and the current user has - * permissions to send the signal, the pid will be forced to shut down. + * @param pid the process id. Required. + * @param force if true, and the current user has + * permissions to send the signal, the pid will be forced to shut down. Defaults to false. */ -export function kill(pid: number | null | undefined, force = false): void { - if (pid == null) return - - if (pid === process.pid || pid === process.ppid) { - throw new Error("cannot self-terminate") - } - - if (isWin) { - const args = ["/PID", safePid(pid), "/T"] - if (force) { - args.push("/F") - } - child_process.execFile("taskkill", args) - } else { - try { - process.kill(pid, force ? "SIGKILL" : "SIGTERM") - } catch (err) { - if (!String(err).includes("ESRCH")) throw err - } +export function kill(pid: number | undefined, force = false): boolean { + if (pid == null || !isFinite(pid) || pid <= 0) return false; + try { + return process.kill(pid, force ? "SIGKILL" : undefined); + } catch (err) { + if (!String(err).includes("ESRCH")) throw err; + return false; + // failed to get priority--assume the pid is gone. } } diff --git a/src/Platform.ts b/src/Platform.ts index d626bc6..74a6cf2 100644 --- a/src/Platform.ts +++ b/src/Platform.ts @@ -1,7 +1,7 @@ -import os from "os" +import os from "node:os"; -const _platform = os.platform() +const _platform = os.platform(); -export const isWin = ["win32", "cygwin"].includes(_platform) -export const isMac = _platform === "darwin" -export const isLinux = _platform === "linux" +export const isWin = ["win32", "cygwin"].includes(_platform); +export const isMac = _platform === "darwin"; +export const isLinux = _platform === "linux"; diff --git a/src/ProcessHealthMonitor.spec.ts b/src/ProcessHealthMonitor.spec.ts new file mode 100644 index 0000000..20a9bab --- /dev/null +++ b/src/ProcessHealthMonitor.spec.ts @@ -0,0 +1,387 @@ +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..fa64b99 --- /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..111e384 --- /dev/null +++ b/src/ProcessPoolManager.spec.ts @@ -0,0 +1,256 @@ +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..f8f9149 --- /dev/null +++ b/src/ProcessPoolManager.ts @@ -0,0 +1,313 @@ +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..5831ec3 --- /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..a457b49 --- /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/Rate.spec.ts b/src/Rate.spec.ts index 9607b1f..5abb1aa 100644 --- a/src/Rate.spec.ts +++ b/src/Rate.spec.ts @@ -1,76 +1,79 @@ -import { minuteMs } from "./BatchClusterOptions" -import { Rate } from "./Rate" -import { expect, times } from "./_chai.spec" - -const tk = require("timekeeper") +import FakeTimers from "@sinonjs/fake-timers"; +import { minuteMs } from "./BatchClusterOptions"; +import { Rate } from "./Rate"; +import { expect, times } from "./_chai.spec"; describe("Rate", () => { - const now = Date.now() - const r = new Rate() + const now = Date.now(); + const r = new Rate(); + let clock: FakeTimers.InstalledClock; beforeEach(() => { - tk.freeze(now) - // clear() must be called _after_ freezing time - r.clear() - }) + 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) - expect(rate.eventsPerSecond).to.be.withinToleranceOf(epm * 1000, tol) - expect(rate.eventsPerMinute).to.be.withinToleranceOf(epm * 60 * 1000, tol) + expect(rate.eventsPerMs).to.be.withinToleranceOf(epm, tol); + expect(rate.eventsPerSecond).to.be.withinToleranceOf(epm * 1000, tol); + expect(rate.eventsPerMinute).to.be.withinToleranceOf(epm * 60 * 1000, tol); } it("is born with a rate of 0", () => { - expectRate(r, 0) - }) + expectRate(r, 0); + }); it("maintains a rate of 0 after time with no events", () => { - tk.freeze(now + minuteMs) - expectRate(r, 0) - }) + clock.tick(minuteMs); + expectRate(r, 0); + }); for (const cnt of [1, 2, 3, 4]) { it( "decays the rate from " + cnt + " simultaneous event(s) as time elapses", () => { - times(cnt, () => r.onEvent()) - expectRate(r, 0) - tk.freeze(now + 100) - expectRate(r, 0) - tk.freeze(now + r.warmupMs + 1) - expectRate(r, cnt / r.warmupMs) - tk.freeze(now + 2 * r.warmupMs) - expectRate(r, cnt / (2 * r.warmupMs)) - tk.freeze(now + 3 * r.warmupMs) - expectRate(r, cnt / (3 * r.warmupMs)) - tk.freeze(now + r.periodMs) - expectRate(r, 0) - expect(r.msSinceLastEvent).to.eql(minuteMs) - } - ) + times(cnt, () => r.onEvent()); + expectRate(r, 0); + clock.tick(100); + expectRate(r, 0); + clock.tick(r.warmupMs - 100 + 1); + expectRate(r, cnt / r.warmupMs); + clock.tick(r.warmupMs); + expectRate(r, cnt / (2 * r.warmupMs)); + clock.tick(r.warmupMs); + expectRate(r, cnt / (3 * r.warmupMs)); + clock.tick(r.periodMs - 3 * r.warmupMs); + expectRate(r, 0); + 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) - r.onEvent() - }) - expectRate(r, events / period, 0.3) - tk.freeze(now + 1.25 * r.periodMs) - expectRate(r, 0.75 * (events / period), 0.3) - tk.freeze(now + 1.5 * r.periodMs) - 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) - expectRate(r, 0) - } - ) + const period = r.periodMs; + times(events, () => { + clock.tick(r.periodMs / events); + r.onEvent(); + }); + const tickMs = r.periodMs / 4; + expectRate(r, events / period, 0.3); + clock.tick(tickMs); + expectRate(r, 0.75 * (events / period), 0.3); + clock.tick(tickMs); + expectRate(r, 0.5 * (events / period), 0.3); + clock.tick(tickMs); + expectRate(r, 0.25 * (events / period), 0.5); + clock.tick(tickMs); + expectRate(r, 0); + }, + ); } -}) +}); diff --git a/src/Rate.ts b/src/Rate.ts index 8fd2938..dd99e53 100644 --- a/src/Rate.ts +++ b/src/Rate.ts @@ -1,4 +1,4 @@ -import { minuteMs, secondMs } from "./BatchClusterOptions" +import { minuteMs, secondMs } from "./BatchClusterOptions"; // Implementation notes: @@ -12,71 +12,74 @@ import { minuteMs, secondMs } from "./BatchClusterOptions" // a large periodMs. export class Rate { - #start = Date.now() - readonly #priorEventTimestamps: number[] = [] - #lastEventTs: number | null = null - #eventCount = 0 + #start = Date.now(); + readonly #priorEventTimestamps: number[] = []; + #lastEventTs: number | null = null; + #eventCount = 0; /** * @param periodMs the length of time to retain event timestamps for computing * rate. Events older than this value will be discarded. - * @param warmupMs return `null` from {@link #msPerEvent} if it's been less - * than `warmupMs` since construction or {@link #clear}. + * @param warmupMs return `null` from {@link Rate#msPerEvent} if it's been less + * than `warmupMs` since construction or {@link Rate#clear}. */ - constructor(readonly periodMs = minuteMs, readonly warmupMs = secondMs) {} + constructor( + readonly periodMs = minuteMs, + readonly warmupMs = secondMs, + ) {} onEvent(): void { - this.#eventCount++ - const now = Date.now() - this.#priorEventTimestamps.push(now) - this.#lastEventTs = now + this.#eventCount++; + const now = Date.now(); + this.#priorEventTimestamps.push(now); + this.#lastEventTs = now; } #vacuum() { - const expired = Date.now() - this.periodMs + const expired = Date.now() - this.periodMs; const firstValidIndex = this.#priorEventTimestamps.findIndex( - (ea) => ea > expired - ) - if (firstValidIndex === -1) this.#priorEventTimestamps.length = 0 + (ea) => ea > expired, + ); + if (firstValidIndex === -1) this.#priorEventTimestamps.length = 0; else if (firstValidIndex > 0) { - this.#priorEventTimestamps.splice(0, firstValidIndex) + this.#priorEventTimestamps.splice(0, firstValidIndex); } } get eventCount(): number { - return this.#eventCount + return this.#eventCount; } get msSinceLastEvent(): number | null { - return this.#lastEventTs == null ? null : Date.now() - this.#lastEventTs + return this.#lastEventTs == null ? null : Date.now() - this.#lastEventTs; } get msPerEvent(): number | null { - const msSinceStart = Date.now() - this.#start - if (this.#lastEventTs == null || msSinceStart < this.warmupMs) return null - this.#vacuum() - const events = this.#priorEventTimestamps.length - return events === 0 ? null : Math.min(this.periodMs, msSinceStart) / events + const msSinceStart = Date.now() - this.#start; + if (this.#lastEventTs == null || msSinceStart < this.warmupMs) return null; + this.#vacuum(); + const events = this.#priorEventTimestamps.length; + return events === 0 ? null : Math.min(this.periodMs, msSinceStart) / events; } get eventsPerMs(): number { - const mpe = this.msPerEvent - return mpe == null ? 0 : mpe < 1 ? 1 : 1 / mpe + const mpe = this.msPerEvent; + return mpe == null ? 0 : mpe < 1 ? 1 : 1 / mpe; } get eventsPerSecond(): number { - return this.eventsPerMs * secondMs + return this.eventsPerMs * secondMs; } get eventsPerMinute(): number { - return this.eventsPerMs * minuteMs + return this.eventsPerMs * minuteMs; } clear(): this { - this.#start = Date.now() - this.#priorEventTimestamps.length = 0 - this.#lastEventTs = null - this.#eventCount = 0 - return this + this.#start = Date.now(); + this.#priorEventTimestamps.length = 0; + this.#lastEventTs = null; + this.#eventCount = 0; + return this; } } diff --git a/src/Stream.ts b/src/Stream.ts index 4c57bad..352dfb1 100644 --- a/src/Stream.ts +++ b/src/Stream.ts @@ -1,15 +1,13 @@ -import stream from "stream" +import { Readable, Writable } from "node:stream"; -export function end( - endable: stream.Writable, - contents?: string -): Promise { - return new Promise((resolve) => endable.end(contents, resolve)) -} - -export function mapNotDestroyed( - obj: T | undefined | null, - f: (t: T) => R -): R | undefined { - return obj != null && !obj.destroyed ? f(obj) : undefined +export function destroy(stream: Readable | Writable | null) { + try { + // .end() may result in an EPIPE when the child process exits. We don't + // care. We just want to make sure the stream is closed. + stream?.removeAllListeners("error"); + // It's fine to call .destroy() on a stream that's already destroyed. + stream?.destroy?.(); + } catch { + // don't care + } } diff --git a/src/StreamHandler.spec.ts b/src/StreamHandler.spec.ts new file mode 100644 index 0000000..f8647c2 --- /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..7e84029 --- /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..63e4111 100644 --- a/src/String.ts +++ b/src/String.ts @@ -1,17 +1,21 @@ -import { isFunction } from "./Object" +export function blank(s: unknown): boolean { + return s == null || toS(s).trim().length === 0; +} -export function blank(s: string | Buffer | undefined): boolean { - return s == null || String(s).trim().length === 0 +export function notBlank(s: unknown): boolean { + return !blank(s); } -export function notBlank(s: string | undefined): s is string { - 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 + 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 ea2ea7b..f104454 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -1,29 +1,29 @@ -import { delay } from "./Async" -import { Deferred } from "./Deferred" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { Parser } from "./Parser" +import { delay } from "./Async"; +import { Deferred } from "./Deferred"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { Parser } from "./Parser"; -type TaskOptions = Pick< +export type TaskOptions = Pick< InternalBatchProcessOptions, "streamFlushMillis" | "observer" | "passRE" | "failRE" | "logger" -> +>; -let _taskId = 1 +let _taskId = 1; /** * Tasks embody individual jobs given to the underlying child processes. Each * instance has a promise that will be resolved or rejected based on the * result of the task. */ -export class Task { - readonly taskId = _taskId++ - #opts?: TaskOptions - #startedAt?: number - #parsing = false - #settledAt?: number - readonly #d = new Deferred() - #stdout = "" - #stderr = "" +export class Task { + readonly taskId = _taskId++; + #opts?: TaskOptions; + #startedAt?: number; + #parsing = false; + #settledAt?: number; + readonly #d = new Deferred(); + #stdout = ""; + #stderr = ""; /** * @param {string} command is the value written to stdin to perform the given @@ -31,43 +31,46 @@ export class Task { * @param {Parser} parser is used to parse resulting data from the * underlying process to a typed object. */ - constructor(readonly command: string, readonly parser: Parser) { + constructor( + readonly command: string, + readonly parser: Parser, + ) { // We can't use .finally here, because that creates a promise chain that, if // rejected, results in an uncaught rejection. this.#d.promise.then( () => this.#onSettle(), - () => this.#onSettle() - ) + () => this.#onSettle(), + ); } /** * @return the resolution or rejection of this task. */ get promise(): Promise { - return this.#d.promise + return this.#d.promise; } get pending(): boolean { - return this.#d.pending + return this.#d.pending; } get state(): string { return this.#d.pending ? "pending" : this.#d.rejected - ? "rejected" - : "resolved" + ? "rejected" + : "resolved"; } onStart(opts: TaskOptions) { - this.#opts = opts - this.#startedAt = Date.now() + this.#opts = opts; + this.#startedAt = Date.now(); } get runtimeMs() { return this.#startedAt == null ? undefined - : (this.#settledAt ?? Date.now()) - this.#startedAt + : (this.#settledAt ?? Date.now()) - this.#startedAt; } toString(): string { @@ -77,80 +80,81 @@ export class Task { this.command.replace(/\s+/gm, " ").slice(0, 80).trim() + ")#" + this.taskId - ) + ); } onStdout(buf: string | Buffer): void { - this.#stdout += buf.toString() - const passRE = this.#opts?.passRE + this.#stdout += buf.toString(); + const passRE = this.#opts?.passRE; if (passRE != null && passRE.exec(this.#stdout) != null) { // remove the pass token from stdout: - this.#stdout = this.#stdout.replace(passRE, "") - this.#resolve(true) + this.#stdout = this.#stdout.replace(passRE, ""); + void this.#resolve(true); } else { - const failRE = this.#opts?.failRE + 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) + this.#stdout = this.#stdout.replace(failRE, ""); + void this.#resolve(false); } } } onStderr(buf: string | Buffer): void { - this.#stderr += buf.toString() - const failRE = this.#opts?.failRE + this.#stderr += buf.toString(); + const failRE = this.#opts?.failRE; if (failRE != null && failRE.exec(this.#stderr) != null) { // remove the fail token from stderr: - this.#stderr = this.#stderr.replace(failRE, "") - this.#resolve(false) + this.#stderr = this.#stderr.replace(failRE, ""); + void this.#resolve(false); } } #onSettle() { - this.#settledAt ??= Date.now() + this.#settledAt ??= Date.now(); } /** * @return true if the wrapped promise was rejected */ reject(error: Error): boolean { - return this.#d.reject(error) + return this.#d.reject(error); } async #resolve(passed: boolean) { // fail always wins. - passed = !this.#d.rejected && passed + passed = !this.#d.rejected && passed; // wait for stderr and stdout to flush: - const flushMs = this.#opts?.streamFlushMillis ?? 0 + const flushMs = this.#opts?.streamFlushMillis ?? 0; if (flushMs > 0) { - await delay(flushMs) + await delay(flushMs); } // we're expecting this method may be called concurrently (if there are both // pass and fail tokens found in stderr and stdout), but we only want to run // this once, so - if (!this.pending || this.#parsing) return + if (!this.pending || this.#parsing) return; // this.#opts // ?.logger() // .trace("Task.#resolve()", { command: this.command, state: this.state }) // Prevent concurrent parsing: - this.#parsing = true + this.#parsing = true; try { - const parseResult = await this.parser(this.#stdout, this.#stderr, passed) + 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") - ) + 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..664dff8 --- /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..1f203fc --- /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 01ab396..ea80bb0 100644 --- a/src/Timeout.ts +++ b/src/Timeout.ts @@ -1,30 +1,38 @@ -import timers from "timers" -export const Timeout = Symbol("timeout") +import timers from "node:timers"; +export const Timeout = Symbol("timeout"); export async function thenOrTimeout( p: Promise, - timeoutMs: number + timeoutMs: number, ): Promise { - return new Promise(async (resolve, reject) => { - let pending = true - try { - const t = timers.setTimeout(() => { - if (pending) { - pending = false - resolve(Timeout) - } - }, timeoutMs) - const result = await p - if (pending) { - pending = false - clearTimeout(t) - resolve(result) - } - } catch (err) { - if (pending) { - pending = false - reject(err) - } - } - }) + // TODO: if timeoutMs is [1, 1000], it's probably a mistake. Should we do + // something else in that case? + return timeoutMs <= 1 + ? p + : new Promise((resolve, reject) => { + let pending = true; + const t = timers.setTimeout(() => { + if (pending) { + pending = false; + resolve(Timeout); + } + }, 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..53f0d70 --- /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 54b8bae..02ac698 100644 --- a/src/_chai.spec.ts +++ b/src/_chai.spec.ts @@ -1,24 +1,25 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ try { - require("source-map-support").install() + require("source-map-support").install(); } catch { // } -import { expect, use } from "chai" -import child_process from "child_process" -import path from "path" -import process from "process" -import { Log, logger, setLogger } from "./Logger" -import { Parser } from "./Parser" -import { pids } from "./Pids" -import { notBlank } from "./String" +import { expect, use } from "chai"; +import child_process from "node:child_process"; +import path from "node:path"; +import process from "node:process"; +import { Log, logger, setLogger } from "./Logger"; +import { Parser } from "./Parser"; +import { pidExists } from "./Pids"; +import { notBlank } from "./String"; -use(require("chai-as-promised")) -use(require("chai-string")) -use(require("chai-subset")) -use(require("chai-withintoleranceof")) +use(require("chai-as-promised")); +use(require("chai-string")); +use(require("chai-subset")); +use(require("chai-withintoleranceof")); -export { expect } from "chai" +export { expect } from "chai"; // Tests should be quiet unless LOG is set to "trace" or "debug" or "info" or... setLogger( @@ -26,65 +27,63 @@ 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" - ) - ) - ) -) + (process.env.LOG as any) ?? "error", + ), + ), + ), +); -export const parserErrors: string[] = [] +export const parserErrors: string[] = []; -export const unhandledRejections: Error[] = [] +export const unhandledRejections: Error[] = []; -beforeEach(() => (parserErrors.length = 0)) +beforeEach(() => (parserErrors.length = 0)); process.on("unhandledRejection", (reason: any) => { - console.error("unhandledRejection:", reason.stack ?? reason) - unhandledRejections.push(reason) -}) + console.error("unhandledRejection:", reason.stack ?? reason); + unhandledRejections.push(reason); +}); -afterEach(() => expect(unhandledRejections).to.eql([])) +afterEach(() => expect(unhandledRejections).to.eql([])); export const parser: Parser = ( stdout: string, stderr: string | undefined, - passed: boolean + passed: boolean, ) => { if (stderr != null) { - parserErrors.push(stderr) + parserErrors.push(stderr); } if (!passed || notBlank(stderr)) { logger().debug("test parser: rejecting task", { stdout, stderr, passed, - }) + }); // process.stdout.write("!") - throw new Error(stderr) + throw new Error(stderr); } else { const str = stdout .split(/(\r?\n)+/) .filter((ea) => notBlank(ea) && !ea.startsWith("# ")) .join("\n") - .trim() - logger().debug("test parser: resolving task", str) + .trim(); + logger().debug("test parser: resolving task", str); // process.stdout.write(".") - return str + return str; } -} +}; export function times(n: number, f: (idx: number) => T): T[] { return Array(n) .fill(undefined) - .map((_, i) => f(i)) + .map((_, i) => f(i)); } // because @types/chai-withintoleranceof isn't a thing (yet) @@ -92,37 +91,38 @@ export function times(n: number, f: (idx: number) => T): T[] { type WithinTolerance = ( expected: number, tol: number | number[], - message?: string -) => Chai.Assertion + message?: string, +) => Chai.Assertion; // eslint-disable-next-line @typescript-eslint/no-namespace declare namespace Chai { interface Assertion { - withinToleranceOf: WithinTolerance - withinTolOf: WithinTolerance + withinToleranceOf: WithinTolerance; + withinTolOf: WithinTolerance; } } -export const procs: child_process.ChildProcess[] = [] +export const childProcs: child_process.ChildProcess[] = []; export function testPids(): number[] { - return procs.map((proc) => proc.pid).filter((ea) => ea != null) as number[] + return childProcs + .map((proc) => proc.pid) + .filter((ea) => ea != null) as number[]; } -export async function currentTestPids(): Promise { - const alivePids = new Set(await pids()) - return testPids().filter((ea) => alivePids.has(ea)) +export function currentTestPids(): number[] { + return testPids().filter((pid) => pidExists(pid)); } export function sortNumeric(arr: number[]): number[] { - return arr.sort((a, b) => a - b) + return arr.sort((a, b) => a - b); } export function flatten(arr: (T | T[])[], result: T[] = []): T[] { arr.forEach((ea) => - Array.isArray(ea) ? result.push(...ea) : result.push(ea) - ) - return result + Array.isArray(ea) ? result.push(...ea) : result.push(ea), + ); + return result; } // Seeding the RNG deterministically _should_ give us repeatable @@ -132,27 +132,27 @@ export function flatten(arr: (T | T[])[], result: T[] = []): T[] { // to make sure different error pathways are exercised. YYYY-MM-$callcount // should do it. -const rngseedPrefix = new Date().toISOString().slice(0, 7) + "." -let rngseedCounter = 0 -let rngseedOverride: string | undefined +const rngseedPrefix = new Date().toISOString().slice(0, 7) + "."; +let rngseedCounter = 0; +let rngseedOverride: string | undefined; export function setRngseed(seed?: string) { - rngseedOverride = seed + rngseedOverride = seed; } function rngseed() { // We need a new rngseed for every execution, or all runs will either pass or // fail: - return rngseedOverride ?? rngseedPrefix + rngseedCounter++ + return rngseedOverride ?? rngseedPrefix + rngseedCounter++; } -let failrate: string +let failrate: string; export function setFailratePct(percent: number) { - failrate = (percent / 100).toFixed(2) + failrate = (percent / 100).toFixed(2); } -let unluckyfail: "1" | "0" +let unluckyfail: "1" | "0"; /** * Should EUNLUCKY be handled properly by the test script, and emit a "FAIL", or @@ -162,28 +162,28 @@ let unluckyfail: "1" | "0" * where all flaky errors require a timeout to recover. */ export function setUnluckyFail(b: boolean) { - unluckyfail = b ? "1" : "0" + unluckyfail = b ? "1" : "0"; } -let newline: "lf" | "crlf" +let newline: "lf" | "crlf"; export function setNewline(eol: "lf" | "crlf") { - newline = eol + newline = eol; } -let ignoreExit: "1" | "0" +let ignoreExit: "1" | "0"; export function setIgnoreExit(ignore: boolean) { - ignoreExit = ignore ? "1" : "0" + ignoreExit = ignore ? "1" : "0"; } beforeEach(() => { - setFailratePct(10) - setUnluckyFail(true) - setNewline("lf") - setIgnoreExit(false) - setRngseed() -}) + setFailratePct(10); + setUnluckyFail(true); + setNewline("lf"); + setIgnoreExit(false); + setRngseed(); +}); export const processFactory = () => { const proc = child_process.spawn( @@ -197,8 +197,8 @@ export const processFactory = () => { ignoreExit, unluckyfail, }, - } - ) - procs.push(proc) - return proc -} + }, + ); + childProcs.push(proc); + return proc; +}; diff --git a/src/test-helpers.ts b/src/test-helpers.ts new file mode 100644 index 0000000..ab79908 --- /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 f503d67..fcf613c 100644 --- a/src/test.spec.ts +++ b/src/test.spec.ts @@ -1,168 +1,159 @@ -import child_process from "child_process" -import { until } from "./Async" -import { Deferred } from "./Deferred" -import { kill, pidExists } from "./Pids" +import child_process from "node:child_process"; +import { until } from "./Async"; +import { Deferred } from "./Deferred"; +import { kill, pidExists } from "./Pids"; import { expect, processFactory, setFailratePct, setIgnoreExit, setRngseed, -} from "./_chai.spec" - -/* eslint-disable @typescript-eslint/no-non-null-assertion */ +} from "./_chai.spec"; describe("test.js", () => { class Harness { - readonly child: child_process.ChildProcess - public output = "" + readonly child: child_process.ChildProcess; + public output = ""; constructor() { - setFailratePct(0) - this.child = processFactory() + setFailratePct(0); + this.child = processFactory(); this.child.on("error", (err: any) => { - throw err - }) + throw err; + }); this.child.stdout!.on("data", (buff: any) => { - this.output += buff.toString() - }) + this.output += buff.toString(); + }); } async untilOutput(minLength = 0): Promise { - await until(() => this.output.length > minLength, 1000) - return + await until(() => this.output.length > minLength, 1000); + return; } async end(): Promise { - this.child.stdin!.end(null) - await until(() => this.running().then((ea) => !ea), 1000) + this.child.stdin!.end(null); + 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) + console.error("Ack, I had to kill child pid " + this.child.pid); + kill(this.child.pid); } - return + return; } - async running(): Promise { - return pidExists(this.child.pid) + 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: - const alive = await until(() => pidExists(this.child.pid), 2000) - expect(alive).to.eql(true) - const d = new Deferred() + const alive = await until(() => pidExists(this.child.pid), 2000); + expect(alive).to.eql(true); + const d = new Deferred(); this.child.on("exit", async () => { try { - f(this.output.trim()) - expect(await this.running()).to.eql(false) - d.resolve("on exit") + f(this.output.trim()); + expect(await this.running()).to.eql(false); + d.resolve("on exit"); } catch (err: any) { - d.reject(err) + d.reject(err); } - }) - return d + }); + return d; } } it("results in expected output", async () => { - const h = new Harness() + const h = new Harness(); const a = h.assertStdout((ea) => - expect(ea).to.eql("HELLO\nPASS\nworld\nPASS\nFAIL\nv1.2.3\nPASS") - ) - h.child.stdin!.end("upcase Hello\ndowncase World\ninvalid input\nversion\n") - return a - }) + expect(ea).to.eql("HELLO\nPASS\nworld\nPASS\nFAIL\nv1.2.3\nPASS"), + ); + h.child.stdin!.end( + "upcase Hello\ndowncase World\ninvalid input\nversion\n", + ); + return a; + }); it("exits properly if ignoreExit is not set", async () => { - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\nexit\n") - await h.untilOutput(9) - expect(h.output).to.eql("FUZZY\nPASS\n") - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(false) - return - }) - - it("kill(!force) with ignoreExit set doesn't cause the process to end", async () => { - setIgnoreExit(true) - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\n") - await h.untilOutput() - kill(h.child.pid, false) - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(true) - return h.end() - }) + const h = new Harness(); + h.child.stdin!.write("upcase fuzzy\nexit\n"); + await h.untilOutput(9); + expect(h.output).to.eql("FUZZY\nPASS\n"); + await until(() => h.notRunning(), 500); + expect(await h.running()).to.eql(false); + return; + }); it("kill(!force) with ignoreExit unset causes the process to end", async () => { - setIgnoreExit(false) - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\n") - await h.untilOutput() - kill(h.child.pid, true) - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(false) - return - }) + setIgnoreExit(false); + const h = new Harness(); + h.child.stdin!.write("upcase fuzzy\n"); + await h.untilOutput(); + kill(h.child.pid, true); + await until(() => h.notRunning(), 500); + expect(await h.running()).to.eql(false); + return; + }); it("kill(force) even with ignoreExit set causes the process to end", async () => { - setIgnoreExit(true) - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\n") - await h.untilOutput() - kill(h.child.pid, true) - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(false) - return - }) + setIgnoreExit(true); + const h = new Harness(); + h.child.stdin!.write("upcase fuzzy\n"); + await h.untilOutput(); + kill(h.child.pid, true); + await until(() => h.notRunning(), 500); + expect(await h.running()).to.eql(false); + return; + }); it("doesn't exit if ignoreExit is set", async () => { - setIgnoreExit(true) - const h = new Harness() - h.child.stdin!.write("upcase Boink\nexit\n") - await h.untilOutput("BOINK\nPASS\nignore".length) - expect(h.output).to.eql("BOINK\nPASS\nignoreExit is set\n") - expect(await h.running()).to.eql(true) - await h.end() - expect(await h.running()).to.eql(false) - return - }) + setIgnoreExit(true); + const h = new Harness(); + h.child.stdin!.write("upcase Boink\nexit\n"); + await h.untilOutput("BOINK\nPASS\nignore".length); + expect(h.output).to.eql("BOINK\nPASS\nignoreExit is set\n"); + expect(await h.running()).to.eql(true); + await h.end(); + expect(await h.running()).to.eql(false); + return; + }); it("returns a valid pid", async () => { - const h = new Harness() - expect(await pidExists(h.child.pid)).to.eql(true) - await h.end() - return - }) + const h = new Harness(); + expect(pidExists(h.child.pid)).to.eql(true); + await h.end(); + return; + }); it("sleeps serially", () => { - const h = new Harness() - const start = Date.now() - const times = [200, 201, 202] + const h = new Harness(); + const start = Date.now(); + const times = [200, 201, 202]; const a = h .assertStdout((output) => { - const actualTimes: number[] = [] - const pids = new Set() + const actualTimes: number[] = []; + const pids = new Set(); output.split(/[\r\n]/).forEach((line) => { if (line.startsWith("{") && line.endsWith("}")) { - const json = JSON.parse(line) - actualTimes.push(json.slept) - pids.add(json.pid) + const json = JSON.parse(line); + actualTimes.push(json.slept); + pids.add(json.pid); } else { - expect(line).to.eql("PASS") + expect(line).to.eql("PASS"); } - }) - expect(pids.size).to.eql(1, "only one pid should have been used") - expect(actualTimes).to.eql(times) + }); + expect(pids.size).to.eql(1, "only one pid should have been used"); + expect(actualTimes).to.eql(times); }) - .then(() => expect(Date.now() - start).to.be.gte(603)) - h.child.stdin!.end(times.map((ea) => "sleep " + ea).join("\n") + "\nexit\n") - return a - }) + .then(() => expect(Date.now() - start).to.be.gte(603)); + h.child.stdin!.end( + times.map((ea) => "sleep " + ea).join("\n") + "\nexit\n", + ); + return a; + }); it("flakes out the first N responses", () => { - setFailratePct(0) - setRngseed("hello") - const h = new Harness() + setFailratePct(0); + setRngseed("hello"); + const h = new Harness(); // These random numbers are consistent because we have a consistent rngseed: const a = h.assertStdout((ea) => expect(ea).to.eql( @@ -173,10 +164,10 @@ describe("test.js", () => { "PASS", "flaky response (FAIL, r: 0.55, flakeRate: 1.00)", "FAIL", - ].join("\n") - ) - ) - h.child.stdin!.end("flaky .5\nflaky 0\nflaky 1\nexit\n") - return a - }) -}) + ].join("\n"), + ), + ); + h.child.stdin!.end("flaky .5\nflaky 0\nflaky 1\nexit\n"); + return a; + }); +}); diff --git a/src/test.ts b/src/test.ts index f0a969a..e2cd114 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import process from "process" -import { delay } from "./Async" -import { Mutex } from "./Mutex" +import process from "node:process"; +import { delay } from "./Async"; +import { Mutex } from "./Mutex"; /** * This is a script written to behave similarly to ExifTool or @@ -10,40 +10,43 @@ import { Mutex } from "./Mutex" * The complexity comes from introducing predictable flakiness. */ -const newline = process.env.newline === "crlf" ? "\r\n" : "\n" +const newline = process.env.newline === "crlf" ? "\r\n" : "\n"; async function write(s: string) { return new Promise((res, rej) => - process.stdout.write(s + newline, (err) => (err == null ? res() : rej(err))) - ) + process.stdout.write(s + newline, (err) => + err == null ? res() : rej(err), + ), + ); } -const ignoreExit = process.env.ignoreExit === "1" +const ignoreExit = process.env.ignoreExit === "1"; if (ignoreExit) { process.addListener("SIGINT", () => { - write("ignoring SIGINT") - }) + write("ignoring SIGINT"); + }); process.addListener("SIGTERM", () => { - write("ignoring SIGTERM") - }) + write("ignoring SIGTERM"); + }); } function toF(s: string | undefined) { - if (s == null) return - const f = parseFloat(s) - return isNaN(f) ? undefined : f + if (s == null) return; + const f = parseFloat(s); + return isNaN(f) ? undefined : f; } -const failrate = toF(process.env.failrate) ?? 0 +const failrate = toF(process.env.failrate) ?? 0; const rng = process.env.rngseed != null - ? require("seedrandom")(process.env.rngseed) - : Math.random + ? // eslint-disable-next-line @typescript-eslint/no-require-imports + require("seedrandom")(process.env.rngseed) + : Math.random; async function onLine(line: string): Promise { // write(`# ${_p.pid} onLine(${line.trim()}) (newline = ${process.env.newline})`) - const r = rng() + const r = rng(); if (r < failrate) { // stderr isn't buffered, so this should be flushed immediately: console.error( @@ -52,26 +55,26 @@ async function onLine(line: string): Promise { ", failrate: " + failrate.toFixed(2) + ", seed: " + - process.env.rngseed - ) + process.env.rngseed, + ); if (process.env.unluckyfail === "1") { // Wait for a bit to ensure streams get merged thanks to streamFlushMillis: - await delay(5) - await write("FAIL") + await delay(5); + await write("FAIL"); } - return + return; } - line = line.trim() - const tokens = line.split(/\s+/) - const firstToken = tokens.shift() + line = line.trim(); + const tokens = line.split(/\s+/); + const firstToken = tokens.shift(); // support multi-line outputs: - const postToken = tokens.join(" ").split("
").join(newline) + const postToken = tokens.join(" ").split("
").join(newline); try { switch (firstToken) { case "flaky": { - const flakeRate = toF(tokens.shift()) ?? failrate + const flakeRate = toF(tokens.shift()) ?? failrate; write( "flaky response (" + (r < flakeRate ? "FAIL" : "PASS") + @@ -81,70 +84,71 @@ async function onLine(line: string): Promise { flakeRate.toFixed(2) + // Extra information is used for context: (tokens.length > 0 ? ", " + tokens.join(" ") : "") + - ")" - ) + ")", + ); if (r < flakeRate) { - write("FAIL") + write("FAIL"); } else { - write("PASS") + write("PASS"); } - break + break; } case "upcase": { - write(postToken.toUpperCase()) - write("PASS") - break + write(postToken.toUpperCase()); + write("PASS"); + break; } case "downcase": { - write(postToken.toLowerCase()) - write("PASS") - break + write(postToken.toLowerCase()); + write("PASS"); + break; } case "sleep": { - const millis = parseInt(tokens[0] ?? "100") - await delay(millis) - write(JSON.stringify({ slept: millis, pid: process.pid })) - write("PASS") - break + const millis = parseInt(tokens[0] ?? "100"); + await delay(millis); + write(JSON.stringify({ slept: millis, pid: process.pid })); + write("PASS"); + break; } case "version": { - write("v1.2.3") - write("PASS") - break + write("v1.2.3"); + write("PASS"); + break; } case "exit": { if (ignoreExit) { - write("ignoreExit is set") + write("ignoreExit is set"); } else { - process.exit(0) + process.exit(0); } - break + break; } case "stderr": { // force stdout to be emitted before stderr, and exercise stream // debouncing: - write("PASS") - await delay(1) - console.error("Error: " + postToken) - break + write("PASS"); + await delay(1); + console.error("Error: " + postToken); + break; } default: { - console.error("invalid or missing command for input", line) - write("FAIL") + console.error("invalid or missing command for input", line); + write("FAIL"); } } } catch (err) { - console.error("Error: " + err) - write("FAIL") + console.error("Error: " + err); + write("FAIL"); } - return + return; } -const m = new Mutex() +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))) + .on("data", (ea: string) => m.serial(() => onLine(ea))); diff --git a/tsconfig.json b/tsconfig.json index d3eac13..d347c60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,10 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["ES2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "target": "ES2019" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "ES2019" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -24,9 +26,9 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -42,13 +44,13 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - "removeComments": false, /* Disable emitting comments. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "removeComments": false /* Disable emitting comments. */, // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ @@ -66,35 +68,35 @@ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ /* Interop Constraints */ - "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - "noPropertyAccessFromIndexSignature": false, /* Enforces using indexed accessors for keys declared using an indexed type */ - "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ - "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */, + "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, + "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, + "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, + "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, + "noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */, + "useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */, + "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, + "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, + "noUnusedParameters": true /* Raise an error when a function parameter isn't read */, + "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */, + "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, + "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, + "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, + "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, + "noPropertyAccessFromIndexSignature": false /* Enforces using indexed accessors for keys declared using an indexed type */, + "allowUnusedLabels": false /* Disable error reporting for unused labels. */, + "allowUnreachableCode": false /* Disable error reporting for unreachable code. */, /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..be76959 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,26 @@ +{ + "entryPoints": ["src/BatchCluster.ts"], + "out": "build/docs", + "name": "batch-cluster", + "includeVersion": false, + "excludePrivate": true, + "excludeInternal": true, + "readme": "README.md", + "githubPages": true, + "exclude": ["**/*test*", "**/*spec*"], + "projectDocuments": ["CHANGELOG.md", "SECURITY.md", "LICENSE"], + "navigationLinks": { + "GitHub": "https://github.com/photostructure/batch-cluster.js", + "NPM": "https://www.npmjs.com/package/batch-cluster" + }, + "highlightLanguages": [ + "bash", + "docker", + "dockerfile", + "javascript", + "json", + "shell", + "typescript", + "yaml" + ] +} diff --git a/types/chai-withintoleranceof/index.d.ts b/types/chai-withintoleranceof/index.d.ts index 77a914c..6ba9166 100644 --- a/types/chai-withintoleranceof/index.d.ts +++ b/types/chai-withintoleranceof/index.d.ts @@ -6,11 +6,14 @@ /// interface WithinTolerance { - (expected: number, tol: number | number[], message?: string): Chai.Assertion + (expected: number, tol: number | number[], message?: string): Chai.Assertion; } declare namespace Chai { - interface Assertion extends LanguageChains, NumericComparison, TypeComparison { + interface Assertion + extends LanguageChains, + NumericComparison, + TypeComparison { withinToleranceOf: WithinTolerance; withinTolOf: WithinTolerance; } diff --git a/types/chai-withintoleranceof/package.json b/types/chai-withintoleranceof/package.json index 810deb2..cd0a1b9 100644 --- a/types/chai-withintoleranceof/package.json +++ b/types/chai-withintoleranceof/package.json @@ -2,4 +2,4 @@ "name": "@types/chai-withintoleranceof", "version": "0.0.1", "types": "./index.d.ts" -} \ No newline at end of file +}