diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0d477ef..6717548 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -17,8 +17,24 @@ "Bash(find:*)", "Bash(/usr/bin/rg -n \"onStdout|onStderr\" BatchProcess.ts)", "Bash(timeout 45s npm run test:compile)", - "Bash(timeout:*)" + "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": [] - } -} \ No newline at end of file + }, + "enableAllProjectMcpServers": false +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 445e8f3..048017e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,19 +1,19 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference version: 2 updates: - - # Maintain dependencies for GitHub Actions + # 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 83b4e50..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@11bd71901bbe5b1630ceea73d27597364c9af683 - 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@v2 - 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@v2 + # 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@v2 + - 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 b28aef5..0000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This is used by the build badge: -name: CI tests -env: - CI: 1 - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - runs-on: [ubuntu-latest] - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a - 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-14, windows-latest] - # See https://github.com/nodejs/release#release-schedule - node-version: [20.x, 22.x, 23.x, 24.x] - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm test diff --git a/.gitignore b/.gitignore index f10c721..6d01c21 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.log coverage/ dist +docs/ node_modules npm-debug.log* yarn.lock \ No newline at end of file diff --git a/.ncurc.json b/.ncurc.json index 3de3741..ae639fb 100644 --- a/.ncurc.json +++ b/.ncurc.json @@ -1,4 +1,6 @@ { + "$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json", + "cooldown": 4, "reject": [ "@types/chai", "@types/chai-as-promised", @@ -9,4 +11,4 @@ "rimraf", "rimraf why: https://github.com/isaacs/rimraf/issues/316 (!!)" ] -} +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 1135760..55c1943 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,13 +1,3 @@ { - "overrides": [ - { - "files": "*.ts", - "options": { - "semi": false - } - } - ], - "plugins": [ - "prettier-plugin-organize-imports" - ] + "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 45e960a..26968c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,18 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.ignoreWords": [ - "Equalish", - "cygwin", - "debouncer", - "debouncing", - "rngseed" - ], - "cSpell.words": [ - "sinonjs", - "zombification" - ] + "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 a533adb..bc20ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,20 @@ 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 @@ -37,7 +51,6 @@ See [Semver](http://semver.org/). - ๐Ÿ’” 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` @@ -257,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 index 4f53832..eeec5b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,12 +5,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 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 @@ -18,6 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `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 @@ -51,6 +54,7 @@ This library manages clusters of child processes to efficiently handle batch ope ### 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 @@ -66,4 +70,4 @@ The test suite uses a custom test script (`src/test.ts`) that simulates a batch- - **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)` \ No newline at end of file + - Bad: `if (value)`, `if (!value)` diff --git a/README.md b/README.md index c1fc419..e7a6e8a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# 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) [![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) @@ -51,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), @@ -59,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. @@ -78,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 e8daf4c..0000000 --- a/docs/assets/highlight.css +++ /dev/null @@ -1,57 +0,0 @@ -:root { - --light-hl-0: #795E26; - --dark-hl-0: #DCDCAA; - --light-hl-1: #000000; - --dark-hl-1: #D4D4D4; - --light-hl-2: #A31515; - --dark-hl-2: #CE9178; - --light-hl-3: #008000; - --dark-hl-3: #6A9955; - --light-hl-4: #0000FF; - --dark-hl-4: #569CD6; - --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); - --hl-2: var(--light-hl-2); - --hl-3: var(--light-hl-3); - --hl-4: var(--light-hl-4); - --code-background: var(--light-code-background); -} } - -@media (prefers-color-scheme: dark) { :root { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --hl-2: var(--dark-hl-2); - --hl-3: var(--dark-hl-3); - --hl-4: var(--dark-hl-4); - --code-background: var(--dark-code-background); -} } - -:root[data-theme='light'] { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --hl-2: var(--light-hl-2); - --hl-3: var(--light-hl-3); - --hl-4: var(--light-hl-4); - --code-background: var(--light-code-background); -} - -:root[data-theme='dark'] { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --hl-2: var(--dark-hl-2); - --hl-3: var(--dark-hl-3); - --hl-4: var(--dark-hl-4); - --code-background: var(--dark-code-background); -} - -.hl-0 { color: var(--hl-0); } -.hl-1 { color: var(--hl-1); } -.hl-2 { color: var(--hl-2); } -.hl-3 { color: var(--hl-3); } -.hl-4 { color: var(--hl-4); } -pre, code { background: var(--code-background); } diff --git a/docs/assets/icons.js b/docs/assets/icons.js deleted file mode 100644 index b79c9e8..0000000 --- a/docs/assets/icons.js +++ /dev/null @@ -1,15 +0,0 @@ -(function(svg) { - svg.innerHTML = ``; - svg.style.display = 'none'; - if (location.protocol === 'file:') { - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', updateUseElements); - else updateUseElements() - function updateUseElements() { - document.querySelectorAll('use').forEach(el => { - if (el.getAttribute('href').includes('#icon-')) { - el.setAttribute('href', el.getAttribute('href').replace(/.*#/, '#')); - } - }); - } - } -})(document.body.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))) \ No newline at end of file diff --git a/docs/assets/icons.svg b/docs/assets/icons.svg deleted file mode 100644 index 7dead61..0000000 --- a/docs/assets/icons.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/assets/main.js b/docs/assets/main.js deleted file mode 100644 index d6f1388..0000000 --- a/docs/assets/main.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -"use strict";(()=>{var Ce=Object.create;var ne=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Oe=Object.getOwnPropertyNames;var _e=Object.getPrototypeOf,Re=Object.prototype.hasOwnProperty;var Me=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Fe=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Oe(e))!Re.call(t,i)&&i!==n&&ne(t,i,{get:()=>e[i],enumerable:!(r=Pe(e,i))||r.enumerable});return t};var De=(t,e,n)=>(n=t!=null?Ce(_e(t)):{},Fe(e||!t||!t.__esModule?ne(n,"default",{value:t,enumerable:!0}):n,t));var ae=Me((se,oe)=>{(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 d=t.utils.clone(n)||{};d.position=[a,u],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}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(ol?d+=2:a==l&&(n+=r[u+1]*i[d+1],u+=2,d+=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 l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,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 u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}s.str.length==1&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),y=s.str.charAt(1),p;y in s.node.edges?p=s.node.edges[y]:(p=new t.TokenSet,s.node.edges[y]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+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),l=0;l1?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 se=="object"?oe.exports=n():e.lunr=n()}(this,function(){return t})})()});var re=[];function G(t,e){re.push({selector:e,constructor:t})}var U=class{constructor(){this.alwaysVisibleMember=null;this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){re.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(console.log("Show page"),document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){console.log("Scorlling");let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!e.checkVisibility()){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(n&&n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let r=document.createElement("p");r.classList.add("warning"),r.textContent="This member is normally hidden due to your filter settings.",n.prepend(r)}}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent="Copied!",e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent="Copy"},100)},1e3)})})}};var ie=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var de=De(ae());async function le(t,e){if(!window.searchData)return;let n=await fetch(window.searchData),r=new Blob([await n.arrayBuffer()]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();t.data=i,t.index=de.Index.load(i.index),e.classList.remove("loading"),e.classList.add("ready")}function he(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:t.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{le(e,t)}),le(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");let s=!1;i.addEventListener("mousedown",()=>s=!0),i.addEventListener("mouseup",()=>{s=!1,t.classList.remove("has-focus")}),r.addEventListener("focus",()=>t.classList.add("has-focus")),r.addEventListener("blur",()=>{s||(s=!1,t.classList.remove("has-focus"))}),Ae(t,i,r,e)}function Ae(t,e,n,r){n.addEventListener("input",ie(()=>{Ve(t,e,n,r)},200));let i=!1;n.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ne(e,n):s.key=="Escape"?n.blur():s.key=="ArrowUp"?ue(e,-1):s.key==="ArrowDown"?ue(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 Ve(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=ce(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` - ${ce(l.parent,i)}.${d}`);let y=document.createElement("li");y.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=u+d,y.append(p),e.appendChild(y)}}function ue(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 Ne(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 ce(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(K(t.substring(s,o)),`${K(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(K(t.substring(s))),i.join("")}var He={"&":"&","<":"<",">":">","'":"'",'"':"""};function K(t){return t.replace(/[&<>"'"]/g,e=>He[e])}var I=class{constructor(e){this.el=e.el,this.app=e.app}};var F="mousedown",fe="mousemove",H="mouseup",J={x:0,y:0},pe=!1,ee=!1,Be=!1,D=!1,me=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(me?"is-mobile":"not-mobile");me&&"ontouchstart"in document.documentElement&&(Be=!0,F="touchstart",fe="touchmove",H="touchend");document.addEventListener(F,t=>{ee=!0,D=!1;let e=F=="touchstart"?t.targetTouches[0]:t;J.y=e.pageY||0,J.x=e.pageX||0});document.addEventListener(fe,t=>{if(ee&&!D){let e=F=="touchstart"?t.targetTouches[0]:t,n=J.x-(e.pageX||0),r=J.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(H,()=>{ee=!1});document.addEventListener("click",t=>{pe&&(t.preventDefault(),t.stopImmediatePropagation(),pe=!1)});var X=class extends I{constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(H,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(F,n=>this.onDocumentPointerDown(n)),document.addEventListener(H,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var te;try{te=localStorage}catch{te={getItem(){return null},setItem(){}}}var Q=te;var ye=document.head.appendChild(document.createElement("style"));ye.dataset.for="filters";var Y=class extends I{constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),ye.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } -`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=Q.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){Q.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var Z=class extends I{constructor(e){super(e),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.dataset.key??this.summary.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`;let n=Q.getItem(this.key);this.el.open=n?n==="true":this.el.open,this.el.addEventListener("toggle",()=>this.update());let r=this.summary.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)}),this.update()}update(){this.icon.style.transform=`rotate(${this.el.open?0:-90}deg)`,Q.setItem(this.key,this.el.open.toString())}};function ge(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,ve(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),ve(t.value)})}function ve(t){document.documentElement.dataset.theme=t}var Le;function be(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",xe),xe())}async function xe(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let n=await(await fetch(window.navigationData)).arrayBuffer(),r=new Blob([n]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();Le=t.dataset.base+"/",t.innerHTML="";for(let s of i)we(s,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function we(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-index-accordion`:"tsd-index-accordion",s.dataset.key=i.join("$");let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.innerHTML='',Ee(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let u of t.children)we(u,l,i)}else Ee(t,r,t.class)}function Ee(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));r.href=Le+t.path,n&&(r.className=n),location.pathname===r.pathname&&r.classList.add("current"),t.kind&&(r.innerHTML=``),r.appendChild(document.createElement("span")).textContent=t.text}else e.appendChild(document.createElement("span")).textContent=t.text}G(X,"a[data-toggle]");G(Z,".tsd-index-accordion");G(Y,".tsd-filter-item input[type=checkbox]");var Se=document.getElementById("tsd-theme");Se&&ge(Se);var je=new U;Object.defineProperty(window,"app",{value:je});he();be();})(); -/*! Bundled license information: - -lunr/lunr.js: - (** - * 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 - *) - (*! - * lunr.utils - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Set - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.tokenizer - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Pipeline - * 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.trimmer - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.TokenSet - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Index - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Builder - * Copyright (C) 2020 Oliver Nightingale - *) -*/ diff --git a/docs/assets/navigation.js b/docs/assets/navigation.js deleted file mode 100644 index 0fa6114..0000000 --- a/docs/assets/navigation.js +++ /dev/null @@ -1 +0,0 @@ -window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA42UUU/CMBDHv0ufUWQKKo8ixgeCBEl8MD7U7WANpV3ag7AYv7sZA+m69cZr//f7pb277fOHIeyRDdkTxzgdya1FMKzDMo4pG7JYcmvBdt30OsWNZB22Fiphw1708NtptLxlKLSytOxY1OqcGR2DDcmOKWV5hiUYA0ndcEooes4R6mRxSlELbtd1qji9tIfjHSh0Xi0Uglny2OtiWVaVRv1BoIm1wfjWah2lHaVCJsfyFx6jNnmjtqGO0k70auWuoWMqIwqecWMDcBlR8CLPIDm0c7wRiAFPraq19adB+VLMM3+Wjcabx/teP/I7P94LnAO3WvlGL26zfaT5VOMrcIlp7rsq4WWmOfAk4DlEra/TymoJ/h7suBH8WxbvcwuqttuouklN+ESvaGgCO5A2gJYhIZjq8M1PGYG/i00mwd/j5VbFhw+y6+ZVzeDO0ayFlE14cU5g0rv6GSyTqx4BZ6LYOuv+s878f0gLQiyFWUC/5Wf2P6wJvv4ANd1pyAEHAAA=" \ No newline at end of file diff --git a/docs/assets/search.js b/docs/assets/search.js deleted file mode 100644 index cf04735..0000000 --- a/docs/assets/search.js +++ /dev/null @@ -1 +0,0 @@ -window.searchData = "data:application/octet-stream;base64,H4sIAAAAAAAAA9VdW7PbOI7+L86r+7RI3fM2nc7sdlXS25Okdh5OpVKKzZOjiS25JTk5qVT++xapGwgDMuVbal8mZ9ogAOIDQfIjJX1fVOXXevH8/vvic16sF89lGC0XRbZVi+eLF4/5Zv1XVa5UXf8zWzVl9W2xXOyrzeL5Ii8aVT1kK1X/SojdPTbbzWK5WG2yulb14vli8WPZmxCeDAYbu5PVPztoCqwtF7usUkXD9GJ0JgpDf+zxhw/Nt506w4u7QYOzM3c7zjchk8Gz37Jm9fhis68bVQ3+dTZ+hT9Ohj4UctC4Kou6qfba6HGFz2xpunOWjwze5a7Jy6J2sDhKnm5NbfPGKWDPRskz+la4dKuYa2MyRacMdcn4i3SyeFcWbMceHlwMGqmbdO3h4dhAs3v28ACMykiKAORIsVZrlwzRcr+IuV2UXpBAW26WzrTy917t1bus/uxkDUrPs2pHMq//WG9c8BsEz7G2U8U6Lz5pv1+U+6JxsEs0OceDrcoKrav+S1W6pjt4QDQ5x4N6l30tlJlQXGNANDkLhRmmdxey+XFff5vTZSx/VsSbrGry4tOskBNtLpT5LhMpEj/H8mpfaXlXy0j8rNqil2JFtnlZVWXlGniy0el1dZevneLdip1up26yxsVQL3eLKdfYujP/+4voFxah2/QL2rLbgPnFnPboDmtyXf0Adf3agvG1y+qZFYDyldB0UU8rla1nVUrGzwM9F/Vymz1dwEek5aIenlR8GD8PdV0+Q82EYwy8yRr1l6pe58W+OX2Y3/EKL5sJ9W/qoazUn+qpeavXKWekA6HqsjE+YenFBBdpukLF0qzDy2Jt1DvPLES9svRcxM+LTkcHDva/RKd6ijTyA26tKheaYa7fd61mx23n8Q44F47r9KXXfqv+fKzKz+qMKsL3pdV8q36sNmXtRFLM7ker+Vb9aNdm1+hHq/mG/bgOHC3J5N+mF7kbWzO7E1rvrZAoN1fBodzcbExoVqQdhtfoiNb+wWi/aX/Web0qi0KtztkdTXdqNHHTnqlrzY6mU+qWs2Pbn6f8ehhp5bdbu+iV0vXwafXfFqG6WefFVbuUFzfvUblvrtqlct/ctk9NvlXl/iqjqFN9s56U5eus+HaVnrSqb9WTffGosk3zeJW+DMpv1ZuvZXWVXYzW+/9l7d+t8C9Ojpy5lm9X7MEFvLLOAFY6pC+17uHKiNOJC93srDOfueTRQYNbnE+gVEQpP+DliNOktrNJIFdf7waN57p8SdpnhvdA65V74Ez0OHs/aLyy587UjrPng8Yre+5c0J09HzRe3/PLhrxXeGW/HQkbZ7dz11s4Z3ntRtE4O92qu7LPs0gZZ9dHMuZGPZhFw8zrhqX6Fn1x3TLO64a6zfw0h2qZ2YFW6dVXCLPIlZkrnFuhMI9OmdOJnka5SR/mEChzOjEQJ9fvhTtl4tyBUeW1fXcmSdx9H1Re2fc5tIiz91Dplf13JEKcXe/0Xd5rezOvp3z46Idy2keTrc64Vqia1+0lKacrIJb06Va/ZKv9futq1ZaeZZV7WuZ/HJ446WQu/uwM1Dv7EZre8elrc+4de7adg6eb9X98Uq/zzSaf7wVsebY3ZaEfKPhDX637km3mesS0vkSM3qisLovs40YND33lm32lardLeThwTurO9ttcSHvXTqdzY0m2PT+SefG72mTfflPNV6UKc6Fvdt5N6TjbwyarP58YMqrpJXIPPmei6nkj9LDt2R6pYv1fVbZSD/vNv7O80T2eG6kpFednfVOpbPvPzb5+nJ30RNOz/VltVFbsd470vz3TEE0vkVG6TL4+NaWIxpfwSVdAtT4n1zkNZ3vXropfPKrV51OnpikVZ/u3y9dnOce2P9uzTfnp05HHdi1PBvlTLM8+cSJM3w3NnD2465yeWMFOZnP34+XWrFCh+2K195FB0vxz3FwndrqdXc6QzJaZVup0K+ZIy8FOL3empf1OV6U/XHqG5U+3/DCUw4nnGizbhy3OwJF9jtYG8uizs0fslDvuLNuy04nNssM/M+ky7izpc+w2MxBsTsWOeTrWNWGPnvYdtTh1CGnZczlbdLHGHRxiY8fOA4/besobN2O94DnWvj5++7Ns/nuKHLSMYvlzbE9SkpZVJ57xqD3+GNUydvx01DGqb/Tjk84x7aXPsVs5WqwuYCs363rHeL6eX00hmVjti8JtuI+Sp1sryuaNs0FL+HSb2+zbR/VmX7Qja6WX1w7G6Vane6Ge1MpxwgKiZ9jj3ouCq+rsOR8sqX9XD6qqiJLa/3CRpbSlzGkZPfjFL4m2OXERwbY0SjlboV8rcczOkYn0uJ2H/eYh32yOAfEMyp1qq1L/UStqGrVNAbFTLdWqaY73aZRytwMHSvNI3HKzTXQip+lf6WF0LKc7mdMsVKouN1+OZfModaqV/1CXXijUT7ZRfqxVdbQno9RZVv61z1WzOZzTSWOj8GkV574p3zaVeYvGp/dHTH4Ash/mWIxGe2/z7W6j/sqqGpA+D/tiZYiSX+HPkzUaqPycbzaEKv2fXVXs8vXLp7wG+8RRz/DbDGWMnmkVcALT7144QEP/x4tMXIMip0nL+MK+zKXKyzWxJhxNAJH5+r9m1Xa/m9QPRJz020OuePlFEfvmUf0o4aYd7ex0W3prPpqwhE6xsq3f5sVKvcrq5lh3CNHTLP6lquO2gNDJ0TMHnlMJYEudZeetWpXE+pSwNUie1y/6XJjq2/SR71SO6yOiyRrQ/e42OkGRIrcN+j9epEgNipyKlPFl4sCWYFZHA800lTqle1VutxmRMtD7XmK+9p09Tx4qHwScdB++0I/cbQD1R3YaR/Qzuwyg/8gOY1q/frhqyvv+dzfd9rTwlqT/R92jxCmeV/tC32EkytpoAcrM70G/VJvK+lHktAitqWuddojWU9c0j+qnnrFC+qcemprSz+waQPindwwHutNYhGNhg4eAL9GbifXpoX3K2ElMlk34tm6rrZkhBuXgPdaHUtPLz4PXSNlj4LjmZ1arCfLGdt3xeHaW/XkPAXY6gPtTYXkJyr2rUy8d3/N7nZC8LNZ9QNyeqYUB0a67P0zo4NLMhwUvHpLRfh8UkcyJCvCfe3gLvvhvjmu44U+IjuXCEKBZ48juBXv23JwQIKvVT4jOaH8IjTcnNMD/icXq71mTzfEKtPkJMemtDyVmVkQG3yfi8aalCGeVXdTuJ8Wl92CIzayBZPXh+G3Uuc69m/EYzVWi0zkwBMft3Y0gOH0PJmIzu8TARj8pLlaBkbOm7NF77pZW+e6ECmO1+glRGe0PdXdWsgD/j9/gnJ0yRNufECPsxRCpWQl00Bf+jvfsOIE2PyE+vfU+LrMWfYPrTDg+mlcQz9wawEY/ISCD+TlXWjsFo+t8fswE56dlxfCSpJkJMbU77+4n4CfSsEO2mPv+/Iuq6rwsXiD20UH9s4Omx29ZuF9+P8UhsvnZTu2yelbYn3UNzjasr8jOMtw1ONuwvhx4Svztdqe4gdkv8zTKy6e8aR9iQ8wX+tWZ9UKHxqBrDufFUNG7bzu1NsMYU3NA54HQtHqbu1yRJYhW+awTZ3jGA19Zo7NMXsAg+FKYg8WJL4Y5m9RfbpthsxM/0+gmrxtVqIqsJoxl2OZM85Xall/UPzabVyf4QTY+xSF7fP+bvEHcjm7rtyNjhtJq36CFOs0vrpcwatW8sp8sGm9iDL+5KttwmtoffhHOpQf5BBB08Mje2+qnI48oetYL0ZC/OngqCRpYq4/7T8cM9EKnGMiLh/KY/k7mFPVfs4osiVB9J3OKesVtfqD+6VesHBjwJUyUV+qL2oyj/UtW5fpxcGOh/W0yW4CyF2VRlxuFkm9UaP3uqvTPktXX/+Sq6lX5ie7m9ACdWvkfqnl2ZJOjneBSKW8ep9CA+u8sYdbU4WtETusOMNdvY/hPCox2oZcTndb8Wt1k251rx60Gt+n8aHJ4U6trAIC33GI+3zSqcsYeiV8/ANDgwPe4dN/y9Mf75SIv1upp8fx7v7VcPF/IO/8uXSwXD7narPWXo1v3lubSi9b4vvvtf5W+m6MlWpFfvcXy3luG3l0s3r9f3vcNzH83/8FIicXyXhBSwpKSi+W9XEp558eJJSYtMX+xvPcJZb4lFSyW98HSj+68ILbEAkssXCzvQ0JZaElFi+V9REhFllS8WN7HyyC8izxpicWWWMJ1M7HE0sXyPqG0pXZoPU6dQBjoYKdLP76LhR0RYcOgWcR74S0DeSd9JGkjIXTIBQmsDYY+VbwXkhK04RA67sKnui1sSDQrfC8CSqWNitDRFyGp0gZGHw7fCwpnYUMjNAQipgRtcPQB2b1ICEFpgyPNCEkpQTRGzCChRpy0sZEaASmobksbHH1SdS8lKWmjIzUEkhp70gZHn+8wQ9kGR8Ys3tIGR5phQ+EtbXCkhkBSY1ra4Phm5FBw+zY4vmBD6dvo+AYdKjF8VMMMOlRi+DY4fsBmr2+D4xtwUlLSRsdn0fFtdHwNge+RKm10fI2BL6ia4dvw+BoEn8w238Yn0Cj4ZG4ENkCBRsEPln56F8vAlrQBCiRXBAMboMDMMiHVoQBNNBoGPyLdtBEKNAw+OUMENkKBxsEnq39gQxQYiEjUAxuiQOMQkGAGNkSBxiEg8z2wIQo1DgEJZmhDFGocAhLM0IYo1EAEASlpYxRqIAJybIQ2RqFZDZAYhWg9oIEISIxCG6MwYpMutDEKYy7pQhui0EBEVYXQRihM2bEe2ghF7PIgsgGKBDvWIxugSLJjPbIBinx2rEc2QFHAjvXIBigK2bBHaNEWcWGPbHyimB3rkQ1QlLBjPbIRilJ2rEc2QrHHjvXYhigW7FiPbYhiyY712IYo9tmxHtsQxQE71mMbojhkx3psQxRH7FiP0do6Zsd6bGMUJ+xYj22M4pQd67GNUWLqHLU8S2yIEo1DSC3PEhuhRMMQUkvnxAYo0SiE1NI5sfFJ2B1PYsOTmD0PtYpLbHQSDUFILbkSG5xEIxBSS64EbX00ACG15EpsaBId/5BaSCU2MqmOf0iVzNRGJjXIUBCmNjKpjn9EQZjayKQ6/hEFYWojk+r4RxSEqY1MquMfUcikNjKp2ZBSyKQ2MqmOf0Qhk9rIpDr+EYVMiralOv4RhUyKN6YagCgh95Ee2pt6gt/Fot2pZ/ChgGx/gqI+NyTan6CoRiKmUG9/gqIai5jc9Xpoj+ppNGJy3+uhXapn2AMK+/YnKKoRicmtr4c2qp7GJKbwb38CooYuiMnd7wGToCGJyf0v5hIMYxBT41NgMsFwBjEJLKYTDGtALwIEZhQMb8DwHgguwxwkZBJgUsFQBwk5bwpMKxj2ICGzABMLhj+glyICcQvCUAgJzZUgekEYFiEhU0Zi9kfDkpApgygGYYiEhEwZxDEIwyQkZMogkkEYLiEhUwbRDMKwCQmZMohoEC3TwLBaCDDDKKRkHiCyQRhOgS4xiG4QhlVI6ZRBjIMwvEJKLrYE4hyEoRZSsnL4mK/TqKRkGiDeQRh2ISXTABEPwtALKZkGiHkQhmBIyTRA3IMwDENKpgEiH4ShGFJyYSwQ/SAMySA8ElxEQAhDMwiPLPWIghCGaBAezXEixIKWYSUhQzyEMGyD8EjMAsyyGrLII0FDXIQwjAPN+gnERgjDOQiPRBjxEcKwDvSAQISEMLSD8MhsQJSECFrYyHRApIQIW9jIsoBoCWHIByHIdEDEhDD0g6D5bkRNiLClxsl0QOSEMBSEEGQ6hJgfNwQ5zXsjgkIYGkIIMh0QRSEMEyFo8huxFMKQEYLmvxFRIUK+PiKmQhhCQpBsuUBkhYha2EiIEV0hDCkhJLkbFoixEIaXYIo04iyEYSaYIo1YC2G4CSHJ3InwyYbBTZK5g6gLYQgKQXLyApEXwlAUguTQBaIvhCEpBEmjC0RgCENTCJJJF4jCEIaoYEo1IjFE3AJH5hmiMYQhKwTJqAtEZIi4PY0ikwdRGcIQFvTGBHEZwjAWwidLSYzPpGJeLUIt5ql1gQgNYWgLRi0CzRAXwicTEpEaIuF3Z4jWEIa9ED6Zu4jZEIbAYNQiyAyFIXwyzRG9IRIeMkRwiKSFjBwRiOMQCQ8ZYjmEITOETw4eRHSIhIcMUR0ibSEjxxliO0TKQ4b4DpG2kNHHmQiylIcMkR4ibSEjRyTiPUTKQ4aYD5G2kJGDF5EfIuUhQ/SHMCyHCMjBixgQkfKQIQ5Eeh6765CIA5E8ByIRByIN0SECavBKRIJIw3SIgBqRErEg0lAdIiBPeBENIg3XoV91Qcmi42CvPawnD2URESIN2yECKsslYkKk16JGpa5EVIj0Un4ZIBEZIg3jIcijGInYEGkoD0GywxLRIbKlQ0gXEB0iDedBHgtIRIfI9noFSTtLRIfI9oYFyTxLxIfI9o4FST5LRIjICUJEIkJEtoQIfeqPGBHZ3rUg6WqJb1tInnGU+MKFoT04WXznwgw3kgmX+NqFIT4ESYZLfPFCtriRqY6vXsgWNzLV8e0L2eJGZi++gNHyIiQxLvEVDNlekKFvnyDcDPshSHpcImZEGvZDkAy5RMyI9PkqiYgRadgPQfLpEjEj0mcnNomYEdleySDZd4moEemzE5tE1Ig0/IcguXqJuBHpsxOb7LgRc8/vi6oatf6jve93fz9cNvy++NBdAhxu5n5fyMXz7z+Wi6T9R3jtvzJq//W7fyOv/110f8Rh/0fc/5F2fyS9TOL3f/TCSS+c9MJpL5z2wmkvnPbCaScse+sa8+4Pv/8j7P8wrX6Mlxj1/9Ox+qgfcVp135oDwfDHYPgOTVX/SNGoQW9WBh16j+KgpXurD1SSQCUurpT9w4ejlhBAm0zq2PUfJBkbRynwwJvsRtea8ECvT8bs4rWY50vNU6GwrQc8SBOubfc9d4Ah6LbfZUvEdn9ff9P+r9o3MgLzEMaUad29MndslcJW7YChmumH4nB3fdjd+FhT468VawkA6wdAyDpu1Dzl+gNX5qE9GPcYYsbF3WjokH/I9JXgb1AL6Iw3paH7ogpMfZh4ccA1bj+FNXhh5y4Yxm0FYTTYI1dEwHLIlQ7z+U7Ye2XZDoD7Cdt1rcOC3wcuB12BjdjO9w9wQudh2CLBtjQPf/TPOIH2EUBMz+R8++HtjKDTwPluDgn6SaIv5bKv8kFfyvns1MltvrVB4xuA6hiyrrZfXCEHtwQO831tFTTmxZdj2xgOccEGemxsj1IrzEzj7kErgE4YwmZcMVsPb8IHFQmWFbav7dOT0CD0U3CJSMx/YAxxNQxXPsvDPlvifprnxlD3LRbQU4hL2zro1IWdtqhbwuhjX15r+xwXLM1QdTrRrU/dVwy/Znmj3+e47b6cBsIDJsSUw7F/USYoD3CEdZ0L+/VYv0biYSr+3qu9wpksBJysuLYH0QgTmItsNPSi5nBWtcAOudHTroh2qtp2b8KFGkKoYdK80WAvSIQPW7MB61vX3Zt/oYYAamDz6EmtcLwTmEZ84J7yhirwQQSnZW526j8aBKzCiAvOavteAmgOVhxvqpla644eQB3DOLHLCPOONGLEgTWcJo2Zxua5pk33BBaczKDz7DIMfIgCVBGYID5n+XH8IAuJlQ+Dx2UZUEJEAO4AUgcVefe1RqLmgMxh19HDJ5hA5kAQBNew/ZwSKFUg9kE3+Uf9lM+upLUWe6gmcJyzU2X7zDCsTnCYSC5u/VsLibjDosgu4Kz2h8sLuOvpQuBztS6vcQQFjDvXg/ZDCHAWheMt4Mo5eFkBDBp0mJ0bN6W9KIkgdeBzoTpca0Ywtb2eZZDDH1ykNuUnYqyHKQScq4vb7GmsVbqyU1tekHHphKI2VxkdAAV2pbDNnnTT7BOxQAiBguSIgsPEA8nPAtI1tq2CgpVw+G+zp2r4UPmw9Ws/VE7O1XAsJlz92GZPU5iA2SvlioD5TFa1Lx7hZ7JAJYH5JtneqayAjtgFHW6uOAVaeKNdMR8iN19MJ/AFzqQsRHVLiRTqqTF6LJCBBp8b5yY/Vfs5Brh4gaUl5BvX+rsRm6xuCBWwQIYcJEYCrgbgKoSdEYuS2pnC4iS5CaQoNXhr89JCWM4tGof1tmyGD8uBvIFt2alk+BAQrMWwtz7nctfy7/6rPlABDHLApYl5zw7wF2RWV0vZZX25sekHMFKDbusecSPW5otAkPo9m+Di3L79CI4rWLzZzXRZHKahBxfTIRuhQhdrfmkE99UJV/I153HAUsGBFHElQbds31oPm0KaKeY9r7sX6sOmcE0UsXlFUMGAMOFbWU1i6Kbg3Oy/SQGdhMkfdTtwfbzKaagRZwzJGY9zd3e4SU4hJv2pgGBZtE4DuXmxt4ldF7j8AIrsnljJzTbWX0xCu1QPVp6AG4fmq9cAMFgz+Ljl62PbBWA8ZVHLNX1co2MLD+ZnwAbefDMLTGhwHdnzhgHbATQ3x9Amn2VVuboztKtV9kBXgw5mdlgZFeu8XpVFYb4WAfSA0ht0tAw7so2eg40H5Jh6hjRmk8aoeLI5O3heEnRneOz5E33mAYc8m3Us5w/zj2/cfmMGjFqLherPFtkDmypDfJAHnQ64Stx9ERdMlnBJx45O04xeakOKgUuZ/rsioLdw2vL7k1YW6PEDlkAF9JxdALZvg8s2G2bHB8ccu6QdPtcIrMPFNHu+Sq2mIE4shd19CAfVQwnLccQZrVVDLB/h1kay/EOtGnJfBMZUwoE0fPwTRAkWJZ8rhLX5ACIxhXqwvwE3hs3OQK3poQwd6MoJ74dW1LSvVyfWSKBIJmz88AophpMxyx+aZoe1EJSEfoz0Cwl94fWYLl0jyA0pPJtnub3xC/agN9bhApe8fUsaEUibc3OD0bDf6aUEmtxhRrDsb/cRLDhyYHWNuDlVt7OnZDho+FZ4bQvPsP3+zIUdNqb54UwI7wwE3UaGvQJRN+u8IHSA7Au7OYU9S24X2oQSEISwW07GfDAqlW0fNvv6kVhSAXdYLvfweAauAtlzTpp8h1Wa3Y7RO2aYacmUswR5Cms8y24cZreAg0NEUw2r4TMf0C6cElnWTDdv+s9gwNawVrI8FWhN1EhQH1Iu4dsvOINJAkbL5xKL8BjukMKuLrI3Rpqy3GaFtfaBZzthx0/HXE1pyrr7chwsKxYhxoasa9pkdmvPIrjZcLXvM4VzGtwZskcC+oLb2hAG1PUseHdAstvanku0wwbwCrt9CjsbfclW+/32cEEBiht7rtW9lY88WYLrNo+riu1ncvGmEo6xgJvI27ekwqDD60DsXPD18VtRNtQhEpyAxMAQcdnW6jlcrcOiNJwUsHsU/b5H6lgQzmrs/K8bN+DVllABTEB2OfW1tEMIL1SF3S4zJuP/frnY5Tu1yQu1eH7//seP/wOcZC8CyMsAAA=="; \ No newline at end of file diff --git a/docs/assets/style.css b/docs/assets/style.css deleted file mode 100644 index 778b949..0000000 --- a/docs/assets/style.css +++ /dev/null @@ -1,1412 +0,0 @@ -:root { - /* Light */ - --light-color-background: #f2f4f8; - --light-color-background-secondary: #eff0f1; - --light-color-warning-text: #222; - --light-color-background-warning: #e6e600; - --light-color-icon-background: var(--light-color-background); - --light-color-accent: #c5c7c9; - --light-color-active-menu-item: var(--light-color-accent); - --light-color-text: #222; - --light-color-text-aside: #6e6e6e; - --light-color-link: #1f70c2; - - --light-color-ts-keyword: #056bd6; - --light-color-ts-project: #b111c9; - --light-color-ts-module: var(--light-color-ts-project); - --light-color-ts-namespace: var(--light-color-ts-project); - --light-color-ts-enum: #7e6f15; - --light-color-ts-enum-member: var(--light-color-ts-enum); - --light-color-ts-variable: #4760ec; - --light-color-ts-function: #572be7; - --light-color-ts-class: #1f70c2; - --light-color-ts-interface: #108024; - --light-color-ts-constructor: var(--light-color-ts-class); - --light-color-ts-property: var(--light-color-ts-variable); - --light-color-ts-method: var(--light-color-ts-function); - --light-color-ts-call-signature: var(--light-color-ts-method); - --light-color-ts-index-signature: var(--light-color-ts-property); - --light-color-ts-constructor-signature: var(--light-color-ts-constructor); - --light-color-ts-parameter: var(--light-color-ts-variable); - /* type literal not included as links will never be generated to it */ - --light-color-ts-type-parameter: #a55c0e; - --light-color-ts-accessor: var(--light-color-ts-property); - --light-color-ts-get-signature: var(--light-color-ts-accessor); - --light-color-ts-set-signature: var(--light-color-ts-accessor); - --light-color-ts-type-alias: #d51270; - /* reference not included as links will be colored with the kind that it points to */ - - --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-background-warning: #bebe00; - --dark-color-warning-text: #222; - --dark-color-icon-background: var(--dark-color-background-secondary); - --dark-color-accent: #9096a2; - --dark-color-active-menu-item: #5d5d6a; - --dark-color-text: #f5f5f5; - --dark-color-text-aside: #dddddd; - --dark-color-link: #00aff4; - - --dark-color-ts-keyword: #3399ff; - --dark-color-ts-project: #e358ff; - --dark-color-ts-module: var(--dark-color-ts-project); - --dark-color-ts-namespace: var(--dark-color-ts-project); - --dark-color-ts-enum: #f4d93e; - --dark-color-ts-enum-member: var(--dark-color-ts-enum); - --dark-color-ts-variable: #798dff; - --dark-color-ts-function: #a280ff; - --dark-color-ts-class: #8ac4ff; - --dark-color-ts-interface: #6cff87; - --dark-color-ts-constructor: var(--dark-color-ts-class); - --dark-color-ts-property: var(--dark-color-ts-variable); - --dark-color-ts-method: var(--dark-color-ts-function); - --dark-color-ts-call-signature: var(--dark-color-ts-method); - --dark-color-ts-index-signature: var(--dark-color-ts-property); - --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); - --dark-color-ts-parameter: var(--dark-color-ts-variable); - /* type literal not included as links will never be generated to it */ - --dark-color-ts-type-parameter: #e07d13; - --dark-color-ts-accessor: var(--dark-color-ts-property); - --dark-color-ts-get-signature: var(--dark-color-ts-accessor); - --dark-color-ts-set-signature: var(--dark-color-ts-accessor); - --dark-color-ts-type-alias: #ff6492; - /* reference not included as links will be colored with the kind that it points to */ - - --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-background-warning: var(--light-color-background-warning); - --color-warning-text: var(--light-color-warning-text); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-active-menu-item: var(--light-color-active-menu-item); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - - --color-ts-keyword: var(--light-color-ts-keyword); - --color-ts-module: var(--light-color-ts-module); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-enum-member: var(--light-color-ts-enum-member); - --color-ts-variable: var(--light-color-ts-variable); - --color-ts-function: var(--light-color-ts-function); - --color-ts-class: var(--light-color-ts-class); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-constructor: var(--light-color-ts-constructor); - --color-ts-property: var(--light-color-ts-property); - --color-ts-method: var(--light-color-ts-method); - --color-ts-call-signature: var(--light-color-ts-call-signature); - --color-ts-index-signature: var(--light-color-ts-index-signature); - --color-ts-constructor-signature: var( - --light-color-ts-constructor-signature - ); - --color-ts-parameter: var(--light-color-ts-parameter); - --color-ts-type-parameter: var(--light-color-ts-type-parameter); - --color-ts-accessor: var(--light-color-ts-accessor); - --color-ts-get-signature: var(--light-color-ts-get-signature); - --color-ts-set-signature: var(--light-color-ts-set-signature); - --color-ts-type-alias: var(--light-color-ts-type-alias); - - --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-background-warning: var(--dark-color-background-warning); - --color-warning-text: var(--dark-color-warning-text); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-active-menu-item: var(--dark-color-active-menu-item); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - - --color-ts-keyword: var(--dark-color-ts-keyword); - --color-ts-module: var(--dark-color-ts-module); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-enum-member: var(--dark-color-ts-enum-member); - --color-ts-variable: var(--dark-color-ts-variable); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-constructor: var(--dark-color-ts-constructor); - --color-ts-property: var(--dark-color-ts-property); - --color-ts-method: var(--dark-color-ts-method); - --color-ts-call-signature: var(--dark-color-ts-call-signature); - --color-ts-index-signature: var(--dark-color-ts-index-signature); - --color-ts-constructor-signature: var( - --dark-color-ts-constructor-signature - ); - --color-ts-parameter: var(--dark-color-ts-parameter); - --color-ts-type-parameter: var(--dark-color-ts-type-parameter); - --color-ts-accessor: var(--dark-color-ts-accessor); - --color-ts-get-signature: var(--dark-color-ts-get-signature); - --color-ts-set-signature: var(--dark-color-ts-set-signature); - --color-ts-type-alias: var(--dark-color-ts-type-alias); - - --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-background-warning: var(--light-color-background-warning); - --color-warning-text: var(--light-color-warning-text); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-active-menu-item: var(--light-color-active-menu-item); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - - --color-ts-keyword: var(--light-color-ts-keyword); - --color-ts-module: var(--light-color-ts-module); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-enum-member: var(--light-color-ts-enum-member); - --color-ts-variable: var(--light-color-ts-variable); - --color-ts-function: var(--light-color-ts-function); - --color-ts-class: var(--light-color-ts-class); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-constructor: var(--light-color-ts-constructor); - --color-ts-property: var(--light-color-ts-property); - --color-ts-method: var(--light-color-ts-method); - --color-ts-call-signature: var(--light-color-ts-call-signature); - --color-ts-index-signature: var(--light-color-ts-index-signature); - --color-ts-constructor-signature: var( - --light-color-ts-constructor-signature - ); - --color-ts-parameter: var(--light-color-ts-parameter); - --color-ts-type-parameter: var(--light-color-ts-type-parameter); - --color-ts-accessor: var(--light-color-ts-accessor); - --color-ts-get-signature: var(--light-color-ts-get-signature); - --color-ts-set-signature: var(--light-color-ts-set-signature); - --color-ts-type-alias: var(--light-color-ts-type-alias); - - --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-background-warning: var(--dark-color-background-warning); - --color-warning-text: var(--dark-color-warning-text); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-active-menu-item: var(--dark-color-active-menu-item); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - - --color-ts-keyword: var(--dark-color-ts-keyword); - --color-ts-module: var(--dark-color-ts-module); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-enum-member: var(--dark-color-ts-enum-member); - --color-ts-variable: var(--dark-color-ts-variable); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-constructor: var(--dark-color-ts-constructor); - --color-ts-property: var(--dark-color-ts-property); - --color-ts-method: var(--dark-color-ts-method); - --color-ts-call-signature: var(--dark-color-ts-call-signature); - --color-ts-index-signature: var(--dark-color-ts-index-signature); - --color-ts-constructor-signature: var( - --dark-color-ts-constructor-signature - ); - --color-ts-parameter: var(--dark-color-ts-parameter); - --color-ts-type-parameter: var(--dark-color-ts-type-parameter); - --color-ts-accessor: var(--dark-color-ts-accessor); - --color-ts-get-signature: var(--dark-color-ts-get-signature); - --color-ts-set-signature: var(--dark-color-ts-set-signature); - --color-ts-type-alias: var(--dark-color-ts-type-alias); - - --external-icon: var(--dark-external-icon); - --color-scheme: var(--dark-color-scheme); -} - -.always-visible, -.always-visible .tsd-signatures { - display: inherit !important; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.2; -} - -h1 > a:not(.link), -h2 > a:not(.link), -h3 > a:not(.link), -h4 > a:not(.link), -h5 > a:not(.link), -h6 > a:not(.link) { - text-decoration: none; - color: var(--color-text); -} - -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; -} - -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -.container { - max-width: 1700px; - padding: 0 2rem; -} - -/* Footer */ -footer { - border-top: 1px solid var(--color-accent); - padding-top: 1rem; - padding-bottom: 1rem; - max-height: 3.5rem; -} -.tsd-generator { - margin: 0 1em; -} - -.container-main { - margin: 0 auto; - /* toolbar, footer, margin */ - min-height: calc(100vh - 41px - 56px - 4rem); -} - -@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 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: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - 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 { - position: relative; - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; - padding: 10px; - border: 1px solid var(--color-accent); -} -pre code { - padding: 0; - font-size: 100%; -} -pre > button { - position: absolute; - top: 10px; - right: 10px; - opacity: 0; - transition: opacity 0.1s; - box-sizing: border-box; -} -pre:hover > button, -pre > button.visible { - opacity: 1; -} - -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 .tsd-index-panel h3, -.tsd-index-panel .tsd-typography h3, -.tsd-typography h4, -.tsd-typography h5, -.tsd-typography h6 { - font-size: 1em; -} -.tsd-typography h5, -.tsd-typography h6 { - font-weight: normal; -} -.tsd-typography p, -.tsd-typography ul, -.tsd-typography ol { - margin: 1em 0; -} -.tsd-typography table { - border-collapse: collapse; - border: none; -} -.tsd-typography td, -.tsd-typography th { - padding: 6px 13px; - border: 1px solid var(--color-accent); -} -.tsd-typography thead, -.tsd-typography tr:nth-child(even) { - background-color: var(--color-background-secondary); -} - -.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-full-hierarchy:not(:last-child) { - margin-bottom: 1em; - padding-bottom: 1em; - border-bottom: 1px solid var(--color-accent); -} -.tsd-full-hierarchy, -.tsd-full-hierarchy ul { - list-style: none; - margin: 0; - padding: 0; -} -.tsd-full-hierarchy ul { - padding-left: 1.5rem; -} -.tsd-full-hierarchy a { - padding: 0.25rem 0 !important; - font-size: 1rem; - display: inline-flex; - align-items: center; - color: var(--color-text); -} - -.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-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: relative; - 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-navigation.settings { - margin: 1rem 0; -} -.tsd-navigation > a, -.tsd-navigation .tsd-accordion-summary { - width: calc(100% - 0.25rem); - display: flex; - align-items: center; -} -.tsd-navigation a, -.tsd-navigation summary > span, -.tsd-page-navigation a { - display: flex; - width: calc(100% - 0.25rem); - align-items: center; - padding: 0.25rem; - color: var(--color-text); - text-decoration: none; - box-sizing: border-box; -} -.tsd-navigation a.current, -.tsd-page-navigation a.current { - background: var(--color-active-menu-item); -} -.tsd-navigation a:hover, -.tsd-page-navigation a:hover { - text-decoration: underline; -} -.tsd-navigation ul, -.tsd-page-navigation ul { - margin-top: 0; - margin-bottom: 0; - padding: 0; - list-style: none; -} -.tsd-navigation li, -.tsd-page-navigation li { - padding: 0; - max-width: 100%; -} -.tsd-nested-navigation { - margin-left: 3rem; -} -.tsd-nested-navigation > li > details { - margin-left: -1.5rem; -} -.tsd-small-nested-navigation { - margin-left: 1.5rem; -} -.tsd-small-nested-navigation > li > details { - margin-left: -1.5rem; -} - -.tsd-page-navigation ul { - padding-left: 1.75rem; -} - -#tsd-sidebar-links a { - margin-top: 0; - margin-bottom: 0.5rem; - line-height: 1.25rem; -} -#tsd-sidebar-links a:last-of-type { - margin-bottom: 0; -} - -a.tsd-index-link { - padding: 0.25rem 0 !important; - font-size: 1rem; - line-height: 1.25rem; - display: inline-flex; - align-items: center; - color: var(--color-text); -} -.tsd-accordion-summary { - list-style-type: none; /* hide marker on non-safari */ - outline: none; /* broken on safari, so just hide it */ -} -.tsd-accordion-summary::-webkit-details-marker { - display: none; /* hide marker on safari */ -} -.tsd-accordion-summary, -.tsd-accordion-summary a { - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - - cursor: pointer; -} -.tsd-accordion-summary a { - width: calc(100% - 1.5rem); -} -.tsd-accordion-summary > * { - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; -} -.tsd-index-accordion .tsd-accordion-summary > svg { - margin-left: 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; -} - -.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, -#tsd-toolbar-links a { - 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 { - background-color: var(--color-background); - line-height: initial; - padding: 4px; -} -#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:not(.no-results), -#tsd-search .results li:hover:not(.no-results) { - background-color: var(--color-accent); -} -#tsd-search .results a { - display: flex; - align-items: center; - padding: 0.25rem; - box-sizing: border-box; -} -#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, -#tsd-search.has-focus #tsd-toolbar-links a { - 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-toolbar-links { - position: absolute; - top: 0; - right: 2rem; - height: 100%; - display: flex; - align-items: center; - justify-content: flex-end; -} -#tsd-toolbar-links a { - margin-left: 1.5rem; -} -#tsd-toolbar-links a:hover { - text-decoration: underline; -} - -.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-keyword { - color: var(--color-ts-keyword); - font-weight: normal; -} - -.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: sticky; - 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; - margin: 0 auto; -} -.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 .tsd-toolbar-icon { - box-sizing: border-box; - line-height: 0; - padding: 12px 0; -} - -.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; -} -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 !important; -} - -.warning { - padding: 1rem; - color: var(--color-warning-text); - background: var(--color-background-warning); -} - -.tsd-kind-project { - color: var(--color-ts-project); -} -.tsd-kind-module { - color: var(--color-ts-module); -} -.tsd-kind-namespace { - color: var(--color-ts-namespace); -} -.tsd-kind-enum { - color: var(--color-ts-enum); -} -.tsd-kind-enum-member { - color: var(--color-ts-enum-member); -} -.tsd-kind-variable { - color: var(--color-ts-variable); -} -.tsd-kind-function { - color: var(--color-ts-function); -} -.tsd-kind-class { - color: var(--color-ts-class); -} -.tsd-kind-interface { - color: var(--color-ts-interface); -} -.tsd-kind-constructor { - color: var(--color-ts-constructor); -} -.tsd-kind-property { - color: var(--color-ts-property); -} -.tsd-kind-method { - color: var(--color-ts-method); -} -.tsd-kind-call-signature { - color: var(--color-ts-call-signature); -} -.tsd-kind-index-signature { - color: var(--color-ts-index-signature); -} -.tsd-kind-constructor-signature { - color: var(--color-ts-constructor-signature); -} -.tsd-kind-parameter { - color: var(--color-ts-parameter); -} -.tsd-kind-type-literal { - color: var(--color-ts-type-literal); -} -.tsd-kind-type-parameter { - color: var(--color-ts-type-parameter); -} -.tsd-kind-accessor { - color: var(--color-ts-accessor); -} -.tsd-kind-get-signature { - color: var(--color-ts-get-signature); -} -.tsd-kind-set-signature { - color: var(--color-ts-set-signature); -} -.tsd-kind-type-alias { - color: var(--color-ts-type-alias); -} - -/* if we have a kind icon, don't color the text by kind */ -.tsd-kind-icon ~ span { - color: var(--color-text); -} - -* { - 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); -} - -/* mobile */ -@media (max-width: 769px) { - .tsd-widget.options, - .tsd-widget.menu { - display: inline-block; - } - - .container-main { - display: flex; - } - html .col-content { - float: none; - max-width: 100%; - width: 100%; - } - html .col-sidebar { - 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; - width: 75vw; - visibility: hidden; - background-color: var(--color-background); - transform: translate(100%, 0); - } - html .col-sidebar > *: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 .col-sidebar { - animation: pop-in-from-right 0.4s; - } - - .from-has-menu .overlay { - animation: fade-out 0.4s; - } - - .from-has-menu .col-sidebar { - animation: pop-out-to-right 0.4s; - } - - .has-menu body { - overflow: hidden; - } - .has-menu .overlay { - visibility: visible; - } - .has-menu .col-sidebar { - visibility: visible; - transform: translate(0, 0); - display: flex; - flex-direction: column; - gap: 1.5rem; - max-height: 100vh; - padding: 1rem 2rem; - } - .has-menu .tsd-navigation { - max-height: 100%; - } -} - -/* one sidebar */ -@media (min-width: 770px) { - .container-main { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); - grid-template-areas: "sidebar content"; - margin: 2rem auto; - } - - .col-sidebar { - grid-area: sidebar; - } - .col-content { - grid-area: content; - padding: 0 1rem; - } -} -@media (min-width: 770px) and (max-width: 1399px) { - .col-sidebar { - max-height: calc(100vh - 2rem - 42px); - overflow: auto; - position: sticky; - top: 42px; - padding-top: 1rem; - } - .site-menu { - margin-top: 1rem; - } -} - -/* two sidebars */ -@media (min-width: 1200px) { - .container-main { - grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax(0, 20rem); - grid-template-areas: "sidebar content toc"; - } - - .col-sidebar { - display: contents; - } - - .page-menu { - grid-area: toc; - padding-left: 1rem; - } - .site-menu { - grid-area: sidebar; - } - - .site-menu { - margin-top: 1rem 0; - } - - .page-menu, - .site-menu { - max-height: calc(100vh - 2rem - 42px); - overflow: auto; - position: sticky; - top: 42px; - } -} diff --git a/docs/classes/BatchCluster.html b/docs/classes/BatchCluster.html deleted file mode 100644 index 12ca6cf..0000000 --- a/docs/classes/BatchCluster.html +++ /dev/null @@ -1,63 +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.

-

Constructors

Properties

emitter: BatchClusterEmitter = ...
off: (<E>(eventName, listener) => this) = ...

Type declaration

    • <E>(eventName, listener): this
    • Type Parameters

      Parameters

      • eventName: E
      • listener: ((...args) => void)

      Returns this

See

BatchClusterEvents

-

Since

v9.0.0

-
on: (<E>(eventName, listener) => this) = ...

Type declaration

    • <E>(eventName, listener): this
    • Type Parameters

      Parameters

      • eventName: E
      • listener: ((...args) => void)

      Returns this

See

BatchClusterEvents

-
options: AllOpts

Accessors

  • get busyProcCount(): number
  • Returns number

    the current number of child processes currently servicing tasks

    -
  • 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 internalErrorCount(): number
  • For integration tests:

    -

    Returns number

  • get isIdle(): boolean
  • Returns boolean

    true if all previously-enqueued tasks have settled

    -
  • get meanTasksPerProc(): number
  • Returns number

    the mean number of tasks completed by child processes

    -
  • get pendingTaskCount(): number
  • Returns number

    the number of pending tasks

    -
  • get procCount(): number
  • Returns number

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

    -
  • get spawnedProcCount(): number
  • Returns number

    the total number of child processes created by this instance

    -

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

    -

    Type Parameters

    • T

    Parameters

    Returns Promise<T>

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

    -
  • Verify that each BatchProcess PID is actually alive.

    -

    Returns number[]

    the spawned PIDs that are still in the process table.

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

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

Class BatchClusterOptions

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

-

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

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

-

Type declaration

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.

-

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

-

Defaults to 5 minutes. Set to 0 to disable.

-
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.

-

Defaults to 10. Set to 0 to disable.

-
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's #onIdle -method, 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.

-

Defaults to 15 seconds. Set to 0 to disable.

-
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.

-

Defaults to 10 seconds. Set to 0 to disable.

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

Class BatchProcess

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

-

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 boolean

    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 () for an authoritative -(but expensive!) answer.

    -
  • get ending(): boolean
  • Returns boolean

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

    -
  • get exited(): boolean
  • Returns boolean

    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 () for an authoritative (but -expensive!) answer.

    -
  • get healthy(): boolean
  • Returns boolean

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

    -
  • get idle(): boolean
  • Returns boolean

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

    -
  • get ready(): boolean
  • Returns boolean

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

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

    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 BatchProcess.whyNotReady if you need to -know if a process can handle a new task.

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

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

    -

Methods

  • End this child process.

    -

    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>

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

    -
  • Returns boolean

    true if the child process is in the process table

    -
\ No newline at end of file diff --git a/docs/classes/Deferred.html b/docs/classes/Deferred.html deleted file mode 100644 index b6db11d..0000000 --- a/docs/classes/Deferred.html +++ /dev/null @@ -1,21 +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

Implements

  • PromiseLike<T>

Constructors

Properties

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

Accessors

  • get fulfilled(): boolean
  • Returns boolean

    true iff resolve has been invoked

    -
  • get pending(): boolean
  • Returns boolean

    true iff neither resolve nor rejected have been invoked

    -
  • get rejected(): boolean
  • Returns boolean

    true iff rejected has been invoked

    -
  • get settled(): boolean
  • Returns boolean

    true iff resolve or rejected have been invoked

    -

Methods

  • Parameters

    • Optional reason: string | Error

    Returns boolean

  • Parameters

    • value: T

    Returns boolean

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

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 Rate#msPerEvent if it's been less -than warmupMs since construction or Rate#clear.

      -

    Returns Rate

Properties

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 Rate#msPerEvent if it's been less -than warmupMs since construction or Rate#clear.

-

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

\ No newline at end of file diff --git a/docs/classes/Task.html b/docs/classes/Task.html deleted file mode 100644 index b8df79d..0000000 --- a/docs/classes/Task.html +++ /dev/null @@ -1,27 +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

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

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.

-
taskId: number = ...

Accessors

  • get pending(): boolean
  • Returns boolean

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

    the resolution or rejection of this task.

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

  • Parameters

    • error: Error

    Returns boolean

    true if the wrapped promise was rejected

    -
  • Returns string

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

Function SimpleParser

  • Invoked once per task.

    -

    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>

    Throws

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

    -

    See

    BatchProcessOptions

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

Function kill

  • Send a signal to the given process id.

    -

    Parameters

    • pid: undefined | number

      the process id. Required.

      -
    • force: boolean = false

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

      -

    Returns boolean

    Export

\ 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 d352b6c..0000000 --- a/docs/functions/logger-1.html +++ /dev/null @@ -1 +0,0 @@ -Codestin Search App

Function logger

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

Function pidExists

  • Parameters

    • pid: undefined | number

      process id. Required.

      -

    Returns boolean

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

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

Function pids

  • Only used by tests

    -

    Returns Promise<number[]>

    all the Process IDs in the process table.

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

Function setLogger

\ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 37aae5f..0000000 --- a/docs/index.html +++ /dev/null @@ -1,69 +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 -CodeQL -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.

-
\ No newline at end of file diff --git a/docs/interfaces/BatchClusterEvents.html b/docs/interfaces/BatchClusterEvents.html deleted file mode 100644 index 5f923aa..0000000 --- a/docs/interfaces/BatchClusterEvents.html +++ /dev/null @@ -1,37 +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.

-
interface BatchClusterEvents {
ย ย ย ย beforeEnd: (() => void);
ย ย ย ย childEnd: ((childProcess, reason) => void);
ย ย ย ย childStart: ((childProcess) => void);
ย ย ย ย end: (() => void);
ย ย ย ย endError: ((error, proc?) => void);
ย ย ย ย fatalError: ((error) => void);
ย ย ย ย healthCheckError: ((error, proc) => void);
ย ย ย ย internalError: ((error) => void);
ย ย ย ย noTaskData: ((stdoutData, stderrData, proc) => void);
ย ย ย ย startError: ((error, childProcess?) => void);
ย ย ย ย taskData: ((data, task, proc) => void);
ย ย ย ย taskError: ((error, task, proc) => void);
ย ย ย ย taskResolved: ((task, proc) => void);
ย ย ย ย taskTimeout: ((timeoutMs, task, proc) => void);
}

Properties

beforeEnd: (() => void)

Emitted when this instance is in the process of ending.

-

Type declaration

    • (): void
    • Returns void

childEnd: ((childProcess, reason) => void)

Emitted when a child process has ended

-

Type declaration

childStart: ((childProcess) => void)

Emitted when a child process has started

-

Type declaration

    • (childProcess): void
    • Parameters

      Returns void

end: (() => void)

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

-

Type declaration

    • (): void
    • Returns void

endError: ((error, proc?) => void)

Emitted when a child process has an error during shutdown

-

Type declaration

    • (error, proc?): void
    • Parameters

      Returns void

fatalError: ((error) => void)

Emitted when .end() is called because the error rate has exceeded -BatchClusterOptions.maxReasonableProcessFailuresPerMinute

-

Type declaration

    • (error): void
    • Parameters

      • error: Error

      Returns void

healthCheckError: ((error, proc) => void)

Emitted when a process fails health checks

-

Type declaration

    • (error, proc): void
    • Parameters

      Returns void

internalError: ((error) => void)

Emitted when an internal consistency check fails

-

Type declaration

    • (error): void
    • Parameters

      • error: Error

      Returns void

noTaskData: ((stdoutData, stderrData, proc) => void)

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

-

Type declaration

    • (stdoutData, stderrData, proc): void
    • Parameters

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

      Returns void

startError: ((error, childProcess?) => void)

Emitted when a child process fails to spin up and run the BatchProcessOptions.versionCommand successfully within BatchClusterOptions.spawnTimeoutMillis.

-

Type declaration

taskData: ((data, task, proc) => void)

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

-

Type declaration

    • (data, task, proc): void
    • Parameters

      Returns void

taskError: ((error, task, proc) => void)

Emitted when a task has an error

-

Type declaration

    • (error, task, proc): void
    • Parameters

      Returns void

taskResolved: ((task, proc) => void)

Emitted when a task has been resolved

-

Type declaration

taskTimeout: ((timeoutMs, task, proc) => void)

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

-

Type declaration

    • (timeoutMs, task, proc): void
    • Parameters

      Returns void

\ No newline at end of file diff --git a/docs/interfaces/BatchProcessOptions.html b/docs/interfaces/BatchProcessOptions.html deleted file mode 100644 index 5477df2..0000000 --- a/docs/interfaces/BatchProcessOptions.html +++ /dev/null @@ -1,24 +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.

-
interface BatchProcessOptions {
ย ย ย ย exitCommand?: string;
ย ย ย ย fail: string | RegExp;
ย ย ย ย healthCheckCommand?: string;
ย ย ย ย pass: string | RegExp;
ย ย ย ย versionCommand: string;
}

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.

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

Interface ChildProcessFactory

These are required parameters for a given BatchCluster.

-
interface ChildProcessFactory {
ย ย ย ย processFactory: (() => ChildProcess | Promise<ChildProcess>);
}

Properties

Properties

processFactory: (() => 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.

-

Type declaration

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

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

Interface Logger

Simple interface for logging.

-
interface Logger {
ย ย ย ย debug: LogFunc;
ย ย ย ย error: LogFunc;
ย ย ย ย info: LogFunc;
ย ย ย ย trace: LogFunc;
ย ย ย ย warn: LogFunc;
}

Properties

Properties

debug: LogFunc
error: LogFunc
info: LogFunc
trace: LogFunc
warn: LogFunc
\ No newline at end of file diff --git a/docs/interfaces/Parser.html b/docs/interfaces/Parser.html deleted file mode 100644 index bbe81fd..0000000 --- a/docs/interfaces/Parser.html +++ /dev/null @@ -1,12 +0,0 @@ -Codestin Search App

Interface Parser<T>

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.

-
interface Parser<T> ((stdout, stderr, passed) => T | Promise<T>)

Type Parameters

  • T
  • Invoked once per task.

    -

    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>

    Throws

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

    -

    See

    BatchProcessOptions

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

Interface TypedEventEmitter<T>

interface TypedEventEmitter<T> {
ย ย ย ย emit<E>(eventName, ...args): boolean;
ย ย ย ย listeners<E>(event): Function[];
ย ย ย ย off<E>(eventName, listener): this;
ย ย ย ย on<E>(eventName, listener): this;
ย ย ย ย once<E>(eventName, listener): this;
ย ย ย ย removeAllListeners(eventName?): this;
}

Type Parameters

  • T

Methods

  • Type Parameters

    • E extends string | number | symbol

    Parameters

    • eventName: E
    • Rest ...args: Args<T[E]>

    Returns boolean

  • Type Parameters

    • E extends string | number | symbol

    Parameters

    • event: E

    Returns Function[]

  • Type Parameters

    • E extends string | number | symbol

    Parameters

    • eventName: E
    • listener: ((...args) => void)
        • (...args): void
        • Parameters

          • Rest ...args: Args<T[E]>

          Returns void

    Returns this

  • Type Parameters

    • E extends string | number | symbol

    Parameters

    • eventName: E
    • listener: ((...args) => void)
        • (...args): void
        • Parameters

          • Rest ...args: Args<T[E]>

          Returns void

    Returns this

  • Type Parameters

    • E extends string | number | symbol

    Parameters

    • eventName: E
    • listener: ((...args) => void)
        • (...args): void
        • Parameters

          • Rest ...args: Args<T[E]>

          Returns void

    Returns this

\ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html deleted file mode 100644 index 2ce90ef..0000000 --- a/docs/modules.html +++ /dev/null @@ -1,27 +0,0 @@ -Codestin Search App
\ 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 2b61157..0000000 --- a/docs/types/BatchClusterEmitter.html +++ /dev/null @@ -1,18 +0,0 @@ -Codestin Search App

Type alias BatchClusterEmitter

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

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

Type alias ChildExitReason

ChildExitReason: WhyNotHealthy | "tooMany"
\ No newline at end of file diff --git a/docs/types/WhyNotHealthy.html b/docs/types/WhyNotHealthy.html deleted file mode 100644 index 2ae8808..0000000 --- a/docs/types/WhyNotHealthy.html +++ /dev/null @@ -1 +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"
\ No newline at end of file diff --git a/docs/types/WhyNotReady.html b/docs/types/WhyNotReady.html deleted file mode 100644 index 1098b47..0000000 --- a/docs/types/WhyNotReady.html +++ /dev/null @@ -1 +0,0 @@ -Codestin Search App

Type alias WhyNotReady

WhyNotReady: WhyNotHealthy | "busy"
\ No newline at end of file diff --git a/docs/variables/ConsoleLogger.html b/docs/variables/ConsoleLogger.html deleted file mode 100644 index 347efea..0000000 --- a/docs/variables/ConsoleLogger.html +++ /dev/null @@ -1,12 +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.

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

Variable LogConst

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

Type declaration

  • filterLevels: ((l, minLogLevel) => any)
      • (l, minLogLevel): any
      • Parameters

        Returns any

  • withLevels: ((delegate) => Logger)
  • withTimestamps: ((delegate) => any)
      • (delegate): any
      • Parameters

        Returns any

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

Variable LogLevelsConst

LogLevels: (keyof Logger)[] = ...
\ No newline at end of file diff --git a/docs/variables/NoLogger.html b/docs/variables/NoLogger.html deleted file mode 100644 index 5c8c0c6..0000000 --- a/docs/variables/NoLogger.html +++ /dev/null @@ -1,2 +0,0 @@ -Codestin Search App

Variable NoLoggerConst

NoLogger: Logger = ...

Logger that disables all logging.

-
\ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 3ed8913..61dc251 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,8 +1,8 @@ // eslint.config.mjs import eslint from "@eslint/js"; +import importPlugin from "eslint-plugin-import"; import globals from "globals"; import tseslint from "typescript-eslint"; -import importPlugin from "eslint-plugin-import"; export default tseslint.config( { @@ -31,13 +31,13 @@ export default tseslint.config( }, rules: { // Project-specific preferences that differ from defaults - "eqeqeq": ["error", "always", { null: "ignore" }], // Allow == null for defensive coding + 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", }, @@ -50,7 +50,7 @@ export default tseslint.config( 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/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-non-null-assertion": "off", @@ -66,9 +66,9 @@ export default tseslint.config( "@typescript-eslint/restrict-plus-operands": "off", "no-console": "off", "@typescript-eslint/no-var-requires": "off", - + // Re-enable one valuable rule that's safe for tests "import/no-cycle": "error", // Circular imports are bad even in tests }, }, -); \ No newline at end of file +); diff --git a/package-lock.json b/package-lock.json index 242cc94..2e41583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "batch-cluster", - "version": "14.0.0", + "version": "15.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "batch-cluster", - "version": "14.0.0", + "version": "15.0.1", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.27.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": "^22.15.21", + "@types/node": "^24.3.0", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -24,21 +24,23 @@ "chai-subset": "^1.6.0", "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", - "eslint-plugin-import": "^2.31.0", - "globals": "^16.2.0", - "mocha": "^11.5.0", - "npm-check-updates": "^18.0.1", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.1.0", + "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.4", + "serve": "^14.2.5", "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", - "typedoc": "^0.28.5", - "typescript": "~5.8.3", - "typescript-eslint": "^8.32.1" + "typedoc": "^0.28.11", + "typescript": "~5.9.2", + "typescript-eslint": "^8.41.0" }, "engines": { "node": ">=20" @@ -100,9 +102,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "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": { @@ -115,9 +117,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "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": { @@ -125,9 +127,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "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": { @@ -175,9 +177,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "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": { @@ -198,13 +200,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "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.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -212,16 +214,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.2.tgz", - "integrity": "sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==", + "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.4.2", - "@shikijs/langs": "^3.4.2", - "@shikijs/themes": "^3.4.2", - "@shikijs/types": "^3.4.2", + "@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" } }, @@ -291,191 +293,822 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "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": "ISC", + "license": "MIT", "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" + "@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": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "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==", + "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": ">=6.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "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" + "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/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "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", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "engines": { + "node": ">=8" } }, - "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==", + "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": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 8" + "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==", + "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": ">=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==", + "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": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "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", - "optional": true, + "dependencies": { + "@inquirer/core": "^10.2.0", + "@inquirer/external-editor": "^1.0.1", + "@inquirer/type": "^3.0.8" + }, "engines": { - "node": ">=14" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "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" + "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/@shikijs/engine-oniguruma": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz", - "integrity": "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==", + "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": { - "@shikijs/types": "3.4.2", - "@shikijs/vscode-textmate": "^10.0.2" + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@shikijs/langs": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.2.tgz", - "integrity": "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==", + "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", - "dependencies": { - "@shikijs/types": "3.4.2" + "engines": { + "node": ">=18" } }, - "node_modules/@shikijs/themes": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.2.tgz", - "integrity": "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==", + "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": { - "@shikijs/types": "3.4.2" + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@shikijs/types": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.2.tgz", - "integrity": "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==", + "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": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@inquirer/core": "^10.2.0", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "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==", + "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" + "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/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "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": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "type-detect": "4.0.8" + "@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/@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==", + "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": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.1" + "@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/@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==", + "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" + "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/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "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" - }, + "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", @@ -528,9 +1161,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "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" }, @@ -566,15 +1199,22 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "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": "~6.21.0" + "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", @@ -590,17 +1230,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "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.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@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", @@ -614,15 +1254,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -630,16 +1270,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "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.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@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": { @@ -651,36 +1291,76 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.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/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "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", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "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/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@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" }, @@ -693,13 +1373,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "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": { @@ -711,14 +1391,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "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/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@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", @@ -734,13 +1416,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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": { @@ -777,16 +1459,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "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.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@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" @@ -797,18 +1479,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "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.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.41.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -825,24 +1507,10 @@ "dev": true, "license": "MIT" }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "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": { @@ -875,6 +1543,16 @@ "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", @@ -947,10 +1625,26 @@ "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.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "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": { @@ -1029,18 +1723,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "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.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "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" @@ -1141,6 +1837,19 @@ "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", @@ -1151,6 +1860,16 @@ "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", @@ -1174,6 +1893,23 @@ "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", @@ -1198,9 +1934,9 @@ } }, "node_modules/boxen/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "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": { @@ -1210,10 +1946,23 @@ "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.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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": { @@ -1248,6 +1997,22 @@ "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", @@ -1258,6 +2023,35 @@ "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", @@ -1377,6 +2171,7 @@ "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": { @@ -1433,6 +2228,13 @@ "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", @@ -1462,6 +2264,32 @@ "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", @@ -1475,6 +2303,45 @@ "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", @@ -1605,24 +2472,34 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "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": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "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", @@ -1640,13 +2517,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1654,6 +2524,23 @@ "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", @@ -1686,6 +2573,16 @@ "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", @@ -1801,6 +2698,36 @@ "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", @@ -1819,6 +2746,19 @@ "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", @@ -1837,6 +2777,35 @@ "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", @@ -1860,6 +2829,19 @@ "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", @@ -1902,10 +2884,20 @@ "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.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "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": { @@ -1936,7 +2928,9 @@ "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", @@ -1951,6 +2945,7 @@ "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", @@ -2065,27 +3060,49 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "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" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "source-map": "~0.6.1" } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "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.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@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.27.0", - "@eslint/plugin-kit": "^0.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", @@ -2096,9 +3113,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "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", @@ -2155,9 +3172,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "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": { @@ -2183,30 +3200,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "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.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "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.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "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.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -2227,9 +3244,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "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": { @@ -2244,9 +3261,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "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": { @@ -2256,16 +3273,29 @@ "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.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "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.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2274,6 +3304,20 @@ "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", @@ -2320,6 +3364,19 @@ "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", @@ -2344,6 +3401,22 @@ "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", @@ -2351,6 +3424,30 @@ "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", @@ -2412,6 +3509,24 @@ "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", @@ -2570,6 +3685,19 @@ "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", @@ -2650,6 +3778,60 @@ "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", @@ -2685,9 +3867,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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": { @@ -2711,9 +3893,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "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": { @@ -2753,6 +3935,13 @@ "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", @@ -2864,6 +4053,41 @@ "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", @@ -2874,6 +4098,19 @@ "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", @@ -2918,6 +4155,33 @@ "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", @@ -2933,6 +4197,16 @@ "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", @@ -2951,6 +4225,13 @@ "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", @@ -3069,16 +4350,16 @@ } }, "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==", + "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": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3152,6 +4433,38 @@ "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", @@ -3165,6 +4478,19 @@ "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", @@ -3263,6 +4589,16 @@ "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", @@ -3399,6 +4735,22 @@ "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", @@ -3413,6 +4765,23 @@ "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", @@ -3429,6 +4798,16 @@ "@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", @@ -3449,6 +4828,13 @@ "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", @@ -3510,6 +4896,22 @@ "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", @@ -3526,6 +4928,41 @@ "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", @@ -3533,6 +4970,13 @@ "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", @@ -3574,6 +5018,19 @@ "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", @@ -3616,6 +5073,15 @@ "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", @@ -3667,190 +5133,456 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "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/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "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": { - "mime-db": "1.52.0" + "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": ">= 0.6" + "node": ">= 4" } }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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": ">= 0.6" + "node": ">=4" } }, - "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==", + "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": ">=6" + "node": ">=4" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "color-name": "1.1.3" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "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", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "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": "ISC", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=0.8.0" } }, - "node_modules/mocha": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.5.0.tgz", - "integrity": "sha512-VKDjhy6LMTKm0WgNEdlY77YVsD49LZnPSXJAaPNL9NRYQADxvORsyG1DIQY6v53BKTnlNbEE2MbVCDbnxr4K3w==", + "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", - "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", - "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=4" } }, - "node_modules/mocha/node_modules/brace-expansion": { + "node_modules/npm-run-all/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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", - "dependencies": { - "balanced-match": "^1.0.0" + "engines": { + "node": ">=4" } }, - "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==", + "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", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "bin": { + "semver": "bin/semver" } }, - "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==", + "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": { - "has-flag": "^4.0.0" + "shebang-regex": "^1.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "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" + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "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": ">= 0.6" + "node": ">=4" } }, - "node_modules/npm-check-updates": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz", - "integrity": "sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==", + "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": "Apache-2.0", - "bin": { - "ncu": "build/cli.js", - "npm-check-updates": "build/cli.js" + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" }, - "engines": { - "node": "^18.18.0 || >=20.0.0", - "npm": ">=8.12.1" + "bin": { + "which": "bin/which" } }, "node_modules/npm-run-path": { @@ -3866,6 +5598,26 @@ "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", @@ -3963,10 +5715,17 @@ "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.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "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": { @@ -3974,37 +5733,178 @@ } }, "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "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": { - "mimic-fn": "^2.1.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6" + "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==", + "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": { - "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" + "macos-release": "^3.3.0", + "windows-release": "^6.1.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/own-keys": { @@ -4057,6 +5957,40 @@ "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", @@ -4077,6 +6011,44 @@ "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", @@ -4135,6 +6107,26 @@ "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", @@ -4145,6 +6137,13 @@ "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", @@ -4152,6 +6151,54 @@ "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", @@ -4173,9 +6220,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "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": { @@ -4189,15 +6236,15 @@ } }, "node_modules/prettier-plugin-organize-imports": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz", - "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==", + "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" + "vue-tsc": "^2.1.0 || 3" }, "peerDependenciesMeta": { "vue-tsc": { @@ -4205,6 +6252,50 @@ } } }, + "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", @@ -4292,6 +6383,32 @@ "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", @@ -4374,6 +6491,67 @@ "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", @@ -4425,6 +6603,33 @@ "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", @@ -4452,6 +6657,29 @@ "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", @@ -4476,6 +6704,16 @@ "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", @@ -4552,6 +6790,13 @@ "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", @@ -4580,9 +6825,9 @@ } }, "node_modules/serve": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", - "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "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": { @@ -4593,7 +6838,7 @@ "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", - "compression": "1.7.4", + "compression": "1.8.1", "is-port-reachable": "4.0.0", "serve-handler": "6.1.6", "update-check": "1.5.4" @@ -4753,6 +6998,19 @@ "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", @@ -4823,23 +7081,64 @@ "side-channel-map": "^1.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "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": "ISC", - "engines": { - "node": ">=14" + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">= 14" } }, "node_modules/source-map": { @@ -4863,6 +7162,42 @@ "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", @@ -4873,6 +7208,33 @@ "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", @@ -4937,6 +7299,25 @@ "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", @@ -5095,6 +7476,30 @@ "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", @@ -5195,6 +7600,13 @@ "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", @@ -5219,13 +7631,13 @@ } }, "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==", + "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": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5310,17 +7722,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz", - "integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==", + "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.2.2", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.7.1" + "yaml": "^2.8.0" }, "bin": { "typedoc": "bin/typedoc" @@ -5330,13 +7742,13 @@ "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" + "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.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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": { @@ -5360,9 +7772,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "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": { @@ -5374,15 +7786,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "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.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@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" @@ -5393,7 +7806,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/uc.micro": { @@ -5422,13 +7835,30 @@ "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": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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", @@ -5450,6 +7880,16 @@ "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", @@ -5457,6 +7897,17 @@ "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", @@ -5588,6 +8039,160 @@ "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", @@ -5599,9 +8204,9 @@ } }, "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "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" }, @@ -5700,6 +8305,38 @@ "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", @@ -5711,9 +8348,9 @@ } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "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": { @@ -5848,6 +8485,19 @@ "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 1dc5ed3..85b9dbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "batch-cluster", - "version": "14.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,7 +10,11 @@ "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": ">=20" @@ -18,17 +22,23 @@ "scripts": { "ci": "npm ci", "clean": "rimraf dist", - "fmt": "prettier --write src/*.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": "npm run clean && npm run lint && npm run 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": "serve docs", - "docs": "bash -c 'for i in {1..4} ; do npm run 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": { @@ -38,25 +48,31 @@ }, "hooks": { "before:init": [ - "npm install", - "npm run lint" + "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": { - "@eslint/js": "^9.27.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": "^22.15.21", + "@types/node": "^24.3.0", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -64,20 +80,22 @@ "chai-subset": "^1.6.0", "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", - "eslint-plugin-import": "^2.31.0", - "globals": "^16.2.0", - "mocha": "^11.5.0", - "npm-check-updates": "^18.0.1", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.1.0", + "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.4", + "serve": "^14.2.5", "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", - "typedoc": "^0.28.5", - "typescript": "~5.8.3", - "typescript-eslint": "^8.32.1" + "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 99d1bf0..f012d88 100644 --- a/src/Array.ts +++ b/src/Array.ts @@ -3,27 +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++) { - 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, ): number { - let acc = 0 + let acc = 0; for (let idx = 0; idx < arr.length; idx++) { - 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 44e63b0..367c5d4 100644 --- a/src/Async.ts +++ b/src/Async.ts @@ -1,10 +1,10 @@ -import timers from "node: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(); + }); } /** @@ -16,15 +16,15 @@ export async function until( timeoutMs: number, 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) + count++; + await delay(delayMs); } } - return false + return false; } diff --git a/src/BatchCluster.procps.spec.ts b/src/BatchCluster.procps.spec.ts deleted file mode 100644 index 281b251..0000000 --- a/src/BatchCluster.procps.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from "chai" -import { describe, it } from "mocha" -import { BatchCluster, ProcpsMissingError } from "./BatchCluster" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { processFactory } from "./_chai.spec" - -describe("BatchCluster procps validation", () => { - it("should validate procps availability during construction", () => { - // This test verifies that BatchCluster calls validateProcpsAvailable() - // On systems where procps is available (like our test environment), - // construction should succeed - expect(() => { - new BatchCluster({ ...DefaultTestOptions, processFactory }) - }).to.not.throw() - }) - - it("should export ProcpsMissingError for user handling", () => { - expect(ProcpsMissingError).to.be.a("function") - expect(new ProcpsMissingError().name).to.equal("ProcpsMissingError") - }) -}) diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index 0f09333..24802e9 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -1,36 +1,36 @@ -import FakeTimers from "@sinonjs/fake-timers" -import process from "node:process" +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" -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" +} 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, @@ -41,14 +41,15 @@ function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { 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, @@ -59,50 +60,50 @@ describe("BatchCluster", function () { bc .enqueueTask(new Task("upcase abc " + (i + start), parser)) .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]+/)), - ) + ); results.forEach((result, index) => { if (!result.startsWith(ErrorPrefix)) { - expect(result).to.eql("ABC " + index) - expect(dataResults.toString()).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))) - }) + 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( @@ -110,57 +111,75 @@ describe("BatchCluster", function () { 5000, 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 = 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, - ) + ); const shutdownOrTimeout = await thenOrTimeout( until(checkShutdown, ShutdownTimeoutMs, 1000), ShutdownTimeoutMs, - ) - expect(endOrTimeout).to.eql(true, ".end() failed") - expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed") + ); + + 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({ @@ -168,85 +187,85 @@ describe("BatchCluster", function () { 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 + ); + 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)) + ); + 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) + const runtimeMs = task.runtimeMs; + expect(runtimeMs).to.not.eql(undefined); - events.runtimeMs.push(runtimeMs!) - }) + 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()", () => { - const emitTimes: number[] = [] - const bc = new BatchCluster({ ...DefaultTestOptions, processFactory }) - const listener = () => emitTimes.push(Date.now()) + 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]) { @@ -262,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: @@ -315,121 +334,121 @@ describe("BatchCluster", function () { ), )) { 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), - ) + ); } 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", - ) + ); expect(pids.every((ea) => process.pid !== ea)).to.eql( true, "no child pids, " + pids.join(", ") + ", should match this process pid, " + process.pid, - ) + ); expect( errors.filter((ea) => !String(ea).includes("EUNLUCKY")), - ).to.eql([], "Unexpected errors") - }) + ).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(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) + ); + 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 = 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, - ) + ); results.push( ...(await Promise.all( runTasks(bc, iters, expectedResultCount), )), - ) - console.log(bc.stats()) + ); + 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([]) + ); + 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 - ) + ); // So, at this point, we should have at least _asked_ the // initial child processes to end because they're "worn". @@ -437,70 +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(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.15, // because flaky (macOS on GHA resulted in 0.21) opts.maxTasksPerProcess, - ) - expect(bc.pids().length).to.be.lte(maxProcs) + ); + expect(bc.pids().length).to.be.lte(maxProcs); expect((await currentTestPids()).length).to.be.lte( bc.spawnedProcCount, - ) // because flaky + ); // 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)), - ) + ); const errorResults = await Promise.all( times(maxProcs * 2, () => bc .enqueueTask(new Task("nonsense", parser)) .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 (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) + return JSON.stringify(ea); } catch { - return "[object Object]" + return "[object Object]"; } } if (typeof ea === "number" || typeof ea === "boolean") { - return String(ea) + return String(ea); } - return "[unknown]" + return "[unknown]"; } filterInPlace(errorResults, (ea) => { - const errorStr = convertErrorToString(ea) - return !errorStr.includes("EUNLUCKY") - }) + const errorStr = convertErrorToString(ea); + return !errorStr.includes("EUNLUCKY"); + }); if ( maxProcs === 1 && ignoreExit === false && @@ -508,97 +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)) + ).to.eql(true, JSON.stringify(errorResults)); expect( parserErrors.some((ea) => ea.includes("nonsense")), - ).to.eql(true, JSON.stringify(parserErrors)) + ).to.eql(true, JSON.stringify(parserErrors)); } - parserErrors.length = 0 + parserErrors.length = 0; // BC should recover: assertExpectedResults( 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, - ) + ); await expect( bc.enqueueTask(task), - ).to.eventually.be.rejectedWith(/timeout|EUNLUCKY/) - postAssertions() - }) + ).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(), - ) + ); const cmd = ["upcase " + idx + " hello", ...worlds].join( "
", - ) - return bc.enqueueTask(new Task(cmd, parser)) + ); + return bc.enqueueTask(new Task(cmd, parser)); }), - ) - expect(results).to.eql(expected) + ); + 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() - }) + ); + 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() - }) + ); + postAssertions(); + }); }, - ) + ); } } } @@ -606,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, @@ -634,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 @@ -642,29 +663,29 @@ 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 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 } + pid: number; + } & Record; + const end = Date.now(); + return { i, start, end, ...result }; }), - ) - const pid2count = new Map() + ); + const pid2count = new Map(); tasks.forEach((ea) => { - const pid = ea.pid - const count = 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, @@ -672,24 +693,24 @@ describe("BatchCluster", function () { uniqPids: pid2count.size, pid2count, 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, @@ -697,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) + ); + 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() + ); + 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") as Record + 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, @@ -771,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, @@ -786,10 +807,10 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - t.catch((err: unknown) => (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 @@ -802,11 +823,11 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - let caught: unknown - 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, @@ -819,15 +840,15 @@ describe("BatchCluster", function () { childEndCounts: { ending: 1 }, ending: true, ended: true, - }) + }); - expect(bc.isIdle).to.eql(true) + expect(bc.isIdle).to.eql(true); expect((caught as Error)?.message).to.include( "Process terminated before task completed", - ) - expect(unhandledRejections).to.eql([]) - }) - }) + ); + expect(unhandledRejections).to.eql([]); + }); + }); describe("maxProcAgeMillis (cull old children)", function () { const opts = { @@ -837,9 +858,9 @@ describe("BatchCluster", function () { spawnTimeoutMillis: 2000, // maxProcAge must be >= this maxProcAgeMillis: 3000, minDelayBetweenSpawnMillis: 0, - } + }; - let bc: BatchCluster + let bc: BatchCluster; beforeEach( () => @@ -849,30 +870,30 @@ describe("BatchCluster", function () { processFactory, }), )), - ) + ); - afterEach(() => shutdown(bc)) + afterEach(() => shutdown(bc)); it("culls old child procs", async () => { assertExpectedResults( await Promise.all(runTasks(bc, opts.maxProcs + 100)), - ) + ); // 0 because we might get unlucky. - expect(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(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 = { @@ -880,9 +901,9 @@ describe("BatchCluster", function () { maxProcs: 4, maxIdleMsPerProcess: 1000, maxProcAgeMillis: 30_000, - } + }; - let bc: BatchCluster + let bc: BatchCluster; beforeEach( () => @@ -892,74 +913,76 @@ describe("BatchCluster", function () { 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(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 (bc.pids().length > 0) { - await delay(1000) + await delay(1000); } - expect(bc.pids().length).to.eql(0) - postAssertions() - }) - }) + expect(bc.pids().length).to.eql(0); + postAssertions(); + }); + }); describe("maxProcAgeMillis (recycling procs)", () => { - let bc: BatchCluster - let clock: FakeTimers.InstalledClock + let bc: BatchCluster; + let clock: FakeTimers.InstalledClock; beforeEach(() => { clock = FakeTimers.install({ shouldClearNativeTimers: true, shouldAdvanceTime: true, - }) - }) + }); + }); afterEach(() => { - clock.uninstall() - 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() - setFailratePct(0) + if (isWin && isCI) return this.skip(); + setFailratePct(0); bc = listen( new BatchCluster({ @@ -969,17 +992,17 @@ describe("BatchCluster", function () { spawnTimeoutMillis: Math.max(maxProcAgeMillis, 200), processFactory, }), - ) - 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 - }) + ); + 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 50705fc..3c67016 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -1,49 +1,51 @@ -import events from "node:events" -import process from "node:process" -import timers from "node:timers" -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, TypedEventEmitter, -} from "./BatchClusterEmitter" -import { BatchClusterOptions } from "./BatchClusterOptions" -import type { BatchClusterStats } from "./BatchClusterStats" -import { BatchProcessOptions } from "./BatchProcessOptions" -import type { ChildProcessFactory } from "./ChildProcessFactory" -import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" -import { Deferred } from "./Deferred" -import { Logger } from "./Logger" -import { verifyOptions } from "./OptionsVerifier" -import { Parser } from "./Parser" -import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator" -import { ProcessPoolManager } from "./ProcessPoolManager" -import { validateProcpsAvailable } from "./ProcpsChecker" -import { Task } from "./Task" -import { TaskQueueManager } from "./TaskQueueManager" -import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" - -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 { ProcpsMissingError } from "./ProcpsChecker" -export { Rate } from "./Rate" -export { Task } from "./Task" +} 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, - BatchClusterStats, - BatchProcessOptions, - ChildEndReason as ChildExitReason, - ChildProcessFactory, - Parser, - TypedEventEmitter, - WhyNotHealthy, - WhyNotReady, -} + 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 @@ -56,78 +58,76 @@ export type { * child tasks can be verified and shut down. */ export class BatchCluster { - 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 + 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, ) { - // Validate that required process listing commands are available - validateProcpsAvailable() - - this.options = verifyOptions({ ...opts, observer: this.emitter }) - this.#logger = this.options.logger + 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) - + ); + 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, + 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.#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 = () => { - void this.end(true) - } + void this.end(true); + }; readonly #exitListener = () => { - void this.end(false) - } + void this.end(false); + }; get ended(): boolean { - return this.#endPromise != null + return this.#endPromise != null; } /** @@ -137,23 +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") + 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) + 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; } /** @@ -166,85 +166,85 @@ export class BatchCluster { if (this.ended) { task.reject( new Error("BatchCluster has ended, cannot enqueue " + task.command), - ) + ); } - this.#taskQueue.enqueue(task as 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.#taskQueue.pendingTaskCount + return this.#taskQueue.pendingTaskCount; } /** * @returns {number} the mean number of tasks completed by child processes */ get meanTasksPerProc(): number { - return this.#eventCoordinator.meanTasksPerProc + return this.#eventCoordinator.meanTasksPerProc; } /** * @return the total number of child processes created by this instance */ get spawnedProcCount(): number { - return this.#processPool.spawnedProcCount + return this.#processPool.spawnedProcCount; } /** * @return the current number of spawned child processes. Some (or all) may be idle. */ get procCount(): number { - return this.#processPool.processCount + return this.#processPool.processCount; } /** * @return the current number of child processes currently servicing tasks */ get busyProcCount(): number { - return this.#processPool.busyProcCount + return this.#processPool.busyProcCount; } get startingProcCount(): number { - return this.#processPool.startingProcCount + return this.#processPool.startingProcCount; } /** * @return the current pending Tasks (mostly for testing) */ - get pendingTasks() { - return this.#taskQueue.pendingTasks + get pendingTasks(): readonly Task[] { + return this.#taskQueue.pendingTasks; } /** * @return the current running Tasks (mostly for testing) */ get currentTasks(): Task[] { - return this.#processPool.currentTasks() + return this.#processPool.currentTasks(); } /** * For integration tests: */ get internalErrorCount(): number { - return this.#eventCoordinator.internalErrorCount + return this.#eventCoordinator.internalErrorCount; } /** @@ -253,7 +253,7 @@ export class BatchCluster { * @return the spawned PIDs that are still in the process table. */ pids(): number[] { - return this.#processPool.pids() + return this.#processPool.pids(); } /** @@ -272,18 +272,18 @@ export class BatchCluster { 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.#eventCoordinator.countEndedChildProcs(why) + return this.#eventCoordinator.countEndedChildProcs(why); } get childEndCounts(): Record, number> { - return this.#eventCoordinator.childEndCounts + return this.#eventCoordinator.childEndCounts; } /** @@ -291,7 +291,7 @@ export class BatchCluster { * be started automatically to handle new tasks. */ async closeChildProcesses(gracefully = true): Promise { - return this.#processPool.closeChildProcesses(gracefully) + return this.#processPool.closeChildProcesses(gracefully); } /** @@ -300,26 +300,26 @@ export class BatchCluster { * completed. */ setMaxProcs(maxProcs: number) { - this.#processPool.setMaxProcs(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 - void this.vacuumProcs() + this.#onIdleRequested = false; + void this.vacuumProcs(); while (this.#execNextTask()) { // } - void this.#maybeSpawnProcs() + void this.#maybeSpawnProcs(); } /** @@ -330,7 +330,7 @@ export class BatchCluster { */ // NOT ASYNC: updates internal state. only exported for tests. vacuumProcs() { - return this.#processPool.vacuumProcs() + return this.#processPool.vacuumProcs(); } /** @@ -338,15 +338,15 @@ export class BatchCluster { * @return true iff a task was submitted to a child process */ #execNextTask(retries = 1): boolean { - if (this.ended) return false - const readyProc = this.#processPool.findReadyProcess() - return this.#taskQueue.tryAssignNextTask(readyProc, retries) + if (this.ended) return false; + const readyProc = this.#processPool.findReadyProcess(); + return this.#taskQueue.tryAssignNextTask(readyProc, retries); } async #maybeSpawnProcs() { return this.#processPool.maybeSpawnProcs( this.#taskQueue.pendingTaskCount, this.ended, - ) + ); } } diff --git a/src/BatchClusterEmitter.ts b/src/BatchClusterEmitter.ts index b19668f..d6d07ad 100644 --- a/src/BatchClusterEmitter.ts +++ b/src/BatchClusterEmitter.ts @@ -1,10 +1,9 @@ -import { BatchProcess } from "./BatchProcess" -import { Task } from "./Task" -import { WhyNotHealthy } from "./WhyNotHealthy" +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 @@ -13,21 +12,21 @@ export interface TypedEventEmitter { once( eventName: E, listener: (...args: Args) => void, - ): this + ): this; on( eventName: E, listener: (...args: Args) => void, - ): this + ): this; off( eventName: E, listener: (...args: Args) => void, - ): this - emit(eventName: E, ...args: Args): boolean + ): this; + emit(eventName: E, ...args: Args): boolean; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - listeners(event: E): Function[] + listeners(event: E): Function[]; - removeAllListeners(eventName?: keyof T): this + removeAllListeners(eventName?: keyof T): this; } /** @@ -40,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 @@ -73,12 +72,12 @@ export interface BatchClusterEvents { data: Buffer | string, task: Task | undefined, proc: BatchProcess, - ) => void + ) => 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. @@ -87,12 +86,12 @@ export interface BatchClusterEvents { timeoutMs: number, task: Task, proc: BatchProcess, - ) => void + ) => 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 @@ -102,28 +101,28 @@ export interface BatchClusterEvents { stdoutData: string | Buffer | null, stderrData: string | Buffer | null, proc: BatchProcess, - ) => void + ) => 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; } /** @@ -146,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 index 07bac7d..ce2fece 100644 --- a/src/BatchClusterEventCoordinator.spec.ts +++ b/src/BatchClusterEventCoordinator.spec.ts @@ -1,157 +1,157 @@ -import events from "node:events" -import { expect } from "./_chai.spec" -import { BatchClusterEmitter } from "./BatchClusterEmitter" +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" +} 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 + let eventCoordinator: BatchClusterEventCoordinator; + let emitter: BatchClusterEmitter; + let onIdleCalledCount = 0; + let endClusterCalledCount = 0; const options: EventCoordinatorOptions = { streamFlushMillis: 100, maxReasonableProcessFailuresPerMinute: 5, logger, - } + }; const onIdleLater = () => { - onIdleCalledCount++ - } + onIdleCalledCount++; + }; const endCluster = () => { - endClusterCalledCount++ - } + endClusterCalledCount++; + }; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter + emitter = new events.EventEmitter() as BatchClusterEmitter; eventCoordinator = new BatchClusterEventCoordinator( emitter, options, onIdleLater, endCluster, - ) - onIdleCalledCount = 0 - endClusterCalledCount = 0 - }) + ); + 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({}) - }) + 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([]) - }) - }) + 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 + } as BatchProcess; // Emit childEnd event - emitter.emit("childEnd", mockProcess, "worn") + 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) - }) + 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) - }) - }) + 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") + const error = new Error("Internal error occurred"); - emitter.emit("internalError", error) + emitter.emit("internalError", error); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) + 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")) + 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) - }) - }) + 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 + const mockProcess = { pid: 12345 } as BatchProcess; - emitter.emit("noTaskData", "some stdout", "some stderr", mockProcess) + emitter.emit("noTaskData", "some stdout", "some stderr", mockProcess); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) + expect(eventCoordinator.internalErrorCount).to.eql(1); + }); it("should handle noTaskData with null data", function () { - const mockProcess = { pid: 12345 } as BatchProcess + const mockProcess = { pid: 12345 } as BatchProcess; - emitter.emit("noTaskData", null, null, mockProcess) + emitter.emit("noTaskData", null, null, mockProcess); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) + 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") + const mockProcess = { pid: 12345 } as BatchProcess; + const bufferData = Buffer.from("test data"); - emitter.emit("noTaskData", bufferData, null, mockProcess) + emitter.emit("noTaskData", bufferData, null, mockProcess); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) - }) + 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") + const error = new Error("Start error"); - emitter.emit("startError", 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) - }) + ); + 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 @@ -160,174 +160,174 @@ describe("BatchClusterEventCoordinator", function () { 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")) + 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 + let fatalErrorEmitted = false; emitter.on("fatalError", () => { - fatalErrorEmitted = true - }) + fatalErrorEmitted = true; + }); // Emit many start errors for (let i = 0; i < 20; i++) { - emitter.emit("startError", new Error(`Start error ${i}`)) + emitter.emit("startError", new Error(`Start error ${i}`)); } - expect(fatalErrorEmitted).to.be.false - expect(endClusterCalledCount).to.eql(0) - }) - }) + 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) - }) + expect(eventCoordinator.events).to.equal(emitter); + }); it("should allow direct event emission through events property", function () { - let eventReceived = false - let receivedData: any + let eventReceived = false; + let receivedData: any; emitter.on("taskData", (data, task, proc) => { - eventReceived = true - receivedData = { data, task, proc } - }) + eventReceived = true; + receivedData = { data, task, proc }; + }); - const mockTask = {} as Task - const mockProcess = {} as BatchProcess - const testData = "test data" + 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) - }) + 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 + let eventReceived = false; const listener = () => { - eventReceived = true - } + eventReceived = true; + }; - eventCoordinator.events.on("beforeEnd", listener) - emitter.emit("beforeEnd") - expect(eventReceived).to.be.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 - }) - }) + 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 + 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")) - }) + 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() + 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") - }) + 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) + expect(eventCoordinator.meanTasksPerProc).to.eql(15); + expect(eventCoordinator.internalErrorCount).to.eql(1); - eventCoordinator.resetStats() + 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({}) + 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([]) - }) + 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 + 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 + 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) + 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) - }) - }) + 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 + const initialCount = onIdleCalledCount; // Events that should trigger onIdleLater - emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn") - emitter.emit("startError", new Error("Start error")) + emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn"); + emitter.emit("startError", new Error("Start error")); - expect(onIdleCalledCount).to.eql(initialCount + 2) - }) + expect(onIdleCalledCount).to.eql(initialCount + 2); + }); it("should have callback integration for endCluster", function () { // This test verifies that the endCluster callback is properly integrated @@ -338,24 +338,24 @@ describe("BatchClusterEventCoordinator", function () { options, onIdleLater, endCluster, - ) + ); // Verify the coordinator is set up and callbacks are connected - expect(testCoordinator.events).to.equal(emitter) + 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 + 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) - }) - }) -}) + 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 index 0e09989..737e531 100644 --- a/src/BatchClusterEventCoordinator.ts +++ b/src/BatchClusterEventCoordinator.ts @@ -1,17 +1,17 @@ -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" +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 + readonly streamFlushMillis: number; + readonly maxReasonableProcessFailuresPerMinute: number; + readonly logger: () => Logger; } /** @@ -19,11 +19,11 @@ export interface EventCoordinatorOptions { * 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 + readonly #logger: () => Logger; + #tasksPerProc = new Mean(); + #startErrorRate = new Rate(); + readonly #childEndCounts = new Map(); + #internalErrorCount = 0; constructor( private readonly emitter: BatchClusterEmitter, @@ -31,42 +31,42 @@ export class BatchClusterEventCoordinator { private readonly onIdleLater: () => void, private readonly endCluster: () => void, ) { - this.#logger = options.logger - this.#setupEventHandlers() + 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("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)) + ); + 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.#tasksPerProc.push(process.taskCount); this.#childEndCounts.set( reason, (this.#childEndCounts.get(reason) ?? 0) + 1, - ) - this.onIdleLater() + ); + this.onIdleLater(); } /** * Handle internal error events */ #handleInternalError(error: Error): void { - this.#logger().error("BatchCluster: INTERNAL ERROR: " + String(error)) - this.#internalErrorCount++ + this.#logger().error("BatchCluster: INTERNAL ERROR: " + String(error)); + this.#internalErrorCount++; } /** @@ -85,16 +85,16 @@ export class BatchClusterEventCoordinator { stderr: toS(stderr), proc_pid: proc?.pid, }, - ) - this.#internalErrorCount++ + ); + this.#internalErrorCount++; } /** * Handle start error events */ #handleStartError(error: Error): void { - this.#logger().warn("BatchCluster.onStartError(): " + String(error)) - this.#startErrorRate.onEvent() + this.#logger().warn("BatchCluster.onStartError(): " + String(error)); + this.#startErrorRate.onEvent(); if ( this.options.maxReasonableProcessFailuresPerMinute > 0 && @@ -109,10 +109,10 @@ export class BatchClusterEventCoordinator { this.#startErrorRate.eventsPerMinute.toFixed(2) + ")", ), - ) - this.endCluster() + ); + this.endCluster(); } else { - this.onIdleLater() + this.onIdleLater(); } } @@ -120,29 +120,29 @@ export class BatchClusterEventCoordinator { * Get the mean number of tasks completed by child processes */ get meanTasksPerProc(): number { - const mean = this.#tasksPerProc.mean - return isNaN(mean) ? 0 : mean + const mean = this.#tasksPerProc.mean; + return isNaN(mean) ? 0 : mean; } /** * Get internal error count */ get internalErrorCount(): number { - return this.#internalErrorCount + return this.#internalErrorCount; } /** * Get start error rate per minute */ get startErrorRatePerMinute(): number { - return this.#startErrorRate.eventsPerMinute + return this.#startErrorRate.eventsPerMinute; } /** * Get count of ended child processes by reason */ countEndedChildProcs(reason: ChildEndReason): number { - return this.#childEndCounts.get(reason) ?? 0 + return this.#childEndCounts.get(reason) ?? 0; } /** @@ -152,7 +152,7 @@ export class BatchClusterEventCoordinator { return Object.fromEntries([...this.#childEndCounts.entries()]) as Record< NonNullable, number - > + >; } /** @@ -168,23 +168,23 @@ export class BatchClusterEventCoordinator { 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 + 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 + return this.emitter; } } diff --git a/src/BatchClusterOptions.spec.ts b/src/BatchClusterOptions.spec.ts index 31f1c8f..0282860 100644 --- a/src/BatchClusterOptions.spec.ts +++ b/src/BatchClusterOptions.spec.ts @@ -1,34 +1,34 @@ -import { BatchCluster } from "./BatchCluster" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { verifyOptions } from "./OptionsVerifier" -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: unknown): string[] { - return String(err).split(/\s*[:;]\s*/) + 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", @@ -36,20 +36,20 @@ describe("BatchClusterOptions", () => { "maxProcAgeMillis must be greater than or equal to " + spawnTimeoutMillis, `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", @@ -57,21 +57,21 @@ describe("BatchClusterOptions", () => { "maxProcAgeMillis must be greater than or equal to " + taskTimeoutMillis, `the max value of spawnTimeoutMillis (${DefaultTestOptions.spawnTimeoutMillis}) and taskTimeoutMillis (${taskTimeoutMillis})`, - ]) + ]); } - }) + }); it("allows maxProcAgeMillis to be 0", () => { - const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1 + const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1; bc = new BatchCluster({ processFactory, ...DefaultTestOptions, taskTimeoutMillis, maxProcAgeMillis: 0, - }) + }); - expect(bc.options.maxProcAgeMillis).to.equal(0) - }) + expect(bc.options.maxProcAgeMillis).to.equal(0); + }); it("reports on invalid opts", () => { try { @@ -90,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", @@ -109,8 +109,8 @@ describe("BatchClusterOptions", () => { "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 64f9710..f81ffc8 100644 --- a/src/BatchClusterOptions.ts +++ b/src/BatchClusterOptions.ts @@ -1,9 +1,9 @@ -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { logger, Logger } from "./Logger" -import { isMac, isWin } from "./Platform" +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 @@ -16,7 +16,7 @@ export class BatchClusterOptions { * * Defaults to 1. */ - maxProcs = 1 + maxProcs = 1; /** * Child processes will be recycled when they reach this age. @@ -26,7 +26,7 @@ export class BatchClusterOptions { * * Defaults to 5 minutes. Set to 0 to disable. */ - maxProcAgeMillis = 5 * minuteMs + maxProcAgeMillis = 5 * minuteMs; /** * This is the minimum interval between calls to BatchCluster's #onIdle @@ -35,7 +35,7 @@ export class BatchClusterOptions { * * Must be > 0. Defaults to 10 seconds. */ - onIdleIntervalMillis = 10 * secondMs + onIdleIntervalMillis = 10 * secondMs; /** * If the initial `versionCommand` fails for new spawned processes more @@ -47,7 +47,7 @@ export class BatchClusterOptions { * * Defaults to 10. Set to 0 to disable. */ - maxReasonableProcessFailuresPerMinute = 10 + maxReasonableProcessFailuresPerMinute = 10; /** * Spawning new child processes and servicing a "version" task must not take @@ -57,7 +57,7 @@ export class BatchClusterOptions { * * 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 @@ -65,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 @@ -75,7 +75,7 @@ export class BatchClusterOptions { * * Defaults to 10 seconds. Set to 0 to disable. */ - taskTimeoutMillis = 10 * secondMs + taskTimeoutMillis = 10 * secondMs; /** * Processes will be recycled after processing `maxTasksPerProcess` tasks. @@ -88,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, @@ -99,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 @@ -123,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 @@ -133,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 @@ -142,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 @@ -150,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 @@ -158,22 +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 + observer: BatchClusterEmitter; } diff --git a/src/BatchClusterStats.ts b/src/BatchClusterStats.ts index ea9adf6..7257874 100644 --- a/src/BatchClusterStats.ts +++ b/src/BatchClusterStats.ts @@ -1,16 +1,16 @@ -import { ChildEndReason } from "./BatchClusterEmitter" +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 + 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 390761b..0c35c30 100644 --- a/src/BatchProcess.ts +++ b/src/BatchProcess.ts @@ -1,56 +1,57 @@ -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" +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() + readonly name: string; + readonly pid: number; + readonly start = Date.now(); - readonly startupTaskId: number - readonly #logger: () => Logger - readonly #terminator: ProcessTerminator - readonly #healthMonitor: ProcessHealthMonitor - readonly #streamHandler: StreamHandler - #lastJobFinshedAt = Date.now() + 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; - #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 + #currentTask: Task | undefined; /** * Getter for current task (required by StreamContext interface) */ get currentTask(): Task | undefined { - return this.#currentTask + return this.#currentTask; } /** @@ -65,11 +66,11 @@ export class BatchProcess { this.#onError(reason as WhyNotHealthy, error), end: (gracefully: boolean, reason: string) => void this.end(gracefully, reason as WhyNotHealthy), - } - } - #currentTaskTimeout: NodeJS.Timeout | undefined + }; + }; + #currentTaskTimeout: NodeJS.Timeout | undefined; - #endPromise: undefined | Deferred + #endPromise: undefined | Deferred; /** * @param onIdle to be called when internal state changes (like the current @@ -81,65 +82,68 @@ export class BatchProcess { private readonly onIdle: () => void, healthMonitor?: ProcessHealthMonitor, ) { - this.name = "BatchProcess(" + proc.pid + ")" - this.#logger = opts.logger - this.#terminator = new ProcessTerminator(opts) + this.name = "BatchProcess(" + proc.pid + ")"; + this.#logger = opts.logger; + this.#terminator = new ProcessTerminator(opts); this.#healthMonitor = - healthMonitor ?? new ProcessHealthMonitor(opts, opts.observer) + 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.pid = proc.pid; - this.proc.on("error", (err) => this.#onError("proc.error", err)) + this.proc.on("error", (err) => this.#onError("proc.error", err)); this.proc.on("close", () => { - void this.end(false, "proc.close") - }) + this.#processExitDeferred.resolve(); + void this.end(false, "proc.close"); + }); this.proc.on("exit", () => { - void this.end(false, "proc.exit") - }) + this.#processExitDeferred.resolve(); + void this.end(false, "proc.exit"); + }); this.proc.on("disconnect", () => { - void this.end(false, "proc.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 + 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"), - ) + ); } // Initialize health monitoring for this process - this.#healthMonitor.initializeProcess(this.pid) + 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) + this.opts.observer.emit("childStart", this); } get taskCount(): number { - return this.#taskCount + return this.#taskCount; } get starting(): boolean { - return this.#starting + return this.#starting; } /** @@ -147,7 +151,7 @@ export class BatchProcess { * child process exiting) */ get ending(): boolean { - return this.#endPromise != null + return this.#endPromise != null; } /** @@ -157,17 +161,16 @@ export class BatchProcess { * (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 BatchProcess#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; } /** @@ -177,14 +180,14 @@ export class BatchProcess { * know if a process can handle a new task. */ get whyNotHealthy(): WhyNotHealthy | null { - return this.#healthMonitor.assessHealth(this, this.#whyNotHealthy) + 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; } /** @@ -192,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; } /** @@ -200,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; } /** @@ -208,118 +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. */ running(): boolean { - if (this.#exited) return false + // If we've been notified via OS events that process exited, trust that immediately + if (this.exited) return false; - const alive = 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. - void this.end(false, "proc.exit") + void this.end(false, "proc.exit"); } - return alive + return alive; } notRunning(): boolean { - return !this.running() + return !this.running(); } - maybeRunHealthcheck(): Task | undefined { - return this.#healthMonitor.maybeRunHealthcheck(this) + 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 + return this.ready ? this.#execTask(task) : false; } #execTask(task: Task): boolean { - if (this.ending) return false + if (this.ending) return false; - this.#taskCount++ - this.#currentTask = task as 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 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 as 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 as 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 as Task) + this.#clearCurrentTask(task as Task); // this.#logger().trace("task failed", { task, err: error }) if (isStartupTask) { this.opts.observer.emit( "startError", error instanceof Error ? error : new Error(String(error)), - ) - void this.end(false, "startError") + ); + void this.end(false, "startError"); } else { 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 { // child process went away. We should too. - void this.end(false, "stdin.error") - return false + void this.end(false, "stdin.error"); + return false; } } @@ -337,14 +344,14 @@ export class BatchProcess { end(gracefully = true, reason: WhyNotHealthy): Promise { return (this.#endPromise ??= new Deferred().observe( this.#end(gracefully, (this.#whyNotHealthy ??= reason)), - )).promise + )).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() + const lastTask = this.#currentTask; + this.#clearCurrentTask(); await this.#terminator.terminate( this.proc, @@ -352,81 +359,81 @@ export class BatchProcess { lastTask, this.startupTaskId, gracefully, - this.#exited, + this.exited, () => this.running(), - ) + ); // Clean up health monitoring for this process - this.#healthMonitor.cleanupProcess(this.pid) + this.#healthMonitor.cleanupProcess(this.pid); - this.opts.observer.emit("childEnd", this, reason) + this.opts.observer.emit("childEnd", this, reason); } #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 (task == null) { - task = this.#currentTask + task = this.#currentTask; } - const cleanedError = new Error(reason + ": " + cleanError(error.message)) + 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) + cleanedError.stack = cleanError(error.stack); } this.#logger().warn(this.name + ".onError()", { reason, task: map(task, (t) => t.command), error: cleanedError, - }) + }); 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 + 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: " + String(cleanedError), - ) - this.opts.observer.emit("startError", 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.`, ), - ) + ); } } } #clearCurrentTask(task?: Task) { - const taskFailed = task?.state === "rejected" + const taskFailed = task?.state === "rejected"; if (taskFailed) { - this.#healthMonitor.recordJobFailure(this.pid) + this.#healthMonitor.recordJobFailure(this.pid); } else if (task != null) { - this.#healthMonitor.recordJobSuccess(this.pid) + this.#healthMonitor.recordJobSuccess(this.pid); } - 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 index fa03034..fdb1ada 100644 --- a/src/ChildProcessFactory.ts +++ b/src/ChildProcessFactory.ts @@ -1,4 +1,4 @@ -import child_process from "node:child_process" +import child_process from "node:child_process"; /** * These are required parameters for a given BatchCluster. @@ -16,5 +16,5 @@ export interface ChildProcessFactory { */ readonly processFactory: () => | child_process.ChildProcess - | Promise + | Promise; } diff --git a/src/CombinedBatchProcessOptions.ts b/src/CombinedBatchProcessOptions.ts index 9321e5d..e3cd3e8 100644 --- a/src/CombinedBatchProcessOptions.ts +++ b/src/CombinedBatchProcessOptions.ts @@ -1,8 +1,8 @@ -import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" -import { ChildProcessFactory } from "./ChildProcessFactory" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"; +import { ChildProcessFactory } from "./ChildProcessFactory"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; export type CombinedBatchProcessOptions = BatchClusterOptions & InternalBatchProcessOptions & ChildProcessFactory & - WithObserver + 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 796959f..f62e9f6 100644 --- a/src/Deferred.ts +++ b/src/Deferred.ts @@ -13,107 +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?: unknown) => 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) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ): Promise { - return this.promise.then(onfulfilled, onrejected) + return this.promise.then(onfulfilled, onrejected); } catch( 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 Deferred + void observeQuietly(this, p); + return this as Deferred; } } async function observe(d: Deferred, p: Promise) { try { - d.resolve(await p) + d.resolve(await p); } catch (err: unknown) { - d.reject(err instanceof Error ? err : new Error(String(err))) + 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 T) + d.resolve(undefined as T); } } diff --git a/src/Error.ts b/src/Error.ts index 853753d..2af1c89 100644 --- a/src/Error.ts +++ b/src/Error.ts @@ -1,4 +1,4 @@ -import { toNotBlank } from "./String" +import { toNotBlank } from "./String"; /** * When we wrap errors, an Error always prefixes the toString() and stack with @@ -7,7 +7,7 @@ import { toNotBlank } from "./String" export function tryEach(arr: (() => void)[]): void { for (const f of arr) { try { - f() + f(); } catch { // } @@ -17,7 +17,7 @@ export function tryEach(arr: (() => void)[]): void { export function cleanError(s: unknown): string { return String(s) .trim() - .replace(/^error: /i, "") + .replace(/^error: /i, ""); } export function asError(err: unknown): Error { @@ -31,5 +31,5 @@ export function asError(err: unknown): Error { ) ?? toNotBlank(err) ?? "(unknown)", - ) + ); } diff --git a/src/HealthCheckStrategy.ts b/src/HealthCheckStrategy.ts index eeb0b28..8360e53 100644 --- a/src/HealthCheckStrategy.ts +++ b/src/HealthCheckStrategy.ts @@ -1,6 +1,6 @@ -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { HealthCheckable } from "./ProcessHealthMonitor" -import { WhyNotHealthy } from "./WhyNotHealthy" +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { HealthCheckable } from "./ProcessHealthMonitor"; +import { WhyNotHealthy } from "./WhyNotHealthy"; /** * Strategy interface for different health check approaches @@ -9,7 +9,7 @@ export interface HealthCheckStrategy { assess( process: HealthCheckable, options: InternalBatchProcessOptions, - ): WhyNotHealthy | null + ): WhyNotHealthy | null; } /** @@ -18,11 +18,11 @@ export interface HealthCheckStrategy { export class LifecycleHealthCheck implements HealthCheckStrategy { assess(process: HealthCheckable): WhyNotHealthy | null { if (process.ended) { - return "ended" + return "ended"; } else if (process.ending) { - return "ending" + return "ending"; } - return null + return null; } } @@ -32,9 +32,9 @@ export class LifecycleHealthCheck implements HealthCheckStrategy { export class StreamHealthCheck implements HealthCheckStrategy { assess(process: HealthCheckable): WhyNotHealthy | null { if (process.proc.stdin == null || process.proc.stdin.destroyed) { - return "closed" + return "closed"; } - return null + return null; } } @@ -50,9 +50,9 @@ export class TaskLimitHealthCheck implements HealthCheckStrategy { options.maxTasksPerProcess > 0 && process.taskCount >= options.maxTasksPerProcess ) { - return "worn" + return "worn"; } - return null + return null; } } @@ -68,9 +68,9 @@ export class IdleTimeHealthCheck implements HealthCheckStrategy { options.maxIdleMsPerProcess > 0 && process.idleMs > options.maxIdleMsPerProcess ) { - return "idle" + return "idle"; } - return null + return null; } } @@ -86,9 +86,9 @@ export class FailureCountHealthCheck implements HealthCheckStrategy { options.maxFailedTasksPerProcess > 0 && process.failedTaskCount >= options.maxFailedTasksPerProcess ) { - return "broken" + return "broken"; } - return null + return null; } } @@ -104,9 +104,9 @@ export class AgeHealthCheck implements HealthCheckStrategy { options.maxProcAgeMillis > 0 && process.start + options.maxProcAgeMillis < Date.now() ) { - return "old" + return "old"; } - return null + return null; } } @@ -122,9 +122,9 @@ export class TaskTimeoutHealthCheck implements HealthCheckStrategy { options.taskTimeoutMillis > 0 && (process.currentTask?.runtimeMs ?? 0) > options.taskTimeoutMillis ) { - return "timeout" + return "timeout"; } - return null + return null; } } @@ -140,18 +140,18 @@ export class CompositeHealthCheckStrategy implements HealthCheckStrategy { 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) + const result = strategy.assess(process, options); if (result != null) { - return result + return result; } } - return null + 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 2c4bd54..ea72f7f 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,18 +1,21 @@ -import util from "node: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: unknown[]) => 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`. * @@ -58,16 +61,16 @@ export const ConsoleLogger: Logger = Object.freeze({ */ warn: (...args: unknown[]) => { // eslint-disable-next-line no-console - console.warn(...args) + console.warn(...args); }, /** * Delegates to `console.error` */ error: (...args: unknown[]) => { // eslint-disable-next-line no-console - console.error(...args) + console.error(...args); }, -}) +}); /** * `Logger` that disables all logging. @@ -78,37 +81,37 @@ export const NoLogger: Logger = Object.freeze({ info: noop, warn: noop, error: noop, -}) +}); -let _logger: Logger = _debuglog.enabled ? ConsoleLogger : 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.join(", ")) + 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: Logger = {} as Logger + const timestamped: Logger = {} as Logger; LogLevels.forEach((ea) => { - const prefix = (ea + " ").substring(0, 5) + " | " + const prefix = (ea + " ").substring(0, 5) + " | "; timestamped[ea] = (message?: unknown, ...optionalParams: unknown[]) => { if (notBlank(String(message))) { - delegate[ea](prefix + String(message), ...optionalParams) + delegate[ea](prefix + String(message), ...optionalParams); } - } - }) - return timestamped + }; + }); + return timestamped; }, withTimestamps: (delegate: Logger) => { - const timestamped: Logger = {} as Logger + const timestamped: Logger = {} as Logger; LogLevels.forEach( (level) => (timestamped[level] = ( @@ -121,17 +124,17 @@ export const Log = { ...optionalParams, ), )), - ) - return timestamped + ); + return timestamped; }, filterLevels: (l: Logger, minLogLevel: keyof Logger) => { - const minLogLevelIndex = LogLevels.indexOf(minLogLevel) - const filtered: Logger = {} as Logger + 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 + ); + return filtered; }, -} +}; diff --git a/src/Mean.ts b/src/Mean.ts index fe1f70b..0ee0204 100644 --- a/src/Mean.ts +++ b/src/Mean.ts @@ -1,39 +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 + 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 accab3a..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 ea4446b..2cc614f 100644 --- a/src/Object.spec.ts +++ b/src/Object.spec.ts @@ -1,24 +1,24 @@ -import { map } from "./Object" -import { expect } from "./_chai.spec" +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") + throw new Error("unexpected"); }), - ).to.eql(undefined) - }) + ).to.eql(undefined); + }); it("skips if target is undefined", () => { expect( map(undefined, () => { - throw new Error("unexpected") + throw new Error("unexpected"); }), - ).to.eql(undefined) - }) + ).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 7bb4839..6067571 100644 --- a/src/Object.ts +++ b/src/Object.ts @@ -6,32 +6,32 @@ export function map( obj: T | undefined | null, f: (t: T) => R, ): R | undefined { - return obj != null ? f(obj) : undefined + return obj != null ? f(obj) : undefined; } export function isFunction(obj: unknown): obj is () => unknown { - return typeof obj === "function" + return typeof obj === "function"; } export function fromEntries( arr: [string | undefined, unknown][], ): Record { - const o: 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>( 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 index bd776fd..946caaf 100644 --- a/src/OptionsVerifier.ts +++ b/src/OptionsVerifier.ts @@ -1,8 +1,8 @@ -import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" -import { BatchProcessOptions } from "./BatchProcessOptions" -import { ChildProcessFactory } from "./ChildProcessFactory" -import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" -import { blank, toS } from "./String" +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. @@ -26,14 +26,14 @@ export function verifyOptions( ...opts, passRE: toRe(opts.pass), failRE: toRe(opts.fail), - } as CombinedBatchProcessOptions + } as CombinedBatchProcessOptions; - const errors: string[] = [] + const errors: string[] = []; function notBlank(fieldName: keyof CombinedBatchProcessOptions) { - const v = toS(result[fieldName]) + const v = toS(result[fieldName]); if (blank(v)) { - errors.push(fieldName + " must not be blank") + errors.push(fieldName + " must not be blank"); } } @@ -42,20 +42,20 @@ export function verifyOptions( value: number, why?: string, ) { - const v = result[fieldName] as number + 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) + const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}`; + errors.push(msg); } } - notBlank("versionCommand") - notBlank("pass") - notBlank("fail") + notBlank("versionCommand"); + notBlank("pass"); + notBlank("fail"); - gte("maxTasksPerProcess", 1) + gte("maxTasksPerProcess", 1); - gte("maxProcs", 1) + gte("maxProcs", 1); if ( opts.maxProcAgeMillis != null && @@ -66,25 +66,25 @@ export function verifyOptions( "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) + 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 + return result; } function escapeRegExp(s: string) { - return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&") + return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&"); } function toRe(s: string | RegExp) { return s instanceof RegExp @@ -98,5 +98,5 @@ function toRe(s: string | RegExp) { .map((ea) => escapeRegExp(ea)) .join(".*"), ) - : new RegExp(escapeRegExp(s)) + : new RegExp(escapeRegExp(s)); } diff --git a/src/Parser.ts b/src/Parser.ts index 8c2b011..b9940ce 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,4 +1,4 @@ -import { notBlank } from "./String" +import { notBlank } from "./String"; /** * Parser implementations convert stdout and stderr from the underlying child @@ -24,14 +24,14 @@ export type Parser = ( stdout: string, stderr: string | undefined, passed: boolean, -) => T | Promise +) => T | Promise; export const SimpleParser: Parser = ( stdout: string, stderr: string | undefined, 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 5cff0ec..d700f3b 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -1,88 +1,57 @@ -import child_process from "node:child_process" -import { existsSync } from "node:fs" -import { readdir } from "node:fs/promises" -import { asError } from "./Error" -import { isWin } from "./Platform" +import { isWin } from "./Platform"; /** * @param {number} pid process id. Required. + * @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 | undefined): boolean { - if (pid == null || !isFinite(pid) || pid <= 0) return false +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 process.kill(pid, 0) + return (killFn ?? process.kill)(pid, 0); } catch (err: unknown) { - // We're expecting err.code to be either "EPERM" (if we don't have - // permission to send `pid` and message), or "ESRCH" if that pid doesn't - // exist. EPERM means it _does_ exist! - if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true + const errorCode = (err as NodeJS.ErrnoException)?.code; - // failed to get priority--assume the pid is gone. - return false + // 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; + } + } + + // For any other error, assume the pid is gone + return false; } } /** * Send a signal to the given process id. * - * @export * @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 | undefined, force = false): boolean { - if (pid == null || !isFinite(pid) || pid <= 0) return false + if (pid == null || !isFinite(pid) || pid <= 0) return false; try { - return process.kill(pid, force ? "SIGKILL" : undefined) + return process.kill(pid, force ? "SIGKILL" : undefined); } catch (err) { - if (!String(err).includes("ESRCH")) throw err - return false + if (!String(err).includes("ESRCH")) throw err; + return false; // failed to get priority--assume the pid is gone. } } - -/** - * Only used by tests - * - * @returns {Promise} all the Process IDs in the process table. - */ -export async function pids(): Promise { - // Linuxโ€style: read /proc - if (!isWin && existsSync("/proc")) { - const names = await readdir("/proc") - return names.filter((d) => /^\d+$/.test(d)).map((d) => parseInt(d, 10)) - } - - // fallback: ps or tasklist - const cmd = isWin ? "tasklist" : "ps" - const args = isWin ? ["/NH", "/FO", "CSV"] : ["-e", "-o", "pid="] - - return new Promise((resolve, reject) => { - child_process.execFile(cmd, args, (err, stdout, stderr) => { - if (err) return reject(asError(err)) - if (stderr.trim()) return reject(new Error(stderr)) - - const pids = stdout - .trim() - .split(/[\r\n]+/) - .map((line) => { - if (isWin) { - // "Image","PID",โ€ฆ - // split on "," and strip outer quotes: - const cols = line.split('","') - const pidStr = cols[1]?.replace(/"/g, "") - return Number(pidStr) - } - // ps -o pid= gives you just the number - return Number(line.trim()) - }) - .filter((n) => Number.isFinite(n) && n > 0) - - resolve(pids) - }) - }) -} diff --git a/src/Platform.ts b/src/Platform.ts index 77e18da..74a6cf2 100644 --- a/src/Platform.ts +++ b/src/Platform.ts @@ -1,7 +1,7 @@ -import os from "node: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 index 74b40b5..20a9bab 100644 --- a/src/ProcessHealthMonitor.spec.ts +++ b/src/ProcessHealthMonitor.spec.ts @@ -1,18 +1,18 @@ -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" +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 + let healthMonitor: ProcessHealthMonitor; + let emitter: BatchClusterEmitter; + let mockProcess: HealthCheckable; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter + emitter = new events.EventEmitter() as BatchClusterEmitter; const options = verifyOptions({ ...DefaultTestOptions, @@ -25,9 +25,9 @@ describe("ProcessHealthMonitor", function () { maxFailedTasksPerProcess: 3, maxProcAgeMillis: 20000, // Must be > spawnTimeoutMillis taskTimeoutMillis: 1000, - }) + }); - healthMonitor = new ProcessHealthMonitor(options, emitter) + healthMonitor = new ProcessHealthMonitor(options, emitter); // Create a healthy mock process mockProcess = { @@ -41,225 +41,228 @@ describe("ProcessHealthMonitor", function () { ended: false, proc: { stdin: { destroyed: false } }, currentTask: null, - } - }) + }; + }); describe("process lifecycle", function () { it("should initialize process health monitoring", function () { - healthMonitor.initializeProcess(mockProcess.pid) + 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 - }) + 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) + healthMonitor.initializeProcess(mockProcess.pid); expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.not.be - .undefined + .undefined; - healthMonitor.cleanupProcess(mockProcess.pid) + healthMonitor.cleanupProcess(mockProcess.pid); expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.be - .undefined - }) - }) + .undefined; + }); + }); describe("health assessment", function () { beforeEach(function () { - healthMonitor.initializeProcess(mockProcess.pid) - }) + 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 - }) + 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 endedProcess = { ...mockProcess, ended: true }; - const healthReason = healthMonitor.assessHealth(endedProcess) - expect(healthReason).to.eql("ended") - expect(healthMonitor.isHealthy(endedProcess)).to.be.false - }) + 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 endingProcess = { ...mockProcess, ending: true }; - const healthReason = healthMonitor.assessHealth(endingProcess) - expect(healthReason).to.eql("ending") - expect(healthMonitor.isHealthy(endingProcess)).to.be.false - }) + 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 - }) + 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 - }) + 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 wornProcess = { ...mockProcess, taskCount: 5 }; - const healthReason = healthMonitor.assessHealth(wornProcess) - expect(healthReason).to.eql("worn") - expect(healthMonitor.isHealthy(wornProcess)).to.be.false - }) + 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 idleProcess = { ...mockProcess, idleMs: 3000 }; - const healthReason = healthMonitor.assessHealth(idleProcess) - expect(healthReason).to.eql("idle") - expect(healthMonitor.isHealthy(idleProcess)).to.be.false - }) + 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 brokenProcess = { ...mockProcess, failedTaskCount: 3 }; - const healthReason = healthMonitor.assessHealth(brokenProcess) - expect(healthReason).to.eql("broken") - expect(healthMonitor.isHealthy(brokenProcess)).to.be.false - }) + 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 - }) + 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 + } as Task; const timedOutProcess = { ...mockProcess, currentTask: mockTask, - } + }; - const healthReason = healthMonitor.assessHealth(timedOutProcess) - expect(healthReason).to.eql("timeout") - expect(healthMonitor.isHealthy(timedOutProcess)).to.be.false - }) + 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 - }) + 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) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.healthCheckFailures = 1 + state.healthCheckFailures = 1; } - const healthReason = healthMonitor.assessHealth(mockProcess) - expect(healthReason).to.eql("unhealthy") - expect(healthMonitor.isHealthy(mockProcess)).to.be.false - }) - }) + 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) - }) + 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 - }) + 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 busyProcess = { ...mockProcess, idle: false }; - const readinessReason = healthMonitor.assessReadiness(busyProcess) - expect(readinessReason).to.eql("busy") - expect(healthMonitor.isReady(busyProcess)).to.be.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 unhealthyProcess = { ...mockProcess, ended: true }; - const readinessReason = healthMonitor.assessReadiness(unhealthyProcess) - expect(readinessReason).to.eql("ended") - expect(healthMonitor.isReady(unhealthyProcess)).to.be.false - }) - }) + 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) - }) + healthMonitor.initializeProcess(mockProcess.pid); + }); it("should record job failures", function () { - healthMonitor.recordJobFailure(mockProcess.pid) + healthMonitor.recordJobFailure(mockProcess.pid); - const state = healthMonitor.getProcessHealthState(mockProcess.pid) - expect(state?.lastJobFailed).to.be.true - }) + 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) + healthMonitor.recordJobFailure(mockProcess.pid); expect( healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed, - ).to.be.true + ).to.be.true; // Then record a success - healthMonitor.recordJobSuccess(mockProcess.pid) + healthMonitor.recordJobSuccess(mockProcess.pid); expect( healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed, - ).to.be.false - }) + ).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() - }) - }) + healthMonitor.recordJobFailure(99999); + healthMonitor.recordJobSuccess(99999); + }).to.not.throw(); + }); + }); describe("health check execution", function () { let mockBatchProcess: HealthCheckable & { - execTask: (task: Task) => boolean - } + execTask: (task: Task) => boolean; + }; beforeEach(function () { - healthMonitor.initializeProcess(mockProcess.pid) + 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 @@ -268,117 +271,117 @@ describe("ProcessHealthMonitor", function () { processFactory, observer: emitter, healthCheckCommand: "", - }) - const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter) + }); + const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter); - const result = noHealthCheckMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.be.undefined - }) + 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 unreadyProcess = { ...mockBatchProcess, idle: false }; - const result = healthMonitor.maybeRunHealthcheck(unreadyProcess) - expect(result).to.be.undefined - }) + const result = healthMonitor.maybeRunHealthCheck(unreadyProcess); + expect(result).to.be.undefined; + }); it("should run health check after job failure", function () { - healthMonitor.recordJobFailure(mockProcess.pid) + healthMonitor.recordJobFailure(mockProcess.pid); - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.not.be.undefined - expect(result?.command).to.eql("healthcheck") - }) + 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) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.lastHealthCheck = Date.now() - 2000 // 2 seconds ago + 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") - }) + 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) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.lastHealthCheck = Date.now() + state.lastHealthCheck = Date.now(); } - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.be.undefined - }) + 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) + healthMonitor.recordJobFailure(mockProcess.pid); - const result = healthMonitor.maybeRunHealthcheck(failingProcess) - expect(result).to.be.undefined - }) - }) + 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) + 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 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) - }) + 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) + healthMonitor.initializeProcess(mockProcess.pid); // Add some failures - const state = healthMonitor.getProcessHealthState(mockProcess.pid) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.healthCheckFailures = 5 + state.healthCheckFailures = 5; } - expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(5) + expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(5); - healthMonitor.resetHealthCheckFailures(mockProcess.pid) + healthMonitor.resetHealthCheckFailures(mockProcess.pid); - expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(0) - expect(healthMonitor.isHealthy(mockProcess)).to.be.true - }) - }) + 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 - }) + 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 - }) - }) -}) + const result = healthMonitor.maybeRunHealthCheck(mockBatchProcess); + expect(result).to.be.undefined; + }); + }); +}); diff --git a/src/ProcessHealthMonitor.ts b/src/ProcessHealthMonitor.ts index 0bc69d4..fa64b99 100644 --- a/src/ProcessHealthMonitor.ts +++ b/src/ProcessHealthMonitor.ts @@ -1,30 +1,30 @@ -import { BatchClusterEmitter } from "./BatchClusterEmitter" +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" +} 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 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 + stdin?: { destroyed?: boolean } | null; + }; + readonly currentTask?: Task | null | undefined; } /** @@ -35,20 +35,20 @@ export class ProcessHealthMonitor { readonly #healthCheckStates = new Map< number, { - lastHealthCheck: number - healthCheckFailures: number - lastJobFailed: boolean + lastHealthCheck: number; + healthCheckFailures: number; + lastJobFailed: boolean; } - >() + >(); - private readonly healthStrategy: HealthCheckStrategy + private readonly healthStrategy: HealthCheckStrategy; constructor( private readonly options: InternalBatchProcessOptions, private readonly emitter: BatchClusterEmitter, healthStrategy?: HealthCheckStrategy, ) { - this.healthStrategy = healthStrategy ?? new CompositeHealthCheckStrategy() + this.healthStrategy = healthStrategy ?? new CompositeHealthCheckStrategy(); } /** @@ -59,23 +59,23 @@ export class ProcessHealthMonitor { lastHealthCheck: Date.now(), healthCheckFailures: 0, lastJobFailed: false, - }) + }); } /** * Clean up health monitoring for a process */ cleanupProcess(pid: number): void { - this.#healthCheckStates.delete(pid) + this.#healthCheckStates.delete(pid); } /** * Record that a job failed for a process */ recordJobFailure(pid: number): void { - const state = this.#healthCheckStates.get(pid) + const state = this.#healthCheckStates.get(pid); if (state != null) { - state.lastJobFailed = true + state.lastJobFailed = true; } } @@ -83,9 +83,9 @@ export class ProcessHealthMonitor { * Record that a job succeeded for a process */ recordJobSuccess(pid: number): void { - const state = this.#healthCheckStates.get(pid) + const state = this.#healthCheckStates.get(pid); if (state != null) { - state.lastJobFailed = false + state.lastJobFailed = false; } } @@ -96,21 +96,21 @@ export class ProcessHealthMonitor { process: HealthCheckable, overrideReason?: WhyNotHealthy, ): WhyNotHealthy | null { - if (overrideReason != null) return overrideReason + if (overrideReason != null) return overrideReason; - const state = this.#healthCheckStates.get(process.pid) + const state = this.#healthCheckStates.get(process.pid); if (state != null && state.healthCheckFailures > 0) { - return "unhealthy" + return "unhealthy"; } - return this.healthStrategy.assess(process, this.options) + 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 + return this.assessHealth(process, overrideReason) == null; } /** @@ -120,31 +120,31 @@ export class ProcessHealthMonitor { process: HealthCheckable, overrideReason?: WhyNotHealthy, ): WhyNotReady | null { - return !process.idle ? "busy" : this.assessHealth(process, overrideReason) + 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 + return this.assessReadiness(process, overrideReason) == null; } /** * Run a health check on a process if needed */ - maybeRunHealthcheck( + maybeRunHealthCheck( process: HealthCheckable & { execTask: (task: Task) => boolean }, ): Task | undefined { - const hcc = this.options.healthCheckCommand + const hcc = this.options.healthCheckCommand; // if there's no health check command, no-op. - if (hcc == null || blank(hcc)) return + if (hcc == null || blank(hcc)) return; // if the prior health check failed, .ready will be false - if (!this.isReady(process)) return + if (!this.isReady(process)) return; - const state = this.#healthCheckStates.get(process.pid) - if (state == null) return + const state = this.#healthCheckStates.get(process.pid); + if (state == null) return; if ( state.lastJobFailed || @@ -152,8 +152,8 @@ export class ProcessHealthMonitor { Date.now() - state.lastHealthCheck > this.options.healthCheckIntervalMillis) ) { - state.lastHealthCheck = Date.now() - const t = new Task(hcc, SimpleParser) + state.lastHealthCheck = Date.now(); + const t = new Task(hcc, SimpleParser); t.promise .catch((err) => { this.emitter.emit( @@ -161,37 +161,37 @@ export class ProcessHealthMonitor { 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++ + ); + state.healthCheckFailures++; // BatchCluster will see we're unhealthy and reap us later }) .finally(() => { - state.lastHealthCheck = Date.now() - }) + state.lastHealthCheck = Date.now(); + }); // Execute the health check task on the process if (process.execTask(t as Task)) { - return t as Task + return t as Task; } } - return + return; } /** * Get health statistics for monitoring */ getHealthStats(): { - monitoredProcesses: number - totalHealthCheckFailures: number - processesWithFailures: number + monitoredProcesses: number; + totalHealthCheckFailures: number; + processesWithFailures: number; } { - let totalFailures = 0 - let processesWithFailures = 0 + let totalFailures = 0; + let processesWithFailures = 0; for (const state of this.#healthCheckStates.values()) { - totalFailures += state.healthCheckFailures + totalFailures += state.healthCheckFailures; if (state.healthCheckFailures > 0) { - processesWithFailures++ + processesWithFailures++; } } @@ -199,16 +199,16 @@ export class ProcessHealthMonitor { 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) + const state = this.#healthCheckStates.get(pid); if (state != null) { - state.healthCheckFailures = 0 + state.healthCheckFailures = 0; } } @@ -216,6 +216,6 @@ export class ProcessHealthMonitor { * Get health check state for a specific process */ getProcessHealthState(pid: number) { - return this.#healthCheckStates.get(pid) + return this.#healthCheckStates.get(pid); } } diff --git a/src/ProcessPoolManager.spec.ts b/src/ProcessPoolManager.spec.ts index a372dc8..111e384 100644 --- a/src/ProcessPoolManager.spec.ts +++ b/src/ProcessPoolManager.spec.ts @@ -1,253 +1,256 @@ -import events from "node:events" +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" +} 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 + 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 + 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) - }) + poolManager = new ProcessPoolManager(options, emitter, onIdle); + }); afterEach(async function () { if (poolManager != null) { - await poolManager.closeChildProcesses(false) + await poolManager.closeChildProcesses(false); // Wait for processes to actually exit - await until(async () => (await currentTestPids()).length === 0, 5000) + 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 - }) + 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([]) - }) - }) + 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) + const pendingTaskCount = 2; + await poolManager.maybeSpawnProcs(pendingTaskCount, false); - expect(poolManager.procCount).to.be.greaterThan(0) - expect(poolManager.spawnedProcCount).to.be.greaterThan(0) + 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 - }) + 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) + const maxProcs = 2; + poolManager.setMaxProcs(maxProcs); // Try to spawn more than maxProcs - await poolManager.maybeSpawnProcs(5, false) + await poolManager.maybeSpawnProcs(5, false); - expect(poolManager.procCount).to.be.at.most(maxProcs) - }) + expect(poolManager.procCount).to.be.at.most(maxProcs); + }); it("should not spawn processes when ended", async function () { - await poolManager.maybeSpawnProcs(2, true) // ended = true + await poolManager.maybeSpawnProcs(2, true); // ended = true - expect(poolManager.procCount).to.eql(0) - expect(poolManager.spawnedProcCount).to.eql(0) - }) + 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) + const pendingTaskCount = 3; + poolManager.setMaxProcs(4); - await poolManager.maybeSpawnProcs(pendingTaskCount, false) + 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)) - }) - }) + 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) - }) + 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 - }) + 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 - }) + 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) + await until(() => poolManager.findReadyProcess() != null, 2000); - const initialCount = poolManager.procCount - expect(initialCount).to.be.greaterThan(0) + 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) - }) + 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) + poolManager.setMaxProcs(3); + await poolManager.maybeSpawnProcs(3, false); + await until(() => poolManager.procCount >= 2, 2000); - const initialCount = poolManager.procCount + const initialCount = poolManager.procCount; // Reduce maxProcs - poolManager.setMaxProcs(1) - await poolManager.vacuumProcs() + 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) - }) - }) + 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) + await poolManager.maybeSpawnProcs(2, false); + await until(() => poolManager.procCount >= 1, 2000); - const initialPids = poolManager.pids() - expect(initialPids.length).to.be.greaterThan(0) + const initialPids = poolManager.pids(); + expect(initialPids.length).to.be.greaterThan(0); - await poolManager.closeChildProcesses(true) + await poolManager.closeChildProcesses(true); - expect(poolManager.procCount).to.eql(0) + expect(poolManager.procCount).to.eql(0); // Wait for processes to actually exit await until(async () => { - const remainingPids = await currentTestPids() + const remainingPids = await currentTestPids(); return ( remainingPids.filter((pid) => initialPids.includes(pid)).length === 0 - ) - }, 5000) - }) + ); + }, 5000); + }); it("should close all processes forcefully", async function () { - await poolManager.maybeSpawnProcs(2, false) - await until(() => poolManager.procCount >= 1, 2000) + await poolManager.maybeSpawnProcs(2, false); + await until(() => poolManager.procCount >= 1, 2000); - const initialPids = poolManager.pids() - expect(initialPids.length).to.be.greaterThan(0) + const initialPids = poolManager.pids(); + expect(initialPids.length).to.be.greaterThan(0); - await poolManager.closeChildProcesses(false) + await poolManager.closeChildProcesses(false); - expect(poolManager.procCount).to.eql(0) + expect(poolManager.procCount).to.eql(0); // Wait for processes to actually exit await until(async () => { - const remainingPids = await currentTestPids() + const remainingPids = await currentTestPids(); return ( remainingPids.filter((pid) => initialPids.includes(pid)).length === 0 - ) - }, 5000) - }) - }) + ); + }, 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) + 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 + 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) + expect(totalProcs).to.be.greaterThan(0); + expect(startingProcs).to.be.greaterThan(0); - await spawnPromise + await spawnPromise; // Wait for processes to be ready - await until(() => poolManager.startingProcCount === 0, 2000) - expect(poolManager.startingProcCount).to.eql(0) - }) + 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) + 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) + expect(poolManager.busyProcCount).to.eql(0); - const readyProcess = poolManager.findReadyProcess() - expect(readyProcess).to.not.be.undefined - expect(readyProcess?.idle).to.be.true - }) - }) + 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[] = [] + const childStartEvents: any[] = []; + const childEndEvents: any[] = []; emitter.on("childStart", (proc) => { - childStartEvents.push(proc) - }) + childStartEvents.push(proc); + }); emitter.on("childEnd", (proc, reason) => { - childEndEvents.push({ proc, reason }) - }) + childEndEvents.push({ proc, reason }); + }); - await poolManager.maybeSpawnProcs(1, false) - await until(() => childStartEvents.length >= 1, 2000) + await poolManager.maybeSpawnProcs(1, false); + await until(() => childStartEvents.length >= 1, 2000); - expect(childStartEvents.length).to.be.greaterThan(0) + expect(childStartEvents.length).to.be.greaterThan(0); - await poolManager.closeChildProcesses(true) - await until(() => childEndEvents.length >= 1, 2000) + await poolManager.closeChildProcesses(true); + await until(() => childEndEvents.length >= 1, 2000); - expect(childEndEvents.length).to.be.greaterThan(0) - expect(childEndEvents[0].reason).to.eql("ending") - }) - }) -}) + expect(childEndEvents.length).to.be.greaterThan(0); + expect(childEndEvents[0].reason).to.eql("ending"); + }); + }); +}); diff --git a/src/ProcessPoolManager.ts b/src/ProcessPoolManager.ts index fd62fed..f8f9149 100644 --- a/src/ProcessPoolManager.ts +++ b/src/ProcessPoolManager.ts @@ -1,54 +1,54 @@ -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" +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 + 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) + this.#logger = options.logger; + this.#healthMonitor = new ProcessHealthMonitor(options, emitter); } /** * Get all current processes */ get processes(): readonly BatchProcess[] { - return this.#procs + return this.#procs; } /** * Get the current number of spawned child processes */ get procCount(): number { - return this.#procs.length + return this.#procs.length; } /** * Alias for procCount to match BatchCluster interface */ get processCount(): number { - return this.procCount + return this.procCount; } /** @@ -59,7 +59,7 @@ export class ProcessPoolManager { this.#procs, // don't count procs that are starting up as "busy": (ea) => !ea.starting && !ea.ending && !ea.idle, - ) + ); } /** @@ -70,48 +70,48 @@ export class ProcessPoolManager { 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) + return count(this.#procs, (ea) => ea.ready); } /** * Get the total number of child processes created by this instance */ get spawnedProcCount(): number { - return this.#spawnedProcs + return this.#spawnedProcs; } /** * Get the milliseconds until the next spawn is allowed */ get msBeforeNextSpawn(): number { - return Math.max(0, this.#nextSpawnTime - Date.now()) + return Math.max(0, this.#nextSpawnTime - Date.now()); } /** * Get all currently running tasks from all processes */ currentTasks(): Task[] { - const tasks: Task[] = [] + const tasks: Task[] = []; for (const proc of this.#procs) { if (proc.currentTask != null) { - tasks.push(proc.currentTask) + tasks.push(proc.currentTask); } } - return tasks + return tasks; } /** * Find the first ready process that can handle a new task */ findReadyProcess(): BatchProcess | undefined { - return this.#procs.find((ea) => ea.ready) + return this.#procs.find((ea) => ea.ready); } /** @@ -119,28 +119,28 @@ export class ProcessPoolManager { * @return the spawned PIDs that are still in the process table. */ pids(): number[] { - const arr: number[] = [] + const arr: number[] = []; for (const proc of [...this.#procs]) { if (proc != null && proc.running()) { - arr.push(proc.pid) + arr.push(proc.pid); } } - return arr + return arr; } /** * Shut down any currently-running child processes. */ async closeChildProcesses(gracefully = true): Promise { - const procs = [...this.#procs] - this.#procs.length = 0 + 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)), ), - ) + ); } /** @@ -148,9 +148,9 @@ export class ProcessPoolManager { * 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) + 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 @@ -161,17 +161,18 @@ export class ProcessPoolManager { // 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) + const why = + proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null); if (why != null) { - endPromises.push(proc.end(true, why)) - return false + endPromises.push(proc.end(true, why)); + return false; } - proc.maybeRunHealthcheck() + proc.maybeRunHealthCheck(); } - return true - }) + return true; + }); - return Promise.all(endPromises) + return Promise.all(endPromises); } /** @@ -181,34 +182,34 @@ export class ProcessPoolManager { pendingTaskCount: number, ended: boolean, ): Promise { - let procsToSpawn = this.#procsToSpawn(pendingTaskCount) + let procsToSpawn = this.#procsToSpawn(pendingTaskCount); if (ended || this.#nextSpawnTime > Date.now() || procsToSpawn === 0) { - return + return; } // prevent concurrent runs: - this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() + this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay(); for (let i = 0; i < procsToSpawn; i++) { if (ended) { - break + break; } // Kick the lock down the road: - this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() - this.#spawnedProcs++ + this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay(); + this.#spawnedProcs++; try { - const proc = this.#spawnNewProc() + const proc = this.#spawnNewProc(); const result = await thenOrTimeout( proc, this.options.spawnTimeoutMillis, - ) + ); if (result === Timeout) { void proc .then((bp) => { - void bp.end(false, "startError") + void bp.end(false, "startError"); this.emitter.emit( "startError", asError( @@ -217,18 +218,18 @@ export class ProcessPoolManager { "ms", ), bp, - ) + ); }) .catch((err) => { // this should only happen if the processFactory throws a // rejection: - this.emitter.emit("startError", asError(err)) - }) + 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 @@ -236,26 +237,26 @@ export class ProcessPoolManager { procsToSpawn = Math.min( this.#procsToSpawn(pendingTaskCount), procsToSpawn, - ) + ); } catch (err) { - this.emitter.emit("startError", asError(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 + 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() + timers.setTimeout(this.onIdle, delay).unref(); } /** * Update the maximum number of processes allowed */ setMaxProcs(maxProcs: number): void { - this.options.maxProcs = maxProcs + this.options.maxProcs = maxProcs; } #maybeCheckPids(): void { @@ -264,46 +265,49 @@ export class ProcessPoolManager { this.options.pidCheckIntervalMillis > 0 && this.#lastPidsCheckTime + this.options.pidCheckIntervalMillis < Date.now() ) { - this.#lastPidsCheckTime = Date.now() - void this.pids() + 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) + return Math.max(10_000, this.options.spawnTimeoutMillis); } #procsToSpawn(pendingTaskCount: number): number { - const remainingCapacity = this.options.maxProcs - this.#procs.length + 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 requestedCapacity = pendingTaskCount - this.startingProcCount; - const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity)) + 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) + 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 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 + ); + this.#procs.push(result); + return result; } } diff --git a/src/ProcessTerminator.spec.ts b/src/ProcessTerminator.spec.ts index 68f29f7..5831ec3 100644 --- a/src/ProcessTerminator.spec.ts +++ b/src/ProcessTerminator.spec.ts @@ -1,37 +1,37 @@ -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" +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 }[] + 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 + pid = 12345; + stdin = new MockWritableStream(); + stdout = new MockReadableStream(); + stderr = new MockReadableStream(); + killed = false; + disconnected = false; kill() { - this.killed = true - return true + this.killed = true; + return true; } disconnect() { - this.disconnected = true + this.disconnected = true; } unref() { @@ -40,53 +40,53 @@ describe("ProcessTerminator", function () { } class MockWritableStream extends stream.Writable { - override destroyed = false - override writable = true - data: string[] = [] + override destroyed = false; + override writable = true; + data: string[] = []; override _write(chunk: any, _encoding: any, callback: any) { - this.data.push(chunk.toString()) - callback() + this.data.push(chunk.toString()); + callback(); } override end(data?: any): this { if (data != null) { - this.data.push(data.toString()) + this.data.push(data.toString()); } - this.writable = false - super.end() - return this + this.writable = false; + super.end(); + return this; } override destroy(): this { - this.destroyed = true - super.destroy() - return this + this.destroyed = true; + super.destroy(); + return this; } } class MockReadableStream extends stream.Readable { - override destroyed = false + override destroyed = false; override _read() { // no-op for tests } override destroy(): this { - this.destroyed = true - super.destroy() - return this + this.destroyed = true; + super.destroy(); + return this; } } beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter - childEndEvents = [] + emitter = new events.EventEmitter() as BatchClusterEmitter; + childEndEvents = []; // Track childEnd events emitter.on("childEnd", (process: any, reason: string) => { - childEndEvents.push({ process, reason }) - }) + childEndEvents.push({ process, reason }); + }); options = { logger, @@ -113,31 +113,31 @@ describe("ProcessTerminator", function () { maxReasonableProcessFailuresPerMinute: 10, minDelayBetweenSpawnMillis: 100, pidCheckIntervalMillis: 150, - } + }; - terminator = new ProcessTerminator(options) - mockProcess = new MockChildProcess() - isRunningResult = true - }) + terminator = new ProcessTerminator(options); + mockProcess = new MockChildProcess(); + isRunningResult = true; + }); function createMockTask( taskId = 1, command = "test", pending = true, ): Task { - const task = new Task(command, SimpleParser) + const task = new Task(command, SimpleParser); if (!pending) { // Simulate task completion by calling onStdout with PASS token - task.onStart(options) - task.onStdout("PASS") + task.onStart(options); + task.onStdout("PASS"); } // Override taskId for testing - Object.defineProperty(task, "taskId", { value: taskId, writable: true }) - return task as Task + Object.defineProperty(task, "taskId", { value: taskId, writable: true }); + return task as Task; } function mockIsRunning(): boolean { - return isRunningResult + return isRunningResult; } describe("basic termination", function () { @@ -150,19 +150,19 @@ describe("ProcessTerminator", function () { true, // graceful false, // not exited mockIsRunning, - ) + ); // Should send exit command - expect(mockProcess.stdin.data).to.include("exit\n") + 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 + 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 - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should terminate process forcefully when not graceful", async function () { await terminator.terminate( @@ -173,11 +173,11 @@ describe("ProcessTerminator", function () { false, // not graceful false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.include("exit\n") - expect(mockProcess.disconnected).to.be.true - }) + 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( @@ -188,24 +188,24 @@ describe("ProcessTerminator", function () { true, true, // already exited mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.include("exit\n") - expect(mockProcess.disconnected).to.be.true - }) - }) + 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 + 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) + taskCompleted = true; + task.onStart(options); + task.onStdout("PASS"); // Complete the task + }, 50); await terminator.terminate( mockProcess as any, @@ -215,21 +215,21 @@ describe("ProcessTerminator", function () { true, // graceful false, mockIsRunning, - ) + ); - expect(taskCompleted).to.be.true - expect(task.state !== "pending").to.be.true - }) + 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 = "" + const task = createMockTask(1, "slow task", true); + let taskRejected = false; + let rejectionReason = ""; task.promise.catch((err) => { - taskRejected = true - rejectionReason = err.message - }) + taskRejected = true; + rejectionReason = err.message; + }); await terminator.terminate( mockProcess as any, @@ -239,16 +239,16 @@ describe("ProcessTerminator", function () { false, // not graceful - shorter timeout false, mockIsRunning, - ) + ); - expect(taskRejected).to.be.true + 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) + const startupTask = createMockTask(999, "version", true); await terminator.terminate( mockProcess as any, @@ -258,11 +258,11 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Should not wait for or reject startup task - expect(startupTask.pending).to.be.true - }) + expect(startupTask.pending).to.be.true; + }); it("should skip task completion wait when no current task", async function () { await terminator.terminate( @@ -273,23 +273,23 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Should complete without errors - expect(mockProcess.disconnected).to.be.true - }) - }) + 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) + }; + 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, @@ -299,17 +299,17 @@ describe("ProcessTerminator", function () { 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) - }) + 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 + mockProcess.stdin.writable = true; await terminator.terminate( mockProcess as any, @@ -319,13 +319,13 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.include("exit\n") - }) + expect(mockProcess.stdin.data).to.include("exit\n"); + }); it("should skip exit command if stdin is not writable", async function () { - mockProcess.stdin.writable = false + mockProcess.stdin.writable = false; await terminator.terminate( mockProcess as any, @@ -335,14 +335,14 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.be.empty - }) + 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) + const optionsNoExit = { ...options, exitCommand: undefined }; + const terminatorNoExit = new ProcessTerminator(optionsNoExit); await terminatorNoExit.terminate( mockProcess as any, @@ -352,12 +352,12 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Should complete without sending exit command - expect(mockProcess.stdin.data).to.be.empty - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.stdin.data).to.be.empty; + expect(mockProcess.disconnected).to.be.true; + }); it("should destroy all streams", async function () { await terminator.terminate( @@ -368,31 +368,31 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.destroyed).to.be.true - expect(mockProcess.stdout.destroyed).to.be.true - expect(mockProcess.stderr.destroyed).to.be.true - }) - }) + 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 + let killCalled = false; mockProcess.kill = () => { - killCalled = true - return true - } + killCalled = true; + return true; + }; // Simulate process still running initially, then stopping - let callCount = 0 + let callCount = 0; const mockIsRunningGraceful = () => { - callCount++ + callCount++; if (callCount <= 2) { - return true // Still running for first few checks + return true; // Still running for first few checks } - return false // Then stops running - } + return false; // Then stops running + }; await terminator.terminate( mockProcess as any, @@ -402,18 +402,18 @@ describe("ProcessTerminator", function () { true, // graceful false, // not already exited mockIsRunningGraceful, - ) + ); - expect(killCalled).to.be.false // Should not need to kill - }) + 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 + let killCalled = false; mockProcess.kill = () => { - killCalled = true - isRunningResult = false // Process stops after kill signal - return true - } + killCalled = true; + isRunningResult = false; // Process stops after kill signal + return true; + }; await terminator.terminate( mockProcess as any, @@ -423,20 +423,20 @@ describe("ProcessTerminator", function () { true, // graceful false, // not already exited mockIsRunning, // Always returns true until killed - ) + ); - expect(killCalled).to.be.true - }) + 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) + const optionsNoCleanup = { ...options, cleanupChildProcs: false }; + const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup); - let killCalled = false + let killCalled = false; mockProcess.kill = () => { - killCalled = true - return true - } + killCalled = true; + return true; + }; await terminatorNoCleanup.terminate( mockProcess as any, @@ -446,20 +446,20 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(killCalled).to.be.false - }) + 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) + const optionsNoWait = { ...options, endGracefulWaitTimeMillis: 0 }; + const terminatorNoWait = new ProcessTerminator(optionsNoWait); - let killCalled = false + let killCalled = false; mockProcess.kill = () => { - killCalled = true - return true - } + killCalled = true; + return true; + }; await terminatorNoWait.terminate( mockProcess as any, @@ -469,16 +469,16 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(killCalled).to.be.false - }) - }) + 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 + const mockIsRunningStubborn = () => true; // Should complete without throwing await terminator.terminate( @@ -489,16 +489,16 @@ describe("ProcessTerminator", function () { true, false, mockIsRunningStubborn, - ) + ); // Should still disconnect and destroy streams - expect(mockProcess.disconnected).to.be.true - expect(mockProcess.stdin.destroyed).to.be.true - }) + 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) + const optionsNoCleanup = { ...options, cleanupChildProcs: false }; + const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup); await terminatorNoCleanup.terminate( mockProcess as any, @@ -508,15 +508,15 @@ describe("ProcessTerminator", function () { true, false, () => true, // Always running - ) + ); // Should still complete basic cleanup - expect(mockProcess.disconnected).to.be.true - expect(mockProcess.stdin.destroyed).to.be.true - }) + 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 + (mockProcess as any).pid = undefined; await terminator.terminate( mockProcess as any, @@ -526,19 +526,19 @@ describe("ProcessTerminator", function () { true, false, () => true, - ) + ); // Should complete without issues - expect(mockProcess.disconnected).to.be.true - expect(mockProcess.stdin.destroyed).to.be.true - }) - }) + 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") - } + throw new Error("EPIPE"); + }; // Should not throw await terminator.terminate( @@ -549,15 +549,15 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should handle stream destruction errors gracefully", async function () { mockProcess.stdout.destroy = () => { - throw new Error("Stream error") - } + throw new Error("Stream error"); + }; // Should not throw await terminator.terminate( @@ -568,15 +568,15 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should handle disconnect errors gracefully", async function () { mockProcess.disconnect = () => { - throw new Error("Disconnect error") - } + throw new Error("Disconnect error"); + }; // Should not throw await terminator.terminate( @@ -587,13 +587,13 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) - }) - }) + ); + }); + }); describe("edge cases", function () { it("should handle null stderr stream", async function () { - ;(mockProcess as any).stderr = null + (mockProcess as any).stderr = null; await terminator.terminate( mockProcess as any, @@ -603,13 +603,13 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should handle already completed task", async function () { - const completedTask = createMockTask(1, "completed", false) + const completedTask = createMockTask(1, "completed", false); await terminator.terminate( mockProcess as any, @@ -619,14 +619,14 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(completedTask.state !== "pending").to.be.true - expect(mockProcess.disconnected).to.be.true - }) + 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 + (mockProcess as any).pid = undefined; await terminator.terminate( mockProcess as any, @@ -636,16 +636,16 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) - }) + 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() + const slowTask = createMockTask(1, "slow", true); + const startTime = Date.now(); await terminator.terminate( mockProcess as any, @@ -655,18 +655,18 @@ describe("ProcessTerminator", function () { true, // graceful - should wait up to 2000ms for task false, mockIsRunning, - ) + ); - const elapsed = Date.now() - startTime + 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 - }) + 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() + const slowTask = createMockTask(1, "slow", true); + const startTime = Date.now(); await terminator.terminate( mockProcess as any, @@ -676,12 +676,12 @@ describe("ProcessTerminator", function () { false, // not graceful - should wait only 250ms for task false, mockIsRunning, - ) + ); - const elapsed = Date.now() - startTime + 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 - }) - }) -}) + 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 index 547bedf..a457b49 100644 --- a/src/ProcessTerminator.ts +++ b/src/ProcessTerminator.ts @@ -1,21 +1,21 @@ -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" +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 + readonly #logger: () => Logger; constructor(private readonly opts: InternalBatchProcessOptions) { - this.#logger = opts.logger + this.#logger = opts.logger; } /** @@ -42,26 +42,26 @@ export class ProcessTerminator { isRunning: () => boolean, ): Promise { // Wait for current task to complete if graceful termination requested - await this.#waitForTaskCompletion(lastTask, startupTaskId, gracefully) + await this.#waitForTaskCompletion(lastTask, startupTaskId, gracefully); // Remove error listeners to prevent EPIPE errors during termination - this.#removeErrorListeners(proc) + this.#removeErrorListeners(proc); // Send exit command to process - this.#sendExitCommand(proc) + this.#sendExitCommand(proc); // Destroy streams - this.#destroyStreams(proc) + this.#destroyStreams(proc); // Handle graceful shutdown with timeouts - await this.#handleGracefulShutdown(proc, gracefully, isExited, isRunning) + await this.#handleGracefulShutdown(proc, gracefully, isExited, isRunning); // Force kill if still running - this.#forceKillIfRunning(proc, processName, isRunning) + this.#forceKillIfRunning(proc, processName, isRunning); // Final cleanup try { - proc.disconnect?.() + proc.disconnect?.(); } catch { // Ignore disconnect errors } @@ -75,12 +75,12 @@ export class ProcessTerminator { ): Promise { // Don't wait for startup tasks or if no task is running if (lastTask == null || lastTask.taskId === startupTaskId) { - return + return; } try { // Wait for the process to complete and streams to flush - await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250) + await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250); } catch { // Ignore errors during task completion wait } @@ -94,7 +94,7 @@ export class ProcessTerminator { lastTask, })})`, ), - ) + ); } } @@ -102,22 +102,22 @@ export class ProcessTerminator { // 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") + stream?.removeAllListeners("error"); } } #sendExitCommand(proc: child_process.ChildProcess): void { if (proc.stdin?.writable !== true) { - return + return; } const exitCmd = this.opts.exitCommand == null ? null - : ensureSuffix(this.opts.exitCommand, "\n") + : ensureSuffix(this.opts.exitCommand, "\n"); try { - proc.stdin.end(exitCmd) + proc.stdin.end(exitCmd); } catch { // Ignore errors when sending exit command } @@ -125,9 +125,9 @@ export class ProcessTerminator { #destroyStreams(proc: child_process.ChildProcess): void { // Destroy all streams to ensure cleanup - destroy(proc.stdin) - destroy(proc.stdout) - destroy(proc.stderr) + destroy(proc.stdin); + destroy(proc.stdout); + destroy(proc.stderr); } async #handleGracefulShutdown( @@ -142,25 +142,25 @@ export class ProcessTerminator { this.opts.endGracefulWaitTimeMillis <= 0 || isExited ) { - return + 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() + proc.kill(); } // Wait for the signal handler to work await this.#awaitNotRunning( this.opts.endGracefulWaitTimeMillis / 2, isRunning, - ) + ); } #forceKillIfRunning( @@ -171,8 +171,8 @@ export class ProcessTerminator { if (this.opts.cleanupChildProcs && proc.pid != null && isRunning()) { this.#logger().warn( `${processName}.terminate(): force-killing still-running child.`, - ) - kill(proc.pid, true) + ); + kill(proc.pid, true); } } @@ -180,6 +180,6 @@ export class ProcessTerminator { timeout: number, isRunning: () => boolean, ): Promise { - await until(() => !isRunning(), timeout) + await until(() => !isRunning(), timeout); } } diff --git a/src/ProcpsChecker.spec.ts b/src/ProcpsChecker.spec.ts deleted file mode 100644 index 5becc76..0000000 --- a/src/ProcpsChecker.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from "chai" -import { describe, it } from "mocha" -import { ProcpsMissingError, validateProcpsAvailable } from "./ProcpsChecker" - -describe("ProcpsChecker", () => { - describe("validateProcpsAvailable()", () => { - it("should not throw on systems with procps installed", () => { - // Since we're running tests, procps should be available - expect(() => validateProcpsAvailable()).to.not.throw() - }) - - it("should create appropriate error message for platform", () => { - const error = new ProcpsMissingError() - expect(error.name).to.equal("ProcpsMissingError") - expect(error.message).to.include("command not available") - - // Message should be specific to platform - if (process.platform === "win32") { - expect(error.message).to.include("tasklist") - } else { - expect(error.message).to.include("ps") - expect(error.message).to.include("procps") - } - }) - - it("should preserve original error", () => { - const originalError = new Error("Command failed") - const procpsError = new ProcpsMissingError(originalError) - - expect(procpsError.originalError).to.equal(originalError) - }) - }) -}) diff --git a/src/ProcpsChecker.ts b/src/ProcpsChecker.ts deleted file mode 100644 index 85a9fa1..0000000 --- a/src/ProcpsChecker.ts +++ /dev/null @@ -1,52 +0,0 @@ -import child_process from "node:child_process" -import { existsSync, readdirSync } from "node:fs" -import { isWin } from "./Platform" - -/** - * Error thrown when procps is missing on non-Windows systems - */ -export class ProcpsMissingError extends Error { - readonly originalError?: Error - - constructor(originalError?: Error) { - const message = isWin - ? "tasklist command not available" - : "ps command not available. Please install procps package (e.g., 'apt-get install procps' on Ubuntu/Debian)" - - super(message) - this.name = "ProcpsMissingError" - - if (originalError != null) { - this.originalError = originalError - } - } -} - -/** - * Check if the required process listing command is available - * @throws {ProcpsMissingError} if the command is not available - */ -export function validateProcpsAvailable(): void { - // on POSIX systems with a working /proc we can skip ps entirely - if (!isWin && existsSync("/proc")) { - const entries = readdirSync("/proc") - // if we see at least one numeric directory, assume /proc is usable - if (entries.some((d) => /^\d+$/.test(d))) { - return - } - // fall through to check `ps` if /proc is empty or unusable - } - - try { - const command = isWin ? "tasklist" : "ps" - const args = isWin ? ["/NH", "/FO", "CSV", "/FI", "PID eq 1"] : ["-p", "1"] - const timeout = isWin ? 15_000 : 5_000 // 15s for Windows, 5s elsewhere - - child_process.execFileSync(command, args, { - stdio: "pipe", - timeout, - }) - } catch (err) { - throw new ProcpsMissingError(err instanceof Error ? err : undefined) - } -} diff --git a/src/Rate.spec.ts b/src/Rate.spec.ts index c2c6c1a..5abb1aa 100644 --- a/src/Rate.spec.ts +++ b/src/Rate.spec.ts @@ -1,79 +1,79 @@ -import FakeTimers from "@sinonjs/fake-timers" -import { minuteMs } from "./BatchClusterOptions" -import { Rate } from "./Rate" -import { expect, times } from "./_chai.spec" +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() - let clock: FakeTimers.InstalledClock + const now = Date.now(); + const r = new Rate(); + let clock: FakeTimers.InstalledClock; beforeEach(() => { - clock = FakeTimers.install({ now: now }) + clock = FakeTimers.install({ now: now }); // clear() must be called _after_ setting up fake timers - r.clear() - }) + r.clear(); + }); afterEach(() => { - clock.uninstall() - }) + 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", () => { - clock.tick(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) - 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) + 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 [4, 32, 256, 1024]) { it( "calculates average rate for " + events + " events, and then decays", () => { - const period = r.periodMs + 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) + 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 3719208..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,10 +12,10 @@ 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 @@ -29,57 +29,57 @@ export class Rate { ) {} 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 + ); + 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 498eafb..352dfb1 100644 --- a/src/Stream.ts +++ b/src/Stream.ts @@ -1,12 +1,12 @@ -import { Readable, Writable } from "node:stream" +import { Readable, Writable } from "node:stream"; 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") + stream?.removeAllListeners("error"); // It's fine to call .destroy() on a stream that's already destroyed. - stream?.destroy?.() + stream?.destroy?.(); } catch { // don't care } diff --git a/src/StreamHandler.spec.ts b/src/StreamHandler.spec.ts index 0d1230a..f8647c2 100644 --- a/src/StreamHandler.spec.ts +++ b/src/StreamHandler.spec.ts @@ -1,32 +1,32 @@ -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 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" +} 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 }[] = [] + 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) + emitter = new events.EventEmitter() as BatchClusterEmitter; + streamHandler = new StreamHandler(options, emitter); - onErrorCalls = [] - endCalls = [] + onErrorCalls = []; + endCalls = []; // Create a mock context that simulates BatchProcess behavior mockContext = { @@ -34,56 +34,56 @@ describe("StreamHandler", function () { isEnding: () => false, getCurrentTask: () => undefined, onError: (reason: string, error: Error) => { - onErrorCalls.push({ reason, error }) + onErrorCalls.push({ reason, error }); }, end: (gracefully: boolean, reason: string) => { - endCalls.push({ gracefully, reason }) + endCalls.push({ gracefully, reason }); }, - } - }) + }; + }); describe("initial state", function () { it("should initialize correctly", function () { - expect(streamHandler).to.not.be.undefined + expect(streamHandler).to.not.be.undefined; - const stats = streamHandler.getStats() - expect(stats.handlerActive).to.be.true - expect(stats.emitterConnected).to.be.true - }) - }) + 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 + let mockProcess: child_process.ChildProcess; beforeEach(async function () { // Create a real process for testing stream setup - mockProcess = await processFactory() - }) + mockProcess = await processFactory(); + }); afterEach(function () { if (mockProcess && !mockProcess.killed) { - mockProcess.kill() + mockProcess.kill(); } - }) + }); it("should set up stream listeners on a child process", function () { expect(() => { - streamHandler.setupStreamListeners(mockProcess, mockContext) - }).to.not.throw() + 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 - }) + 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 + const invalidProcess = { stdin: null } as child_process.ChildProcess; expect(() => { - streamHandler.setupStreamListeners(invalidProcess, mockContext) - }).to.throw("Given proc had no stdin") - }) + streamHandler.setupStreamListeners(invalidProcess, mockContext); + }).to.throw("Given proc had no stdin"); + }); it("should throw error if stdout is missing", function () { const invalidProcess = { @@ -93,31 +93,31 @@ describe("StreamHandler", function () { }, }, // Mock stdin with on method stdout: null, - } as any as child_process.ChildProcess + } as any as child_process.ChildProcess; expect(() => { - streamHandler.setupStreamListeners(invalidProcess, mockContext) - }).to.throw("Given proc had no stdout") - }) - }) + 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 }[] = [] + let mockTask: Task; + let taskDataEvents: { data: any; task: any; context: any }[] = []; + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []; beforeEach(function () { - taskDataEvents = [] - noTaskDataEvents = [] + taskDataEvents = []; + noTaskDataEvents = []; // Set up event listeners emitter.on("taskData", (data, task, context) => { - taskDataEvents.push({ data, task, context }) - }) + taskDataEvents.push({ data, task, context }); + }); emitter.on("noTaskData", (stdout, stderr, context) => { - noTaskDataEvents.push({ stdout, stderr, context }) - }) + noTaskDataEvents.push({ stdout, stderr, context }); + }); // Create a mock task mockTask = { @@ -125,73 +125,73 @@ describe("StreamHandler", function () { onStdout: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should process stdout data with active task", function () { - mockContext.getCurrentTask = () => mockTask - const testData = "test output" + mockContext.getCurrentTask = () => mockTask; + const testData = "test output"; - streamHandler.processStdout(testData, mockContext) + 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) - }) + 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" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => true; + const testData = "test output"; - streamHandler.processStdout(testData, mockContext) + streamHandler.processStdout(testData, mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + 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" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; + const testData = "unexpected output"; - streamHandler.processStdout(testData, mockContext) + 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") - }) + 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 + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; - streamHandler.processStdout("", mockContext) - streamHandler.processStdout(" ", mockContext) - streamHandler.processStdout("\n", mockContext) + 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) - }) + 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 + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; - streamHandler.processStdout(null as any, mockContext) + streamHandler.processStdout(null as any, mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + 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 = { @@ -199,32 +199,32 @@ describe("StreamHandler", function () { onStdout: () => { /* mock implementation */ }, - } as unknown as Task + } as unknown as Task; - mockContext.getCurrentTask = () => nonPendingTask - mockContext.isEnding = () => false - const testData = "test output" + mockContext.getCurrentTask = () => nonPendingTask; + mockContext.isEnding = () => false; + const testData = "test output"; - streamHandler.processStdout(testData, mockContext) + 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") - }) - }) + 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 }[] = [] + let mockTask: Task; + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []; beforeEach(function () { - noTaskDataEvents = [] + noTaskDataEvents = []; // Set up event listeners emitter.on("noTaskData", (stdout, stderr, context) => { - noTaskDataEvents.push({ stdout, stderr, context }) - }) + noTaskDataEvents.push({ stdout, stderr, context }); + }); // Create a mock task mockTask = { @@ -232,56 +232,56 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should process stderr data with active task", function () { - mockContext.getCurrentTask = () => mockTask - const testData = "error output" + mockContext.getCurrentTask = () => mockTask; + const testData = "error output"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + 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" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => true; + const testData = "error output"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + 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" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; + const testData = "unexpected error"; - streamHandler.processStderr(testData, mockContext) + 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") - }) + 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 + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; - streamHandler.processStderr("", mockContext) - streamHandler.processStderr(" ", mockContext) - streamHandler.processStderr("\n", mockContext) + streamHandler.processStderr("", mockContext); + streamHandler.processStderr(" ", mockContext); + streamHandler.processStderr("\n", mockContext); - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + 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 = { @@ -289,54 +289,54 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task + } as unknown as Task; - mockContext.getCurrentTask = () => nonPendingTask - mockContext.isEnding = () => false - const testData = "error output" + mockContext.getCurrentTask = () => nonPendingTask; + mockContext.isEnding = () => false; + const testData = "error output"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(1) - expect(endCalls).to.have.length(1) - expect(endCalls[0]?.reason).to.eql("stderr") - }) - }) + 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 - }) + 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() + 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 - }) - }) + 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 }[] = [] + let mockTask: Task; + let taskDataEvents: { data: any; task: any; context: any }[] = []; beforeEach(function () { - taskDataEvents = [] + taskDataEvents = []; emitter.on("taskData", (data, task, context) => { - taskDataEvents.push({ data, task, context }) - }) + taskDataEvents.push({ data, task, context }); + }); mockTask = { pending: true, @@ -346,46 +346,46 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should handle Buffer data in stdout", function () { - mockContext.getCurrentTask = () => mockTask - const bufferData = Buffer.from("test buffer data") + mockContext.getCurrentTask = () => mockTask; + const bufferData = Buffer.from("test buffer data"); - streamHandler.processStdout(bufferData, mockContext) + streamHandler.processStdout(bufferData, mockContext); - expect(taskDataEvents).to.have.length(1) - expect(taskDataEvents[0]?.data).to.eql(bufferData) - }) + 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") + 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() - }) - }) + 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 }[] = [] + let mockTask: Task; + let taskDataEvents: { data: any; task: any; context: any }[] = []; + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []; beforeEach(function () { - taskDataEvents = [] - noTaskDataEvents = [] + taskDataEvents = []; + noTaskDataEvents = []; emitter.on("taskData", (data, task, context) => { - taskDataEvents.push({ data, task, context }) - }) + taskDataEvents.push({ data, task, context }); + }); emitter.on("noTaskData", (stdout, stderr, context) => { - noTaskDataEvents.push({ stdout, stderr, context }) - }) + noTaskDataEvents.push({ stdout, stderr, context }); + }); mockTask = { pending: true, @@ -395,47 +395,47 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should handle mixed stdout and stderr with active task", function () { - mockContext.getCurrentTask = () => mockTask + mockContext.getCurrentTask = () => mockTask; - streamHandler.processStdout("stdout data", mockContext) - streamHandler.processStderr("stderr data", mockContext) + 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) - }) + 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) + mockContext.getCurrentTask = () => mockTask; + streamHandler.processStdout("initial output", mockContext); - expect(taskDataEvents).to.have.length(1) + expect(taskDataEvents).to.have.length(1); // Task completes, no current task - mockContext.getCurrentTask = () => undefined - streamHandler.processStdout("stray output", mockContext) + 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") - }) + 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) - }) - }) -}) + 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 index 40e9fdd..7e84029 100644 --- a/src/StreamHandler.ts +++ b/src/StreamHandler.ts @@ -1,26 +1,26 @@ -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" +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 + 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 + readonly name: string; + isEnding(): boolean; + getCurrentTask(): Task | undefined; + onError: (reason: string, error: Error) => void; + end: (gracefully: boolean, reason: string) => void; } /** @@ -28,13 +28,13 @@ export interface StreamContext { * Manages stream event listeners, data routing, and error handling. */ export class StreamHandler { - readonly #logger: () => Logger + readonly #logger: () => Logger; constructor( options: StreamHandlerOptions, private readonly emitter: BatchClusterEmitter, ) { - this.#logger = options.logger + this.#logger = options.logger; } /** @@ -44,40 +44,40 @@ export class StreamHandler { 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 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)) + 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("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 + if (data == null) return; - const task = context.getCurrentTask() + 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) + 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") + this.emitter.emit("noTaskData", data, null, context as any); + context.end(false, "stdout.error"); } } @@ -85,18 +85,18 @@ export class StreamHandler { * Handle stderr data from a child process */ #onStderr(data: string | Buffer, context: StreamContext): void { - if (blank(data)) return + if (blank(data)) return; - this.#logger().warn(context.name + ".onStderr(): " + String(data)) + this.#logger().warn(context.name + ".onStderr(): " + String(data)); - const task = context.getCurrentTask() + const task = context.getCurrentTask(); if (task != null && task.pending) { - task.onStderr(data) + 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") + this.emitter.emit("noTaskData", null, data, context as any); + context.end(false, "stderr"); } } @@ -104,21 +104,21 @@ export class StreamHandler { * Process stdout data directly (for testing or manual processing) */ processStdout(data: string | Buffer, context: StreamContext): void { - this.#onStdout(data, context) + this.#onStdout(data, context); } /** * Process stderr data directly (for testing or manual processing) */ processStderr(data: string | Buffer, context: StreamContext): void { - this.#onStderr(data, context) + this.#onStderr(data, context); } /** * Check if data is considered blank/empty */ isBlankData(data: string | Buffer | null | undefined): boolean { - return blank(data) + return blank(data); } /** @@ -128,6 +128,6 @@ export class StreamHandler { return { handlerActive: true, emitterConnected: this.emitter != null, - } + }; } } diff --git a/src/String.ts b/src/String.ts index 6c9b2d1..63e4111 100644 --- a/src/String.ts +++ b/src/String.ts @@ -1,21 +1,21 @@ export function blank(s: unknown): boolean { - return s == null || toS(s).trim().length === 0 + return s == null || toS(s).trim().length === 0; } export function notBlank(s: unknown): boolean { - return !blank(s) + return !blank(s); } export function toNotBlank(s: unknown): string | undefined { - const result = toS(s).trim() - return result.length === 0 ? undefined : result + 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: unknown): string { /* eslint-disable-next-line @typescript-eslint/no-base-to-string */ - return s == null ? "" : s.toString() + return s == null ? "" : s.toString(); } diff --git a/src/Task.ts b/src/Task.ts index ddd5bc3..f104454 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -1,14 +1,14 @@ -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 @@ -16,14 +16,14 @@ let _taskId = 1 * result of the task. */ export class Task { - readonly taskId = _taskId++ - #opts?: TaskOptions - #startedAt?: number - #parsing = false - #settledAt?: number - readonly #d = new Deferred() - #stdout = "" - #stderr = "" + 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 @@ -40,18 +40,18 @@ export class Task { this.#d.promise.then( () => 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 { @@ -59,18 +59,18 @@ export class Task { ? "pending" : this.#d.rejected ? "rejected" - : "resolved" + : "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 { @@ -80,81 +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, "") - void 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, "") - void 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, "") - void 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"), - ) + ); } } catch (error: unknown) { - this.reject(error instanceof Error ? error : new Error(String(error))) + this.reject(error instanceof Error ? error : new Error(String(error))); } } } diff --git a/src/TaskQueueManager.spec.ts b/src/TaskQueueManager.spec.ts index 2632760..664dff8 100644 --- a/src/TaskQueueManager.spec.ts +++ b/src/TaskQueueManager.spec.ts @@ -1,19 +1,19 @@ -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" +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 + let queueManager: TaskQueueManager; + let emitter: BatchClusterEmitter; + let mockProcess: BatchProcess; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter - queueManager = new TaskQueueManager(logger, emitter) + emitter = new events.EventEmitter() as BatchClusterEmitter; + queueManager = new TaskQueueManager(logger, emitter); // Create a mock process that can execute tasks mockProcess = { @@ -21,243 +21,243 @@ describe("TaskQueueManager", function () { idle: true, pid: 12345, execTask: () => true, // Always succeed - } as unknown as BatchProcess - }) + } 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([]) - }) + 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 - }) - }) + 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) + 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) - }) + 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) + 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 - }) + 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) + 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) + 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) - }) - }) + expect(queueManager.pendingTaskCount).to.eql(3); + expect(queueManager.pendingTasks).to.have.length(3); + }); + }); describe("task assignment", function () { - let task: Task + let task: Task; beforeEach(function () { - task = new Task("test command", parser) - queueManager.enqueueTask(task, false) - }) + task = new Task("test command", parser); + queueManager.enqueueTask(task, false); + }); it("should assign task to ready process", function () { - const result = queueManager.tryAssignNextTask(mockProcess) + const result = queueManager.tryAssignNextTask(mockProcess); - expect(result).to.be.true - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - }) + 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) + const result = queueManager.tryAssignNextTask(undefined); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(1) - expect(queueManager.isEmpty).to.be.false - }) + 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 + } as unknown as BatchProcess; - const result = queueManager.tryAssignNextTask(failingProcess) + const result = queueManager.tryAssignNextTask(failingProcess); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(1) // Task should be re-queued - }) + 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 + } as unknown as BatchProcess; - const result = queueManager.tryAssignNextTask(failingProcess, 0) + const result = queueManager.tryAssignNextTask(failingProcess, 0); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(1) // Task remains when retries exhausted - }) + 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() + queueManager.clearAllTasks(); - const result = queueManager.tryAssignNextTask(mockProcess) + const result = queueManager.tryAssignNextTask(mockProcess); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(0) - }) - }) + 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) + 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) + 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 - }) + 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) + 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 - }) + 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 + let callCount = 0; const findReadyProcess = () => { - callCount++ - return callCount <= 3 ? mockProcess : undefined - } + callCount++; + return callCount <= 3 ? mockProcess : undefined; + }; - const assignedCount = queueManager.processQueue(findReadyProcess) + const assignedCount = queueManager.processQueue(findReadyProcess); - expect(assignedCount).to.eql(3) - expect(queueManager.pendingTaskCount).to.eql(2) - }) + 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 + } as unknown as BatchProcess; - const findReadyProcess = () => failingProcess - const assignedCount = queueManager.processQueue(findReadyProcess) + const findReadyProcess = () => failingProcess; + const assignedCount = queueManager.processQueue(findReadyProcess); - expect(assignedCount).to.eql(0) - expect(queueManager.pendingTaskCount).to.be.greaterThan(0) // Tasks remain queued - }) - }) + 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) + const task = new Task(`command ${i}`, parser); + queueManager.enqueueTask(task, false); } - }) + }); it("should clear all tasks", function () { - expect(queueManager.pendingTaskCount).to.eql(3) + expect(queueManager.pendingTaskCount).to.eql(3); - queueManager.clearAllTasks() + queueManager.clearAllTasks(); - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - expect(queueManager.pendingTasks).to.eql([]) - }) + 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() + const stats = queueManager.getQueueStats(); - expect(stats.pendingTaskCount).to.eql(3) - expect(stats.isEmpty).to.be.false - }) - }) + 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) + 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 + 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) - }) - }) + 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 executedTasks: Task[] = []; const trackingProcess = { ...mockProcess, execTask: (task: Task) => { - executedTasks.push(task) - return true + executedTasks.push(task); + return true; }, - } as unknown as BatchProcess + } 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) + 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) + 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") - }) - }) -}) + 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 index 3412e96..1f203fc 100644 --- a/src/TaskQueueManager.ts +++ b/src/TaskQueueManager.ts @@ -1,21 +1,21 @@ -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { BatchProcess } from "./BatchProcess" -import { Logger } from "./Logger" -import { Task } from "./Task" +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 + readonly #tasks: Task[] = []; + readonly #logger: () => Logger; constructor( logger: () => Logger, private readonly emitter?: BatchClusterEmitter, ) { - this.#logger = logger + this.#logger = logger; } /** @@ -25,39 +25,39 @@ export class TaskQueueManager { if (ended) { task.reject( new Error("BatchCluster has ended, cannot enqueue " + task.command), - ) + ); } else { - this.#tasks.push(task as Task) + this.#tasks.push(task as Task); } - return task.promise + return task.promise; } /** * Simple enqueue method (alias for enqueueTask without ended check) */ enqueue(task: Task): void { - this.#tasks.push(task) + this.#tasks.push(task); } /** * Get the number of pending tasks in the queue */ get pendingTaskCount(): number { - return this.#tasks.length + return this.#tasks.length; } /** * Get all pending tasks (mostly for testing) */ get pendingTasks(): readonly Task[] { - return this.#tasks + return this.#tasks; } /** * Check if the queue is empty */ get isEmpty(): boolean { - return this.#tasks.length === 0 + return this.#tasks.length === 0; } /** @@ -69,28 +69,28 @@ export class TaskQueueManager { retries = 1, ): boolean { if (this.#tasks.length === 0 || retries < 0) { - return false + return false; } // no procs are idle and healthy :( if (readyProcess == null) { - return false + return false; } - const task = this.#tasks.shift() + const task = this.#tasks.shift(); if (task == null) { - this.emitter?.emit("internalError", new Error("unexpected null task")) - return false + this.emitter?.emit("internalError", new Error("unexpected null task")); + return false; } - const submitted = readyProcess.execTask(task) + 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) + 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) + return this.tryAssignNextTask(readyProcess, retries - 1); } this.#logger().trace( @@ -99,9 +99,9 @@ export class TaskQueueManager { child_pid: readyProcess.pid, task, }, - ) + ); - return submitted + return submitted; } /** @@ -109,24 +109,24 @@ export class TaskQueueManager { * Returns the number of tasks successfully assigned. */ processQueue(findReadyProcess: () => BatchProcess | undefined): number { - let assignedCount = 0 + let assignedCount = 0; while (this.#tasks.length > 0) { - const readyProcess = findReadyProcess() + const readyProcess = findReadyProcess(); if (!this.tryAssignNextTask(readyProcess)) { - break + break; } - assignedCount++ + assignedCount++; } - return assignedCount + return assignedCount; } /** * Clear all pending tasks (used during shutdown) */ clearAllTasks(): void { - this.#tasks.length = 0 + this.#tasks.length = 0; } /** @@ -136,6 +136,6 @@ export class TaskQueueManager { return { pendingTaskCount: this.#tasks.length, isEmpty: this.isEmpty, - } + }; } } diff --git a/src/Timeout.ts b/src/Timeout.ts index 94a16c9..ea80bb0 100644 --- a/src/Timeout.ts +++ b/src/Timeout.ts @@ -1,5 +1,5 @@ -import timers from "node:timers" -export const Timeout = Symbol("timeout") +import timers from "node:timers"; +export const Timeout = Symbol("timeout"); export async function thenOrTimeout( p: Promise, @@ -10,29 +10,29 @@ export async function thenOrTimeout( return timeoutMs <= 1 ? p : new Promise((resolve, reject) => { - let pending = true + let pending = true; const t = timers.setTimeout(() => { if (pending) { - pending = false - resolve(Timeout) + pending = false; + resolve(Timeout); } - }, timeoutMs) + }, timeoutMs); p.then( (result) => { if (pending) { - pending = false - clearTimeout(t) - resolve(result) + pending = false; + clearTimeout(t); + resolve(result); } }, (err: unknown) => { if (pending) { - pending = false - clearTimeout(t) - reject(err instanceof Error ? err : new Error(String(err))) + pending = false; + clearTimeout(t); + reject(err instanceof Error ? err : new Error(String(err))); } }, - ) - }) + ); + }); } diff --git a/src/WhyNotHealthy.ts b/src/WhyNotHealthy.ts index 4bdb103..53f0d70 100644 --- a/src/WhyNotHealthy.ts +++ b/src/WhyNotHealthy.ts @@ -20,6 +20,6 @@ export type WhyNotHealthy = | "tooMany" // < only sent by BatchCluster when maxProcs is reduced | "startError" | "unhealthy" - | "worn" + | "worn"; -export type WhyNotReady = WhyNotHealthy | "busy" +export type WhyNotReady = WhyNotHealthy | "busy"; diff --git a/src/_chai.spec.ts b/src/_chai.spec.ts index e52fe78..02ac698 100644 --- a/src/_chai.spec.ts +++ b/src/_chai.spec.ts @@ -1,25 +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 "node:child_process" -import path from "node:path" -import process from "node: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( @@ -37,20 +37,20 @@ setLogger( ), ), ), -) +); -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, @@ -58,32 +58,32 @@ export const parser: Parser = ( 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,36 +92,37 @@ type WithinTolerance = ( expected: number, tol: number | number[], message?: string, -) => Chai.Assertion +) => 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 + ); + return result; } // Seeding the RNG deterministically _should_ give us repeatable @@ -131,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 @@ -161,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,7 +198,7 @@ export const processFactory = () => { unluckyfail, }, }, - ) - procs.push(proc) - return proc -} + ); + childProcs.push(proc); + return proc; +}; diff --git a/src/test-helpers.ts b/src/test-helpers.ts index f256031..ab79908 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -1 +1 @@ -export const ErrorPrefix = "ERROR: " +export const ErrorPrefix = "ERROR: "; diff --git a/src/test.spec.ts b/src/test.spec.ts index 98b89e8..fcf613c 100644 --- a/src/test.spec.ts +++ b/src/test.spec.ts @@ -1,155 +1,159 @@ -import child_process from "node: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" +} 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.notRunning(), 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; } running(): boolean { - return pidExists(this.child.pid) + return pidExists(this.child.pid); } notRunning(): boolean { - return !this.running() + 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 - }) + ); + 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 - }) + 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(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( @@ -162,8 +166,8 @@ describe("test.js", () => { "FAIL", ].join("\n"), ), - ) - h.child.stdin!.end("flaky .5\nflaky 0\nflaky 1\nexit\n") - return a - }) -}) + ); + 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 afee866..e2cd114 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import process from "node: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,43 +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), ), - ) + ); } -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 ? // eslint-disable-next-line @typescript-eslint/no-require-imports require("seedrandom")(process.env.rngseed) - : Math.random + : 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( @@ -56,25 +56,25 @@ async function onLine(line: string): Promise { failrate.toFixed(2) + ", seed: " + 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") + @@ -85,70 +85,70 @@ async function onLine(line: string): Promise { // 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 +}