diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..0d477ef --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,24 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run compile:*)", + "Bash(npm run lint:*)", + "Bash(ls:*)", + "Bash(rg:*)", + "Bash(grep:*)", + "Bash(npm info:*)", + "Bash(sed:*)", + "Bash(npm test)", + "Bash(npx eslint:*)", + "Bash(git add:*)", + "Bash(npm t)", + "Bash(mocha:*)", + "Bash(npx mocha:*)", + "Bash(find:*)", + "Bash(/usr/bin/rg -n \"onStdout|onStderr\" BatchProcess.ts)", + "Bash(timeout 45s npm run test:compile)", + "Bash(timeout:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 2e0a7e4..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = { - env: { - node: true, - }, - extends: [ - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:eslint-plugin-import/recommended", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - project: "tsconfig.json", - sourceType: "module", - }, - plugins: ["@typescript-eslint", "eslint-plugin-import"], - rules: { - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/member-delimiter-style": [ - "warn", - { multiline: { delimiter: "none" } }, - ], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/await-thenable": ["error"], - eqeqeq: ["warn", "always", { null: "ignore" }], - "import/no-cycle": "warn", - "import/no-unresolved": "off", - "no-redeclare": "warn", - "no-undef-init": "warn", - "no-unused-expressions": "warn", - }, -} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 21c1640..83b4e50 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + 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. diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f28622a..b28aef5 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,12 +13,12 @@ jobs: lint: runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a with: - node-version: "18" - - run: yarn install - - run: yarn lint + node-version: "20" + - run: npm ci + - run: npm run lint build: runs-on: ${{ matrix.os }} @@ -29,13 +29,13 @@ jobs: matrix: os: [ubuntu-latest, macos-14, windows-latest] # See https://github.com/nodejs/release#release-schedule - node-version: [18.x, 20.x, 21.x] + node-version: [20.x, 22.x, 23.x, 24.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a with: node-version: ${{ matrix.node-version }} - - run: yarn ci - - run: yarn test + - run: npm ci + - run: npm test diff --git a/.gitignore b/.gitignore index 951ea24..f10c721 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.dccache .node_repl_history .npm .nyc_output/ @@ -6,5 +7,4 @@ coverage/ dist node_modules npm-debug.log* -yarn.lock -package-lock.json \ No newline at end of file +yarn.lock \ No newline at end of file diff --git a/.ncurc.json b/.ncurc.json index e5848b7..3de3741 100644 --- a/.ncurc.json +++ b/.ncurc.json @@ -1,6 +1,12 @@ { "reject": [ "@types/chai", - "chai" + "@types/chai-as-promised", + "chai-as-promised", + "chai-as-promised why: v8 went to ESM", + "chai", + "eslint", + "rimraf", + "rimraf why: https://github.com/isaacs/rimraf/issues/316 (!!)" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6af3ca4..45e960a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "rngseed" ], "cSpell.words": [ + "sinonjs", "zombification" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 34b97d5..a533adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,18 @@ See [Semver](http://semver.org/). - 📦 Minor packaging changes +## v14.0.0 + +- 💔 Dropped official support for Node v14, v16, and v18. Minimum Node.js version is now v20. + +- ✨ Added startup validation for procps availability: `BatchCluster` now throws `ProcpsMissingError` during construction if the required `ps` command (or `tasklist` on Windows) is not available. This provides clear, actionable error messages instead of cryptic runtime failures. Resolves [#13](https://github.com/photostructure/batch-cluster.js/issues/13) and [#39](https://github.com/photostructure/batch-cluster.js/issues/39). + +- 📦 Significant internal refactoring to improve maintainability: + - Extracted process management logic into dedicated classes (`ProcessPoolManager`, `TaskQueueManager`, `ProcessHealthMonitor`, `StreamHandler`, `ProcessTerminator`) + - Implemented strategy pattern for health checking logic + - Improved type safety by replacing `any` with `unknown` throughout the codebase + - Enhanced error handling and process lifecycle management + ## v13.0.0 - 💔 Dropped official support for [Node v16, which is EOL](https://nodejs.org/en/blog/announcements/nodejs16-eol/). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4f53832 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +### Build & Development +- `npm install` - Install dependencies +- `npm run compile` - Compile TypeScript to JavaScript (outputs to dist/) +- `npm run watch` - Watch mode for TypeScript compilation +- `npm run clean` - Clean build artifacts + +### Testing & Quality +- `npm test` - Run all tests (includes linting and compilation) +- `npm run lint` - Run ESLint on TypeScript source files +- `npm run fmt` - Format code with Prettier +- `mocha dist/**/*.spec.js` - Run specific test files after compilation +- `npx mocha --require ts-node/register src/Object.spec.ts` - Run individual tests like this + +### Documentation +- `npm run docs` - Generate and serve TypeDoc documentation + +## Architecture Overview + +This library manages clusters of child processes to efficiently handle batch operations through stdin/stdout communication. Key architectural concepts: + +### Core Components + +1. **BatchCluster** (`src/BatchCluster.ts`) - Main entry point that manages a pool of child processes + - Handles process lifecycle, task queuing, and load balancing + - Monitors process health and automatically recycles processes + - Emits events for monitoring and debugging + +2. **BatchProcess** (`src/BatchProcess.ts`) - Wrapper around individual child processes + - Manages communication with a single child process + - Tracks process state, health, and task processing + - Handles process recycling based on task count or runtime + +3. **Task** (`src/Task.ts`) - Represents a unit of work to be processed + - Contains the command to send to child process + - Includes parser for processing responses + - Manages timeouts and completion promises + +### Key Patterns + +- **Parser Interface** - Consumers must implement parsers to handle child process output +- **Deferred Pattern** - Used extensively for promise-based task completion +- **Rate Monitoring** - Tracks error rates to prevent runaway failures +- **Process Recycling** - Automatic process replacement after N tasks or N seconds + +### Testing Approach + +The test suite uses a custom test script (`src/test.ts`) that simulates a batch-mode command-line tool with configurable failure rates. Tests can control: +- `failrate` - Probability of task failure +- `rngseed` - Seed for deterministic randomness +- `ignoreExit` - Whether to ignore termination signals + +## TypeScript Configuration + +- Strict mode enabled with all strict checks +- Targets ES2019, CommonJS modules +- Outputs to `dist/` with source maps and declarations +- No implicit any, strict null checks, no unchecked indexed access + +## Code Style Guidelines + +- **Null checks**: Always use explicit `x == null` or `x != null` checks. Do not use falsy/truthy checks for nullish values. + - Good: `if (value != null)`, `if (value == null)` + - Bad: `if (value)`, `if (!value)` \ No newline at end of file diff --git a/LICENSE b/LICENSE index b6f0c29..0c4295c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2024 Matthew McEachen +Copyright (c) 2017-2025 Matthew McEachen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b5938a5..c1fc419 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,7 @@ whose source you can examine as an example consumer. ## Installation -Depending on your yarn/npm preference: - ```bash -$ yarn add batch-cluster -# or $ npm install --save batch-cluster ``` diff --git a/docs/assets/icons.js b/docs/assets/icons.js new file mode 100644 index 0000000..b79c9e8 --- /dev/null +++ b/docs/assets/icons.js @@ -0,0 +1,15 @@ +(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 new file mode 100644 index 0000000..7dead61 --- /dev/null +++ b/docs/assets/icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/main.js b/docs/assets/main.js index 7270cff..d6f1388 100644 --- a/docs/assets/main.js +++ b/docs/assets/main.js @@ -1,8 +1,8 @@ "use strict"; -"use strict";(()=>{var Pe=Object.create;var ne=Object.defineProperty;var Ie=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=Ie(e,i))||r.enumerable});return t};var De=(t,e,n)=>(n=t!=null?Pe(_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),v=s.str.charAt(1),f;v in s.node.edges?f=s.node.edges[v]:(f=new t.TokenSet,s.node.edges[v]=f),s.str.length==1&&(f.final=!0),i.push({node:f,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.ensureActivePageVisible(),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible())}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()}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){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r}}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(()=>{Ne(t,e,n,r)},200));let i=!1;n.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ve(e,n):s.key=="Escape"?n.blur():s.key=="ArrowUp"?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 Ne(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 v=document.createElement("li");v.classList.value=l.classes??"";let f=document.createElement("a");f.href=r.base+l.url,f.innerHTML=u+d,v.append(f),e.appendChild(v)}}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 Ve(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),e.blur()}}function 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 Be={"&":"&","<":"<",">":">","'":"'",'"':"""};function K(t){return t.replace(/[&<>"'"]/g,e=>Be[e])}var C=class{constructor(e){this.el=e.el,this.app=e.app}};var F="mousedown",pe="mousemove",B="mouseup",J={x:0,y:0},fe=!1,ee=!1,He=!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&&(He=!0,F="touchstart",pe="touchmove",B="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(pe,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(B,()=>{ee=!1});document.addEventListener("click",t=>{fe&&(t.preventDefault(),t.stopImmediatePropagation(),fe=!1)});var X=class extends C{constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(B,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(F,n=>this.onDocumentPointerDown(n)),document.addEventListener(B,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 ve=document.head.appendChild(document.createElement("style"));ve.dataset.for="filters";var Y=class extends C{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()),ve.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } -`,this.handleValueChange()}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(),document.querySelectorAll(".tsd-index-section").forEach(e=>{e.style.display="block";let n=Array.from(e.querySelectorAll(".tsd-index-link")).every(r=>r.offsetParent==null);e.style.display=n?"none":"block"})}};var Z=class extends C{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,ye(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),ye(t.value)})}function ye(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.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.href===r.href&&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();})(); +"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: diff --git a/docs/assets/navigation.js b/docs/assets/navigation.js index 553b3a9..0fa6114 100644 --- a/docs/assets/navigation.js +++ b/docs/assets/navigation.js @@ -1 +1 @@ -window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA42UTVPCMBCG/0vPKFIFlaOI44FBBp3x4HiI7UIzhKaTLAwdx/9uSvlI02bLdd99njabbb9+A4QdBsPgiWGUjMRGI6igE2QME1ONBNMadNdOrxNcC9Oy4mkcDHvhw1+n0fKWIZeppmWHplbnTMkItE92SCnLMyxAKYjrhmNC0XOGUCeLKkV9ML2qU0X10hmOt5CidWqemuKCRc4Uy7aqNOwPPEOsXYxrrfZR2lHCRXxof2ERSpU3ahv6KO1ELpf2GlqmMqLgGVPaA5cRBX/kGcT7cY7XHNHjqXW1jv54Ua4Ujcm5y0bjzeN9rx+6kx/vOM6BaZm6Ridus30m+VTiKzCBSe66KuFlJvPU2OPZR62nM3snBbh7sGWKsx9RnM9uqNpuw+omNeGmTEMT2ILQHrQMCcFU+t/8mBH4O19nAtw9XmzSaP9Bdu28qhncWZoVF6IJL+oEJpxXP4NlctUj4IwXW6ftf9aZP4W0wMdSmAZ0R35mT2FN8P0PNd1pyAEHAAA=" \ No newline at end of file +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 index 9000844..cf04735 100644 --- a/docs/assets/search.js +++ b/docs/assets/search.js @@ -1 +1 @@ -window.searchData = "data:application/octet-stream;base64,H4sIAAAAAAAAA9VdW5Mbt47+L+PXyURk3/N24vjspirJ5sSpPQ9TLpcs9Xh0LLUUqeVLpfzfl5e+ABDQYuvm2pfYjkAAxAeC5Ed299932/Wn3d0Pj3/ffVhU87sfdJLe31XTVXn3w93L58Vy/vt2PSt3u39OZ/V6++Xu/m6/XZrfFlVdbp+m5qfvGbGH53q1NLKz5XS3K43+u7uv960JNdFxZ2NzsvoXB02Btfu7zXRbVrXQi96ZNEmivsdv39ZfNuUZXjx0GoKdedhIvimdd579OK1nzy+X+53xqPOvsfE9/HEw9InSncbZutrV2701elzhCyzNdw75KOC93tQLoyrAYi95urVytaiDAvailzyjb1VIt6qxNgZTdMhQk4zf6SCLD8YzqWNPTyEGndRNuvb0dGyg4Z4Zz3qjOtUqBjlSzct5SIZYue/U2C7qSZxDW2GWzrTy177cl39Odx+CrEHpcVZxJBe7n+fLEPw6wXOsbUyYFtV76/fL9d40PG6XaXKOB6tyWlldu9/Lra3pAR4wTc7xYLeZfqpKN6GExoBpchYKI0xvLmTz3X73ZUyXqfxZEa+n29rk0KiQM20ulPkhEykRP8fybL+18qGWifhZtcUuxarp8tV2u96GBp5tdHpd3SzmQfH2YqfbMflShxhq5W4x5TpbD+6/36l2YZGETb+grbgNGF/MeY8eqKbQ1Q9Q164tBF+brB5ZAThfGU0X9XRbTuejKqXg54Gei3q5mn6+gI9Ey0U9PKn4CH4e6rp8hroJxxn4Y1qXZqnx66La16cP8wdZ4WUzYfdj+bTelr+Vn+vXdp1yRjowqi4b4xOWXkJwiaYrVCzLOryq5k598MzC1Cuk5yJ+XnQ6OnCw/SU91VOi8fiAu4LvYPQFbj+Pd+RIwrzbrj+UZ4w+uS9e8636MVuud0Gb+9H98Jpv1Q+/prlGP7zmG/bjOnB4cia6TS8WYSzH6E5YvbdCYr28Cg5G7a16YNkEPwyv0RGr/a3TftP+zBe72bqqytk5u4rhTvUmbtqz8lqzo+tUecvZ0ffn8+J6GFnlt+rNrp6b6F0PH6//tgh5m9frzQ37saiuCs2iujky63191S4Z/bftU71YlcboNbrTqL5ZT9brX6fVl6v0xKu+VU/21XM5XdbPV+lLp/xWvfm03l5lN2b1/n/ZwzQ7lYuTI2fuSfzOI76AV+gMYGZD+srq7q6MBJ248M3OOvMZSx4dNLjF+QRJRZLyHV6BOA1quwgJFOrvA9J6ruuXon2Cve80XtnzYKIn2PNO45U9Dy6LwZ53Gq/v+WVD3iq8st+B9E2w24vQuyxneR1G2AQ77dVd2edRFE2w6z01c6MejCJlxnUDqb5FX0I3XuO6Ud5mfhpDvIzsgFd6Zf9HUi0j1gg9xXKrXlze/5t4PoJOGeN+S6PcpA9jCJQxneiIk+v3IpwyCe5Ar/LavgeTJOG+dyqv7PsYWiTYe6j0yv4HEiHBrjf6Lu813szbxQp89KMM2kezrc64VljWv/pLUkFXQJD06VY/Tmf7/SrUKpYeZVV6WuZ/Ap44aWQu/uwM1Dv6EZrW8eFrc+Ede7Eag2eY9X+8L39dLJeL8V7Almd7s67sAwU/26t1H6fLsR4JrS8Roz/K6W5dTd8ty+6hr8Vyvy13YZfyaOCC1J3tt7uQ9qefTsfGkm17fiQX1U/lcvrlx7L+VJaVu9A3Ou+GdJztYT3dfTgxZFzTS+QefM7EpMqoRDtse7ZHZTX/r+10Vj7tl/+eLmrb47GRGlJxftbX23K6+qeRfR6d9EzTs/2ZLctptd8E0v94pmGaXiKjbJn89dSUYhpfwidbAcv5ObkuaTjbO78qfvlczj6cOjUNqTjbv81ifpZzYvuzPVuu378/8tgu8qSTP8Xy6BMnxvRD1yzYg4fG6YEV7GA2Nz9ebs0KFYYvVlsfBSTdH8fNNWKn2zG5GGDGS51uxR3GBdhp5c60tN/YqvRzSM+o/OmWn7pyOPBcA7J92OIMHMXnaDGQR5+dPWJnvZHOspGdRmyUHfmZyZBxh6TPsVuPQLA+FTvh6djQhD16TnnU4tDxKbIXcioaYk068qTGjp1kHrf1eVGHGWsFz7H26fnLb+v6v4fIQWSUyp9je5CSRFaDeMaj9uQDYGTs+LluYFT/sI9PBse0lT7H7jbQ4vYCthZuXR8Yz1/HV1NIJm73VRU23HvJ061VBoxgg0j4dJur6Zd3pVHkR9bMLq8DjPOtTvei/FzOAicsIHqGPem9KLSqjp7zwZL6p/KpNJProaX2h4sspZGyoGV055e8JFotmCsU2FIvFWyFf63EMTtHJtLjdp72yyezUzwGxAsod6qtbfmfcsZNo9gUEDvV0q6s6+N96qXC7cCBUj8z9/OwiUbkNP0zO4yO5XQjc5qFbblbLz8ey+Ze6lQr/+Gu63Con2xj/W5Xbo/2pJc6y8q/9ouyXh7O6ayxXvi0ivNYr1/XW/cWjfdvjph8C2TfjrGY9vZeL1abZfn7dLsDpM/Tvpo5ouR7+PNgjQYqP5jKwaiy/ztUxWYxf/V5sQP7xF5P99sIZYKeYRVwArPvXjhAw/7Pi0xcnaKgScv5Ir7MZbtYz5k1YW8CiIzX/2m6Xe03g/qBSJB+POSqVx9LZt/cq+8lwrSTnZ1ty2/NexNI6BQrq93rRTUrf5nu6mPdYURPs/h7uT1uCwidHD134DmUAFjqLDuvSzMgDid2xlYneV6/+HNhrm/DR75DOW6PiAZrQPN72OgERYrdNtj/eZEi1SkKKlLOl4EDW4ZZ7Q3Uw1TqkO7ZerWaMikDvW8lxmvf4HnyUHknEKT78IV+7G4DqD+y0ziiX9hlAP1HdhjD+u3DVUPet7+H6cbTwmuW/u919xKneL41td6eaB+Wtd4ClBnfg3apNpT1vchpEZpz1zpxiOZD1zSP6ufuHhP9Q5eJh/QLuwYQ/uEdw4HuIlNJX9jgIeAr8mZie3qITxkbicGyCd/Wjdq6GaJTDt5jfSg1vPw8eI0UHgPHNb9ArQbIG+x64PHsKPvjHgJsdAD3h8LyCpT7UKdeHeOzrhoSY70NSNgztTAg1vXwxyADXBr5mOPFQ9Lbb4Oi8jFRAf5Lj53BF/+NcY02/AbRQS50ARo1jnAvxLPn+oQAoVbfIDq9/S40kzGhAf4PLFZ/MlJjvAJtvkFMWutdiRkVkc73gXj84SnCUWWXtPtGcWk96GIzaiChPhy/jTrWuT9HPEZzleg0DnTBCXt3IwhO24OB2IwuMbDRN4oLKjB61JTdey/d0lr/eUKFQa2+QVR6+13dHZUswP/jNzhHpwzT9hvEiHrRRWpUAh30Rb7jPTpOoM03iE9rvY3LqEVf57oQjnfuFcQjtwaw0TcISGd+zJXWRkHvupwfI8H5ZlnRvSRpZEIM7c6b+wn0iTTqEBYL359/LLc70+IlYR8D1L84aHr8lkX45fdTHGKbn+3UZrobFfYXTYOzDdsrsqMMNw3ONmwvB54Sf9zuFDco++WeRnll1PqH2AjzRX4NZr3IoTHoWsB5MVT0p3Fj7oYxpeaAzgOhYfWYu5yxJYhX+aIRF3jGA19Fo6NMXsAg+FJYgMWBL4YFm7RfbhthsxE/0+hyYap+ZYrmCMuwzZnmt+Vq/bH8x3L5ywl+sI1PcQiP73+zN4j96Ea/HRkznFZ8gxbqdL+EXsLYlfUv+Mmi/iZG91uosqWkyf/wnQouPcQngGCAR3hva5+OPKLoRSvEQ/7LwVNJ0MC8fLd/f8xAK3SKgUX1tD6mv5E5Rf2n6ZYtiVB9I3OK+lLa/ED9w69YOTAQaZgov5Qfy2U/2j9Otwv7OLiz4H8bzBag7KXJ1fWyJMnXK0S/hyr9bS3qa38KVWWk+W4OD9Chlf+hmhdHNjnWCSmVFvXzEBpQ/wMSFk0dvkbktO4Ac+02Rv40Qm8XejnQacuv7erpahPacdTgNp3vTXZvag0NAPBWWswvlmZgB2NPxK8fAGiw43tCuo88/frm3lSwefn57oe/262lEdcP0UNh2j8tyuXc6Hr07t27Sy9W45vmt/8t7d0cK+FFvp/c3T9O7pPJQ6bevLl/bBu4/+/+h5Myjj4qRkohKW3+pe+1cSbLkZhGYibrHyNGWYSkYvOv+D5KHyZxhsRiJGYy6DFhlCVIKjX/ShmpFEll5l/ZfZw8pBONxDIklkvdzJGYweMx57QVOLQTSZ0iGNhgF/dRZjqBI6IwDJZFfFST+1g/6IhIYiSUDbligcVg2FPFR6U5QQyHsnFXEddthSGxrPCjijmVGBVlo68SViUGxh4OPyoOZ4WhURYClXGCGBx7QPaockZQY3C0GyEFJ0jGiBsk3IjTGBttEdCK67bG4NiTqketWUmMjrYQaG7saQyOPd8RhjIGR2ci3hqDo92w4fDWGBxtIdDcmNYYnMiNHA7uCIMTKTGUEUYncuhwiRGRGubQ4RIjwuBEsZi9EQYncuAUrCRGJxLRiTA6kYUgYitBhNGJLAYRHyIMT2RBiNhsizA+sUUhiu6j4iHTMa7hGKBYLG0xxie2KEQx16EYAxS7WYaNe0wmGguDmdE5SYxQbGGI2BkixgjFFoeIrf4xhih2ELGoxxii2OIQT1hJDFFscYhZMGMMUWJxiFkwEwxRYoGI2YGeYIwSC0Qcs5IYo8QCEbMYJRijxK0GWIwSsh6wQMQsRgnGKEnF9EwwRkkmpWeCIUocRFxVSDBCSSGO9QQjlIrLgxQDlCpxrKcYoFSLYz3FAKWRONZTDFAai8FMMUBpIgUzJWu2VBzrKcYnzcSxnmKA0lwc6ylGKC3EsZ5ihLKJONYzDFGmxLGeYYgyLY71DEOUReJYzzBEWSyO9QxDlCXiWM8wRlkqjvWMrK0zcaxnGKMsF8d6hjHKCnGsZxij3NU5bnmWY4hyi0PCLc9yjFBuYUi4pXOOAcotCgm3dM4xPrm448kxPLnb83CruByjk1sIEm7JlWNwcotAwi25crL1sQAk3JIrx9DkNv4Jt5DKMTKFjX/ClcwCI1M4ZDgIC4xMYeOfchAWGJnCxj/lICwwMoWNf8pBWGBkChv/lEOmwMgUbkPKIVNgZAob/5RDpsDIFDb+KYdMQbalNv4ph0xBN6YWgDRn95ETsjedKGma8j9BUYcPB6T/CYpG0pDwP0FRi0TGoe5/gqIWi4zd9U7IHnVi0cjYfe+E7FInjj3gsPc/QVGLSMZufSdkozqxmGQc/v4nIOrogozd/R4wCRaSjN3/Ui7BMQYZNz4VJRMcZ5CxwFI6QcnLBUUZBSUuGBSlFBxzkLNJQEkFRx3k7LypKK3g2IOczQJKLDj+gF+0KMItKEch5DxXQugF5ViEnE0ZTdkfC0vOpgyhGJQjEnI2ZQjHoByTkLMpQ0gG5biEnE0ZQjMoxybkbMoQokF5pkFgtQhgjlEo2DwgZINynAJfYgjdoByrUPApQxgH5XiFgl1sKcI5KEctFGzliChfZ1Ep2DQgvINy7ELBpgEhHpSjFwo2DQjzoBzBULBpQLgH5RiGgk0DQj4oRzEU7MJYEfpBOZLBzD+sXoKYoxnMBMTylgQxxzSYGYiVJYjFnmFlISM8hHJsg5mDWFnKsjqyaMKCRrgI5RgHnvVThI1QjnMwMxarl8DmWAd+QBBCQjnawcxurFoCW+xhY9OBkBIq8bCxZYHQEsqRD2aCY2UJbI5+UDzfTagJlXhqnE0HQk4oR0GYOY6Vpfy4I8h53psQFMrREGaSY2UJbI6JUDz5TVgK5cgIxfPfhKhQiVwfCVOhHCGhWLZcEbJCpR42FmJCVyhHSpgZkU11wlgox0sIRZpwFsoxE0KRJqyFcuSEmWtZh+nJhsNNs7lDqAvlCArFcvKKkBfKURSK5dAVoS+UIykUS6MrQmAoR1MolklXhMJQjqgQSjUhMVTmgWPzjNAYypEVimXUFSEyVOZPo9jkIVSGcoQFvzEhXIZyjIWZyVm19Ewqk9US1BxpwRNzihAaytEWgloCmiMuzFqCPe0ioOXy7ozQGsqxF2bdwaolmDkCQ1BLIHMUhlmjsGoJZLkMGSE4VO4hY0cE4ThULkNGWA7lyAyz9mHVEshyGTJCdajCQ8aOM8J2qEKGjPAdqvCQ8ceZBLJChoyQHqrwkLEjkvAeqpAhI8yHKjxk7OAl5IcqZMgI/aEcy2EWdaxaevorQ0Y4ED3xSxFu8GrCgWiZA9GEA9GO6DDLSvZgmZwDO6bDLCtZWXIS7KgOs6xkZclZsOM67KsuOFlyHDzxh/XsoSwhQrRjO8yykpUlZ8ITjxqXuppQIXpSyMsATcgQ7RgPxR7FaMKGaEd5KJYd1oQO0Z4OYV0gdIh2nAd7LKAJHaL99QqWdtaEDtH+hgXLPGvCh2h/x4IlnzUhRPQAIaIJIaI9IcKf+hNGRPu7FixdreltCy0zjppeuHC0hyRL71y44cYy4Zpeu3DEh2LJcE0vXmiPG5vq9OqF9rixqU5vX2iPG5u99AKG50VYYlzTKxjaX5Dhb58Q3Bz7oVh6XBNmRDv2Q7EMuSbMiI7kKkmIEe3YD8Xy6ZowIzoSJzZNmBHtr2Sw7Lsm1IiOxIlNE2pEO/5DsVy9JtyIjsSJTTfciLvn97Hc1uX8Z3/f7/Gxu2z4993b5hJgdzP37ztt/vP1/i73f5gi5/406eL+jJo/00n7u2r+YpbBzV+y9i9F85e8lTHLueYvrXDeCuetcNEKF61w0QoXrXDRCOvWusW8+UvU/iVp/+Jafe0vMdp/2Vi9s484zZpvzYFgRH0wooCmZftIUa/BblY6HXaPEqCleasPVJJDJSGurNuHD3stCYA2H9SxaT9I0jdOC+DBZLAbTWvGA7s+6bNL1uKeL3VPhcK2E+BBkUttmy/RAwyBzajJ5VQ0vd99sf7P/BsZgXkIYyG0bl6Z27cqYCs/YLhm9qE42t0Idjc71tT5i2KtAWDtAEhEx52azwv7gSv30B6MewYxk+LuNDTIP03tleAvUAvozGRIQ/NFFZj6MPHMXlxo7D+F1XmBcxcMY19BBA145KoUWE6k0uE+3wl7XyLbMXA/F7tudSD4IzBWo6bEpdKYtf+ekvRRMGypElu6hz/aZ5xA+xQgZmdyuX33dkbQ6b5t3MwhcTtJtKVct1U+bku5nJ02ud23Nnh8Y1AdE9FV/8UVdnBr4LDcV6+gdi++7NtmcIgrMdB9YzxKUZiFxs2DVgAds5gEzaS8mHdvwgcVCZYVsa/+6UloEPqppFHIzH9gDEk1jFY+5GGbLVk7zUtjqPkWC+gpxMW3jps/k0Zb2i4nlFQVrFb/HBcszVB1MdCt981XDD9NTVwWKxMe/+U0EB4wORUSju2LMkF5ABUtbtZfSbsea4eaDFP1177clzSTlYKTldT2IBpJDnNRjIZd1BzOqgjsRBo9fkW0Kber5k24UEMCNQyadxrwgkRFsLUYsLb1rnnzL9QQQw1iHn0uZzTeOUwjOXBmVuYKfJzCaVmandqPBgGrMOJKsurfSwDNwYozGWpWzm1HD6DOYJzEZYR7Rxoz4sB0aEljobF7rmnZPIEFJzPovLgMAx+iAFUEJkgkWX7uP8jCYhXB4ElZBpQwEYA7gCJAxaL5WiNTc0DmiOvo7hNMIHMgCEpq6D+nBEoVXAw05TxN2xIlTflWCx6qORzn4lTpnxmG1QkOEy3FrX1rIRN3WBTFBRxqf7i8gLueZv0TSbVusaMRVDDuUg/8hxDgLArHWyyVc/CyAhg06LA4Ny7XeFGSQuogkkJ1uNZMYWpPWpZBd3+RImU0MWM9KSDgUl1cTT/3tcpWdm7LCzKuGFDkc1XQAVAQVwpGh206fc8sEBKgID+i4DDxQPKLgDSNsVVQsHIJf9Nw232ovNv6+Q+Vs3M1HIu5VD+M1iFMwOxVSEXAfSZru6+e4WeyQCWB+abF3pkNJXQEF3S4uZIUWOGldcV9iNx9MZ3BFzhTiBDtPCVSlZ9rpweBDDRE0jh3+Vn6zzHAxQssLYnceGe/G7GcmlpxqAIWyESCxEnA1QBchYgzYrXmdqawOGlpAqnWFry5e2khLOeIxhG9Xdfdh+VA3sC24lTSfQgI1mLY20hyuWn5V/tVH6gABjmW0sS9Zwf4CzKrqaXisn69xPQDKH1xM2OlUgnEfBEIUrtnU1Kc/duP4LiCxVvcTK+rwzScwMV0IkaossVaXhrBfXUu95dhqeBASqWSYFv6t9bDppBmymTPd80L9WFTuCZKxbxiqGBAmMitUJMMuqkkN9tvUkAnYfKnzQrQHq9KGnaEM4bkzERyd3O4SS4gJu2pgEqlgdtoYDcveJvYdEHKD6AI9wQlt9jYfjGJ7FInsPLE0szpvnoNAIM1Q47bYn5suwCMFyJqC0sf78ixxQTmZywG3n0zC0xocB3Zkhmx2AEyN2fQppxlptWDo11R2QM5HjeWUzHcVsV8sZutq8p9LQLoASGLm3QRh6fTc7DxgPu9uNkuicXBq/iMOTvIUrUcayamHXvmAYf8UBh4zh/mn9zYf2MGjFrEQrVniyL3vZ0SPmgCnY6lStx8ERdMlnBJJ45O14xfakOKQYK6/a4I6C2cttrTKSXC1H/AEqiAnosLQP82uOlyKez44JgTl7Td5xqBdbiYFs9XudUUxEmksJsP4ZB6qGE5FtclO7OMOlw+wq2NFvkH05bdF4G5LJdA6j7+CaIEi1IkFcKd+wAiM4VOYH9jaTXldgblnB/K0IGmGMh+WEW1f706s0YCxS0X40dXSBmcjEX+0DU7rIUg6lFL3XeXDaTC1OuyNYLdkMKzeZHb679gD3qDDhek5G1b8ohA2lwq7E7DfmOXEmRyhxkhsr/NR7DgyIHVNZXmVNsOT8lw0Mit6NoWHvfHzR5EvL3gmx/OhPDcP24qpHiPwihZVIwOkH1JM6eIZ8l+oc0oAUFImvTL5GCYuWL1tNzvnpklFXBH5HIPj2fgKlA85+TJd1ilxe0Yv2OGmZYPOcuQp7DGi+zGYXYrODhUOtRw233mA9qFU6LImtnmdfsZDNga1kqRpwKtmRoJ6kMhzRP+C85gkoDRiqTEYjyGO6Sk2WCJN0bq9Xo1rdDaB57tJM1uP5NqSr3eNV+Og2UFEWJiyJqm9RS3niCCWwyXf58pnNPgzlA8ErAX3OaOMOCuZ8G7A1rc1rZcIg4bwCtpdgvibPRxOtvvV4cLClAhxXOt5q187MkSXLdNpNLqP5NLN5VwjMXSRO7fkgqDDq8DiXPBp+cv1brmDpHgBKQ6hkjKNq/ncLUOi1J3UiDuUez7HrljQTirifO/bVyDV1tCBTABxeXUpzUOIbxQlTS7w4yN/5t7sznelMtFZYQf33z9+n8U8eSOyMsAAA=="; \ No newline at end of file +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 index 98a4377..778b949 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -327,17 +327,14 @@ dd { } /* Footer */ -.tsd-generator { +footer { border-top: 1px solid var(--color-accent); padding-top: 1rem; padding-bottom: 1rem; max-height: 3.5rem; } - -.tsd-generator > p { - margin-top: 0; - margin-bottom: 0; - padding: 0 1rem; +.tsd-generator { + margin: 0 1em; } .container-main { @@ -405,7 +402,8 @@ dd { } body { background: var(--color-background); - font-family: "Segoe UI", sans-serif; + 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); } diff --git a/docs/classes/BatchCluster.html b/docs/classes/BatchCluster.html index 86d5146..12ca6cf 100644 --- a/docs/classes/BatchCluster.html +++ b/docs/classes/BatchCluster.html @@ -1,66 +1,63 @@ -Codestin Search App

Class BatchCluster

BatchCluster instances manage 0 or more homogeneous child processes, and +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

      +

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

-

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

      -

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 +

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 +

      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 +

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.

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

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

    Parameters

    • maxProcs: number

    Returns void

  • For diagnostics. Contents may change.

    +

    Returns {
        childEndCounts: {
            broken: number;
            closed: number;
            ended: number;
            ending: number;
            idle: number;
            old: number;
            proc.close: number;
            proc.disconnect: number;
            proc.error: number;
            proc.exit: number;
            startError: number;
            stderr: number;
            stderr.error: number;
            stdin.error: number;
            stdout.error: number;
            timeout: number;
            tooMany: number;
            unhealthy: number;
            worn: number;
        };
        currentProcCount: number;
        ended: boolean;
        ending: boolean;
        internalErrorCount: number;
        maxProcCount: number;
        msBeforeNextSpawn: number;
        pendingTaskCount: number;
        readyProcCount: number;
        spawnedProcCount: number;
        startErrorRatePerMinute: number;
    }

    • childEndCounts: {
          broken: number;
          closed: number;
          ended: number;
          ending: number;
          idle: number;
          old: number;
          proc.close: number;
          proc.disconnect: number;
          proc.error: number;
          proc.exit: number;
          startError: number;
          stderr: number;
          stderr.error: number;
          stdin.error: number;
          stdout.error: number;
          timeout: number;
          tooMany: number;
          unhealthy: number;
          worn: number;
      }
      • broken: number
      • closed: number
      • ended: number
      • ending: number
      • idle: number
      • old: number
      • proc.close: number
      • proc.disconnect: number
      • proc.error: number
      • proc.exit: number
      • startError: number
      • stderr: number
      • stderr.error: number
      • stdin.error: number
      • stdout.error: number
      • timeout: number
      • tooMany: number
      • unhealthy: number
      • worn: number
    • currentProcCount: number
    • ended: boolean
    • ending: boolean
    • internalErrorCount: number
    • maxProcCount: number
    • msBeforeNextSpawn: number
    • pendingTaskCount: number
    • readyProcCount: number
    • spawnedProcCount: number
    • startErrorRatePerMinute: number
  • Run maintenance on currently spawned child processes. This method is normally invoked automatically as tasks are enqueued and processed.

    Only public for tests.

    -

    Returns Promise<void[]>

Generated using TypeDoc

\ No newline at end of file +

Returns Promise<void[]>

\ No newline at end of file diff --git a/docs/classes/BatchClusterOptions.html b/docs/classes/BatchClusterOptions.html index 249f32d..76ce7a7 100644 --- a/docs/classes/BatchClusterOptions.html +++ b/docs/classes/BatchClusterOptions.html @@ -1,60 +1,58 @@ -Codestin Search App

Class BatchClusterOptions

These parameter values have somewhat sensible defaults, but can be +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 +

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, +

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 +

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 +

logger: (() => Logger) = logger

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

-

Type declaration

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

      -

      Returns Logger

maxFailedTasksPerProcess: number = 2

How many failed tasks should a process be allowed to process before it is +

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 +

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.

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

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 +

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

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 @@ -62,21 +60,21 @@ 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 +

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 +

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.

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

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 +

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 @@ -90,8 +88,8 @@

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 +

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.

-

Generated using TypeDoc

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

Class BatchProcess

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

-

Constructors

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 +

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

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

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

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

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)

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

    -

Generated using TypeDoc

\ No newline at end of file +
  • 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 index 3285054..b6db11d 100644 --- a/docs/classes/Deferred.html +++ b/docs/classes/Deferred.html @@ -1,21 +1,21 @@ -Codestin Search App

Class Deferred<T>

Enables a Promise to be resolved or rejected at a future time, outside of +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

Generated using TypeDoc

\ No newline at end of file +

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 index 322023f..e45df63 100644 --- a/docs/classes/Rate.html +++ b/docs/classes/Rate.html @@ -1,20 +1,20 @@ -Codestin Search App

Constructors

  • Parameters

    • periodMs: number = minuteMs

      the length of time to retain event timestamps for computing +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 +

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

    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 +

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

Generated using TypeDoc

\ No newline at end of file +

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 index 768e4d9..b8df79d 100644 --- a/docs/classes/Task.html +++ b/docs/classes/Task.html @@ -1,27 +1,27 @@ -Codestin Search App

Class Task<T>

Tasks embody individual jobs given to the underlying child processes. Each +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 +

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 +

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

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 +

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

Generated using TypeDoc

\ No newline at end of file +
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 index b5ccf2d..0d6d433 100644 --- a/docs/functions/SimpleParser.html +++ b/docs/functions/SimpleParser.html @@ -1,9 +1,9 @@ -Codestin Search App

Function SimpleParser

\ No newline at end of file diff --git a/docs/functions/kill.html b/docs/functions/kill.html index 85e458e..e7b104b 100644 --- a/docs/functions/kill.html +++ b/docs/functions/kill.html @@ -1,5 +1,5 @@ -Codestin Search App

Function kill

\ No newline at end of file diff --git a/docs/functions/logger-1.html b/docs/functions/logger-1.html index 32a1063..d352b6c 100644 --- a/docs/functions/logger-1.html +++ b/docs/functions/logger-1.html @@ -1 +1 @@ -Codestin Search App

Generated using TypeDoc

\ No newline at end of file +Codestin Search App

Function logger

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

Function pidExists

\ No newline at end of file diff --git a/docs/functions/pids.html b/docs/functions/pids.html index 422fa89..fde27f4 100644 --- a/docs/functions/pids.html +++ b/docs/functions/pids.html @@ -1,3 +1,3 @@ -Codestin Search App

Function pids

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

Generated using TypeDoc

\ No newline at end of file +Codestin Search App

Function setLogger

\ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 4b9b5f7..37aae5f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,4 +1,4 @@ -Codestin Search App

batch-cluster

batch-cluster

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

+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 @@ -66,4 +66,4 @@ processes are cleaned up.

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

See issue #13 for details.

-

Generated using TypeDoc

\ No newline at end of file +
\ No newline at end of file diff --git a/docs/interfaces/BatchClusterEvents.html b/docs/interfaces/BatchClusterEvents.html index 9db732a..5f923aa 100644 --- a/docs/interfaces/BatchClusterEvents.html +++ b/docs/interfaces/BatchClusterEvents.html @@ -1,56 +1,37 @@ -Codestin Search App

Interface BatchClusterEvents

This interface describes the BatchCluster's event names as fields. The type +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
    • Emitted when this instance is in the process of ending.

      -

      Returns void

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

Emitted when a child process has ended

-

Type declaration

    • (childProcess, reason): void
    • Emitted when a child process has ended

      -

      Parameters

      Returns void

childStart: ((childProcess) => void)

Emitted when a child process has started

-

Type declaration

    • (childProcess): void
    • Emitted when a child process has started

      -

      Parameters

      Returns void

end: (() => void)

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

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
    • Emitted when this instance has ended. No child processes should remain at -this point.

      -

      Returns void

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

Emitted when a child process has an error during shutdown

-

Type declaration

    • (error, proc?): void
    • Emitted when a child process has an error during shutdown

      -

      Parameters

      Returns void

fatalError: ((error) => void)

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

-

Type declaration

    • (error): void
    • Emitted when .end() is called because the error rate has exceeded +

      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

-

Parameters

  • error: Error

Returns void

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

Emitted when a process fails health checks

-

Type declaration

    • (error, proc): void
    • Emitted when a process fails health checks

      -

      Parameters

      Returns void

internalError: ((error) => void)

Emitted when an internal consistency check fails

-

Type declaration

    • (error): void
    • Emitted when an internal consistency check fails

      -

      Parameters

      • error: Error

      Returns void

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

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

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
    • Emitted when child processes write to stdout or stderr without a current -task

      -

      Parameters

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

      Returns void

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

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

-

Type declaration

Param: childProcess

will be undefined if the error is from ChildProcessFactory.processFactory

-
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
    • Emitted when tasks receive data, which may be partial chunks from the 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.

-

Parameters

Returns void

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

Emitted when a task has an error

-

Type declaration

    • (error, task, proc): void
    • Emitted when a task has an error

      -

      Parameters

      Returns void

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

Emitted when a task has been resolved

-

Type declaration

    • (task, proc): void
    • Emitted when a task has been resolved

      -

      Parameters

      Returns void

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
    • Emitted when a task times out. Note that a taskError event always succeeds these events.

      -

      Parameters

      Returns void

Generated using TypeDoc

\ No newline at end of file +

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 index d12f207..5477df2 100644 --- a/docs/interfaces/BatchProcessOptions.html +++ b/docs/interfaces/BatchProcessOptions.html @@ -1,24 +1,24 @@ -Codestin Search App

Interface BatchProcessOptions

BatchProcessOptions have no reasonable defaults, as they are specific to +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), +

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 +

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 +

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 +

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 +

versionCommand: string

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

-

Generated using TypeDoc

\ No newline at end of file +
\ No newline at end of file diff --git a/docs/interfaces/ChildProcessFactory.html b/docs/interfaces/ChildProcessFactory.html index 600cf5e..0f96d5f 100644 --- a/docs/interfaces/ChildProcessFactory.html +++ b/docs/interfaces/ChildProcessFactory.html @@ -1,15 +1,9 @@ -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 +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>
    • Expected to be a simple call to execFile. Platform-specific code is the -responsibility of this thunk. Error handlers will be registered as -appropriate.

      -

      If this function throws an error or rejects the promise after you've -spawned a child process, the child process may continue to run and leak -system resources.

      -

      Returns ChildProcess | Promise<ChildProcess>

Generated using TypeDoc

\ No newline at end of file +

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 index 83d2624..ab9bdff 100644 --- a/docs/interfaces/Logger.html +++ b/docs/interfaces/Logger.html @@ -1,7 +1,7 @@ -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

Generated using TypeDoc

\ No newline at end of file +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 index 015e523..bbe81fd 100644 --- a/docs/interfaces/Parser.html +++ b/docs/interfaces/Parser.html @@ -1,12 +1,12 @@ -Codestin Search App

Interface Parser<T>

Parser implementations convert stdout and stderr from the underlying child +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 +

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.

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

    -

Generated using TypeDoc

\ No newline at end of file +
\ No newline at end of file diff --git a/docs/interfaces/TypedEventEmitter.html b/docs/interfaces/TypedEventEmitter.html index 88cc5fe..2e42e84 100644 --- a/docs/interfaces/TypedEventEmitter.html +++ b/docs/interfaces/TypedEventEmitter.html @@ -1,7 +1,7 @@ -Codestin Search App

Interface TypedEventEmitter<T>

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

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

Generated using TypeDoc

\ No newline at end of file +Codestin Search App

Interface TypedEventEmitter<T>

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

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 index cbef775..2ce90ef 100644 --- a/docs/modules.html +++ b/docs/modules.html @@ -1,27 +1,27 @@ -Codestin Search App

Generated using TypeDoc

\ No newline at end of file +Codestin Search App
\ No newline at end of file diff --git a/docs/types/BatchClusterEmitter.html b/docs/types/BatchClusterEmitter.html index 94fc57e..2b61157 100644 --- a/docs/types/BatchClusterEmitter.html +++ b/docs/types/BatchClusterEmitter.html @@ -1,4 +1,4 @@ -Codestin Search App

Type alias BatchClusterEmitter

The BatchClusterEmitter signature is built up automatically by the +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.

@@ -15,4 +15,4 @@

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

-

Generated using TypeDoc

\ No newline at end of file +
\ No newline at end of file diff --git a/docs/types/ChildExitReason.html b/docs/types/ChildExitReason.html index a02b14f..a935ca7 100644 --- a/docs/types/ChildExitReason.html +++ b/docs/types/ChildExitReason.html @@ -1 +1 @@ -Codestin Search App

Generated using TypeDoc

\ No newline at end of file +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 index 74974d7..2ae8808 100644 --- a/docs/types/WhyNotHealthy.html +++ b/docs/types/WhyNotHealthy.html @@ -1 +1 @@ -Codestin Search App

Type alias WhyNotHealthy

WhyNotHealthy: "broken" | "closed" | "ending" | "ended" | "idle" | "old" | "proc.close" | "proc.disconnect" | "proc.error" | "proc.exit" | "stderr.error" | "stderr" | "stdin.error" | "stdout.error" | "timeout" | "tooMany" | "startError" | "unhealthy" | "worn"

Generated using TypeDoc

\ No newline at end of file +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 index d2fa796..1098b47 100644 --- a/docs/types/WhyNotReady.html +++ b/docs/types/WhyNotReady.html @@ -1 +1 @@ -Codestin Search App

Generated using TypeDoc

\ No newline at end of file +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 index df33ed4..347efea 100644 --- a/docs/variables/ConsoleLogger.html +++ b/docs/variables/ConsoleLogger.html @@ -1,4 +1,4 @@ -Codestin Search App

Variable ConsoleLoggerConst

ConsoleLogger: Logger = ...

Default Logger implementation.

+Codestin Search App

Generated using TypeDoc

\ No newline at end of file +
\ No newline at end of file diff --git a/docs/variables/Log.html b/docs/variables/Log.html index f563e71..cd1f379 100644 --- a/docs/variables/Log.html +++ b/docs/variables/Log.html @@ -1 +1 @@ -Codestin Search App

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

Generated using TypeDoc

\ No newline at end of file +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 index 974e82f..627276d 100644 --- a/docs/variables/LogLevels.html +++ b/docs/variables/LogLevels.html @@ -1 +1 @@ -Codestin Search App

Generated using TypeDoc

\ No newline at end of file +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 index df78e77..5c8c0c6 100644 --- a/docs/variables/NoLogger.html +++ b/docs/variables/NoLogger.html @@ -1,2 +1,2 @@ -Codestin Search App

Generated using TypeDoc

\ No newline at end of file +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 new file mode 100644 index 0000000..3ed8913 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,74 @@ +// eslint.config.mjs +import eslint from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import importPlugin from "eslint-plugin-import"; + +export default tseslint.config( + { + ignores: ["dist/", "node_modules/", "**/*.d.ts", "coverage/", "docs/"], + }, + { + files: ["src/**/*.ts"], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: "./tsconfig.json", + ecmaVersion: "latest", + sourceType: "module", + }, + globals: globals.node, + }, + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylistic, + { + files: ["src/**/*.ts"], + ignores: ["src/**/*.spec.ts", "src/test.ts"], // Exclude test files from strict rules + plugins: { + import: importPlugin, + }, + rules: { + // Project-specific preferences that differ from defaults + "eqeqeq": ["error", "always", { null: "ignore" }], // Allow == null for defensive coding + "@typescript-eslint/no-unnecessary-condition": "off", // We want defensive null checks + "@typescript-eslint/prefer-optional-chain": "off", // Prefer explicit null checks for clarity + + // Import rules + "import/no-cycle": "error", // TypeScript can't catch circular imports + + // Stricter than defaults + "no-console": "error", + }, + }, + { + files: ["src/**/*.spec.ts", "src/test.ts", "src/_chai.spec.ts"], + plugins: { + import: importPlugin, + }, + rules: { + // Relax rules that are problematic for test files + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/switch-exhaustiveness-check": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/await-thenable": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "no-console": "off", + "@typescript-eslint/no-var-requires": "off", + + // Re-enable one valuable rule that's safe for tests + "import/no-cycle": "error", // Circular imports are bad even in tests + }, + }, +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..242cc94 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5853 @@ +{ + "name": "batch-cluster", + "version": "14.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "batch-cluster", + "version": "14.0.0", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.27.0", + "@sinonjs/fake-timers": "^14.0.0", + "@types/chai": "^4.3.11", + "@types/chai-as-promised": "^7", + "@types/chai-string": "^1.4.5", + "@types/chai-subset": "^1.3.6", + "@types/mocha": "^10.0.10", + "@types/node": "^22.15.21", + "@types/sinonjs__fake-timers": "^8.1.5", + "chai": "^4.3.10", + "chai-as-promised": "^7.1.2", + "chai-string": "^1.6.0", + "chai-subset": "^1.6.0", + "chai-withintoleranceof": "^1.0.1", + "eslint": "^9.27.0", + "eslint-plugin-import": "^2.31.0", + "globals": "^16.2.0", + "mocha": "^11.5.0", + "npm-check-updates": "^18.0.1", + "prettier": "^3.5.3", + "prettier-plugin-organize-imports": "^4.1.0", + "rimraf": "^5.0.10", + "seedrandom": "^3.0.5", + "serve": "^14.2.4", + "source-map-support": "^0.5.21", + "split2": "^4.2.0", + "ts-node": "^10.9.2", + "typedoc": "^0.28.5", + "typescript": "~5.8.3", + "typescript-eslint": "^8.32.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.2.tgz", + "integrity": "sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.4.2", + "@shikijs/langs": "^3.4.2", + "@shikijs/themes": "^3.4.2", + "@shikijs/types": "^3.4.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz", + "integrity": "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.2.tgz", + "integrity": "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.2.tgz", + "integrity": "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2" + } + }, + "node_modules/@shikijs/types": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.2.tgz", + "integrity": "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-14.0.0.tgz", + "integrity": "sha512-QfoXRaUTjMVVn/ZbnD4LS3TPtqOkOdKIYCKldIVPnuClcwRKat6LI2mRZ2s5qiBfO6Fy03An35dSls/2/FEc0Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/chai-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/chai-string/-/chai-string-1.4.5.tgz", + "integrity": "sha512-IecXRMSnpUvRnTztdpSdjcmcW7EdNme65bfDCQMi7XrSEPGmyDYYTEfc5fcactWDA6ioSm8o7NUqg9QxjBCCEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/chai-subset": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/chai": "<5.2.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "node_modules/chai-string": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.6.0.tgz", + "integrity": "sha512-sXV7whDmpax+8H++YaZelgin7aur1LGf9ZhjZa3ojETFJ0uPVuS4XEXuIagpZ/c8uVOtsSh4MwOjy5CBLjJSXA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "chai": "^4.1.2" + } + }, + "node_modules/chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-withintoleranceof": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chai-withintoleranceof/-/chai-withintoleranceof-1.0.1.tgz", + "integrity": "sha512-KxXzpcb/jWgBPNEVbOGbN4I4ChooIw0oTsxWDWN6EO/ZMivj+lkvm8ME4+vNVsSnjJGyWljj8CI3jS13NclYIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.10", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", + "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.5.0.tgz", + "integrity": "sha512-VKDjhy6LMTKm0WgNEdlY77YVsD49LZnPSXJAaPNL9NRYQADxvORsyG1DIQY6v53BKTnlNbEE2MbVCDbnxr4K3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-check-updates": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz", + "integrity": "sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "ncu": "build/cli.js", + "npm-check-updates": "build/cli.js" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0", + "npm": ">=8.12.1" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz", + "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", + "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.7.4", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.6", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/serve/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedoc": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz", + "integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.2.2", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.7.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 4820f67..1dc5ed3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "batch-cluster", - "version": "13.0.0", + "version": "14.0.0", "description": "Manage a cluster of child processes", "main": "dist/BatchCluster.js", "homepage": "https://photostructure.github.io/batch-cluster.js/", @@ -13,22 +13,22 @@ "url": "https://github.com/photostructure/batch-cluster.js.git" }, "engines": { - "node": ">=14" + "node": ">=20" }, "scripts": { - "ci": "yarn install --frozen-lockfile", + "ci": "npm ci", "clean": "rimraf dist", - "prettier": "prettier --write src/*.ts", - "lint": "yarn eslint src --ext .ts", + "fmt": "prettier --write src/*.ts", + "lint": "eslint src", "compile": "tsc", "watch": "rimraf dist & tsc --watch", - "pretest": "yarn clean && yarn lint && yarn compile", + "pretest": "npm run clean && npm run lint && npm run compile", "test": "mocha dist/**/*.spec.js", "docs:1": "typedoc --options .typedoc.js", "docs:2": "cp .serve.json docs/serve.json", "docs:3": "touch docs/.nojekyll", - "docs:4": "yarn serve docs", - "docs": "bash -c 'for i in {1..4} ; do yarn docs:$i ; done'" + "docs:4": "serve docs", + "docs": "bash -c 'for i in {1..4} ; do npm run docs:$i ; done'" }, "release-it": { "src": { @@ -38,8 +38,8 @@ }, "hooks": { "before:init": [ - "yarn install", - "yarn lint" + "npm install", + "npm run lint" ] }, "github": { @@ -49,33 +49,35 @@ "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.27.0", + "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", - "@types/chai-as-promised": "^7.1.8", + "@types/chai-as-promised": "^7", "@types/chai-string": "^1.4.5", - "@types/chai-subset": "^1.3.5", - "@types/mocha": "^10.0.6", - "@types/node": "^20.11.17", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@types/chai-subset": "^1.3.6", + "@types/mocha": "^10.0.10", + "@types/node": "^22.15.21", + "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", - "chai-as-promised": "^7.1.1", - "chai-string": "^1.5.0", + "chai-as-promised": "^7.1.2", + "chai-string": "^1.6.0", "chai-subset": "^1.6.0", "chai-withintoleranceof": "^1.0.1", - "eslint": "^8.56.0", - "eslint-plugin-import": "^2.29.1", - "mocha": "^10.3.0", - "npm-check-updates": "^16.14.14", - "prettier": "^3.2.5", - "prettier-plugin-organize-imports": "^3.2.4", - "rimraf": "^5.0.5", - "release-it": "^17.0.3", + "eslint": "^9.27.0", + "eslint-plugin-import": "^2.31.0", + "globals": "^16.2.0", + "mocha": "^11.5.0", + "npm-check-updates": "^18.0.1", + "prettier": "^3.5.3", + "prettier-plugin-organize-imports": "^4.1.0", + "rimraf": "^5.0.10", "seedrandom": "^3.0.5", - "serve": "^14.2.1", + "serve": "^14.2.4", "source-map-support": "^0.5.21", "split2": "^4.2.0", - "timekeeper": "^2.3.1", - "typedoc": "^0.25.7", - "typescript": "~5.3.3" + "ts-node": "^10.9.2", + "typedoc": "^0.28.5", + "typescript": "~5.8.3", + "typescript-eslint": "^8.32.1" } } diff --git a/src/Array.ts b/src/Array.ts index 527061d..99d1bf0 100644 --- a/src/Array.ts +++ b/src/Array.ts @@ -7,7 +7,6 @@ export function filterInPlace(arr: T[], filter: (t: T) => boolean): T[] { let j = 0 // PERF: for-loop to avoid the additional closure from a forEach for (let i = 0; i < len; i++) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const ea = arr[i]! if (filter(ea)) { if (i !== j) arr[j] = ea @@ -24,7 +23,6 @@ export function count( ): number { let acc = 0 for (let idx = 0; idx < arr.length; idx++) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (predicate(arr[idx]!, idx)) acc++ } return acc diff --git a/src/Async.ts b/src/Async.ts index fd507cf..44e63b0 100644 --- a/src/Async.ts +++ b/src/Async.ts @@ -28,23 +28,3 @@ export async function until( } return false } - -/** - * @return a thunk that will call the underlying thunk at most every `minDelayMs` - * milliseconds. The thunk will accept a boolean, that, when set, will force the - * underlying thunk to be called (mostly useful for tests) - */ -export function ratelimit( - f: () => T, - minDelayMs: number, -): () => T | undefined { - let next = 0 - return (force?: boolean) => { - if (Date.now() > next || force === true) { - next = Date.now() + minDelayMs - return f() - } else { - return - } - } -} diff --git a/src/BatchCluster.procps.spec.ts b/src/BatchCluster.procps.spec.ts new file mode 100644 index 0000000..281b251 --- /dev/null +++ b/src/BatchCluster.procps.spec.ts @@ -0,0 +1,21 @@ +import { expect } from "chai" +import { describe, it } from "mocha" +import { BatchCluster, ProcpsMissingError } from "./BatchCluster" +import { DefaultTestOptions } from "./DefaultTestOptions.spec" +import { processFactory } from "./_chai.spec" + +describe("BatchCluster procps validation", () => { + it("should validate procps availability during construction", () => { + // This test verifies that BatchCluster calls validateProcpsAvailable() + // On systems where procps is available (like our test environment), + // construction should succeed + expect(() => { + new BatchCluster({ ...DefaultTestOptions, processFactory }) + }).to.not.throw() + }) + + it("should export ProcpsMissingError for user handling", () => { + expect(ProcpsMissingError).to.be.a("function") + expect(new ProcpsMissingError().name).to.equal("ProcpsMissingError") + }) +}) diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index d3662f9..0f09333 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -1,14 +1,5 @@ +import FakeTimers from "@sinonjs/fake-timers" import process from "node:process" -import { filterInPlace } from "./Array" -import { delay, until } from "./Async" -import { BatchCluster } from "./BatchCluster" -import { secondMs } from "./BatchClusterOptions" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { map, omit, orElse } from "./Object" -import { isWin } from "./Platform" -import { toS } from "./String" -import { Task } from "./Task" -import { thenOrTimeout } from "./Timeout" import { currentTestPids, expect, @@ -24,9 +15,18 @@ import { times, unhandledRejections, } from "./_chai.spec" +import { filterInPlace } from "./Array" +import { delay, until } from "./Async" +import { BatchCluster } from "./BatchCluster" +import { secondMs } from "./BatchClusterOptions" +import { DefaultTestOptions } from "./DefaultTestOptions.spec" +import { map, omit } from "./Object" +import { isWin } from "./Platform" +import { toS } from "./String" +import { Task } from "./Task" +import { thenOrTimeout } from "./Timeout" const isCI = process.env.CI === "1" -const tk = require("timekeeper") function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { const common = a.filter((ea) => b.includes(ea)) @@ -88,7 +88,7 @@ describe("BatchCluster", function () { results.forEach((result, index) => { if (!result.startsWith(ErrorPrefix)) { expect(result).to.eql("ABC " + index) - expect(dataResults).to.include(result) + expect(dataResults.toString()).to.include(result) } }) } @@ -209,7 +209,7 @@ describe("BatchCluster", function () { bc.on("taskResolved", (task: Task) => { const runtimeMs = task.runtimeMs expect(runtimeMs).to.not.eql(undefined) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + events.runtimeMs.push(runtimeMs!) }) @@ -232,7 +232,7 @@ describe("BatchCluster", function () { newlines.push("crlf") } - it("supports .off()", async () => { + it("supports .off()", () => { const emitTimes: number[] = [] const bc = new BatchCluster({ ...DefaultTestOptions, processFactory }) const listener = () => emitTimes.push(Date.now()) @@ -444,7 +444,7 @@ describe("BatchCluster", function () { expect(bc.pids()).to.not.include.members(pids) expect(bc.meanTasksPerProc).to.be.within( - 0.25, // because flaky + 0.15, // because flaky (macOS on GHA resulted in 0.21) opts.maxTasksPerProcess, ) expect(bc.pids().length).to.be.lte(maxProcs) @@ -477,13 +477,30 @@ describe("BatchCluster", function () { times(maxProcs * 2, () => bc .enqueueTask(new Task("nonsense", parser)) - .catch((err) => err), + .catch((err: unknown) => err), ), ) - filterInPlace( - errorResults, - (ea) => ea != null && !String(ea).includes("EUNLUCKY"), - ) + function convertErrorToString(ea: unknown): string { + if (ea == null) return "[unknown]" + if (ea instanceof Error) return ea.message + if (typeof ea === "string") return ea + if (typeof ea === "object") { + try { + return JSON.stringify(ea) + } catch { + return "[object Object]" + } + } + if (typeof ea === "number" || typeof ea === "boolean") { + return String(ea) + } + return "[unknown]" + } + + filterInPlace(errorResults, (ea) => { + const errorStr = convertErrorToString(ea) + return !errorStr.includes("EUNLUCKY") + }) if ( maxProcs === 1 && ignoreExit === false && @@ -634,7 +651,9 @@ describe("BatchCluster", function () { const task = new Task("sleep " + sleepTimeMs, parser) const resultP = bc.enqueueTask(task) expect(bc.isIdle).to.eql(false) - const result = JSON.parse(await resultP) + const result = JSON.parse(await resultP) as { + pid: number + } & Record const end = Date.now() return { i, start, end, ...result } }), @@ -642,7 +661,7 @@ describe("BatchCluster", function () { const pid2count = new Map() tasks.forEach((ea) => { const pid = ea.pid - const count = orElse(pid2count.get(pid), 0) + const count = pid2count.get(pid) ?? 0 pid2count.set(pid, count + 1) }) expect(bc.isIdle).to.eql(true) @@ -727,7 +746,7 @@ describe("BatchCluster", function () { function stats() { // we don't want msBeforeNextSpawn because it'll be wiggly and we're not // freezing time (here) - return omit(bc.stats(), "msBeforeNextSpawn") + return omit(bc.stats(), "msBeforeNextSpawn") as Record } it("shut down rejects long-running pending tasks", async () => { @@ -769,7 +788,7 @@ describe("BatchCluster", function () { ended: false, }) - t.catch((err) => (caught = err)) + t.catch((err: unknown) => (caught = err)) await delay(2) expect(stats()).to.eql({ @@ -785,7 +804,7 @@ describe("BatchCluster", function () { ended: false, }) - let caught: any + let caught: unknown expect(bc.isIdle).to.eql(false) await bc.end(false) // not graceful just to shut down faster @@ -803,7 +822,9 @@ describe("BatchCluster", function () { }) expect(bc.isIdle).to.eql(true) - expect(caught?.message).to.include("end() called before task completed") + expect((caught as Error)?.message).to.include( + "Process terminated before task completed", + ) expect(unhandledRejections).to.eql([]) }) }) @@ -901,11 +922,20 @@ describe("BatchCluster", function () { describe("maxProcAgeMillis (recycling procs)", () => { let bc: BatchCluster + let clock: FakeTimers.InstalledClock + + beforeEach(() => { + clock = FakeTimers.install({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }) + }) afterEach(() => { - tk.reset() + clock.uninstall() return shutdown(bc) }) + for (const { maxProcAgeMillis, ctx, exp } of [ { maxProcAgeMillis: 0, @@ -929,8 +959,6 @@ describe("BatchCluster", function () { it("(" + maxProcAgeMillis + "): " + ctx, async function () { // TODO: look into why this fails in CI on windows if (isWin && isCI) return this.skip() - const start = Date.now() - tk.freeze(start) setFailratePct(0) bc = listen( @@ -944,7 +972,7 @@ describe("BatchCluster", function () { ) assertExpectedResults(await Promise.all(runTasks(bc, 2))) const pidsBefore = bc.pids() - tk.freeze(start + 7000) + clock.tick(7000) assertExpectedResults(await Promise.all(runTasks(bc, 2))) const pidsAfter = bc.pids() console.dir({ maxProcAgeMillis, pidsBefore, pidsAfter }) diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index 306e6a0..50705fc 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -1,31 +1,27 @@ -import child_process from "node:child_process" import events from "node:events" import process from "node:process" import timers from "node:timers" -import { count, filterInPlace } from "./Array" import { BatchClusterEmitter, BatchClusterEvents, ChildEndReason, TypedEventEmitter, } from "./BatchClusterEmitter" -import { - AllOpts, - BatchClusterOptions, - verifyOptions, -} from "./BatchClusterOptions" -import { BatchProcess, WhyNotHealthy, WhyNotReady } from "./BatchProcess" +import { BatchClusterOptions } from "./BatchClusterOptions" +import type { BatchClusterStats } from "./BatchClusterStats" import { BatchProcessOptions } from "./BatchProcessOptions" +import type { ChildProcessFactory } from "./ChildProcessFactory" +import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" import { Deferred } from "./Deferred" -import { asError } from "./Error" import { Logger } from "./Logger" -import { Mean } from "./Mean" -import { fromEntries, map } from "./Object" +import { verifyOptions } from "./OptionsVerifier" import { Parser } from "./Parser" -import { Rate } from "./Rate" -import { toS } from "./String" +import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator" +import { ProcessPoolManager } from "./ProcessPoolManager" +import { validateProcpsAvailable } from "./ProcpsChecker" import { Task } from "./Task" -import { Timeout, thenOrTimeout } from "./Timeout" +import { TaskQueueManager } from "./TaskQueueManager" +import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" export { BatchClusterOptions } from "./BatchClusterOptions" export { BatchProcess } from "./BatchProcess" @@ -33,37 +29,22 @@ export { Deferred } from "./Deferred" export * from "./Logger" export { SimpleParser } from "./Parser" export { kill, pidExists, pids } from "./Pids" +export { ProcpsMissingError } from "./ProcpsChecker" export { Rate } from "./Rate" export { Task } from "./Task" export type { BatchClusterEmitter, BatchClusterEvents, + BatchClusterStats, BatchProcessOptions, ChildEndReason as ChildExitReason, + ChildProcessFactory, Parser, TypedEventEmitter, WhyNotHealthy, WhyNotReady, } -/** - * These are required parameters for a given BatchCluster. - */ -export interface ChildProcessFactory { - /** - * Expected to be a simple call to execFile. Platform-specific code is the - * responsibility of this thunk. Error handlers will be registered as - * appropriate. - * - * If this function throws an error or rejects the promise _after_ you've - * spawned a child process, **the child process may continue to run** and leak - * system resources. - */ - readonly processFactory: () => - | child_process.ChildProcess - | Promise -} - /** * BatchCluster instances manage 0 or more homogeneous child processes, and * provide the main interface for enqueuing `Task`s via `enqueueTask`. @@ -75,20 +56,14 @@ export interface ChildProcessFactory { * child tasks can be verified and shut down. */ export class BatchCluster { - readonly #tasksPerProc = new Mean() readonly #logger: () => Logger - readonly options: AllOpts - readonly #procs: BatchProcess[] = [] + readonly options: CombinedBatchProcessOptions + readonly #processPool: ProcessPoolManager + readonly #taskQueue: TaskQueueManager + readonly #eventCoordinator: BatchClusterEventCoordinator #onIdleRequested = false - #nextSpawnTime = 0 - #lastPidsCheckTime = 0 - readonly #tasks: Task[] = [] #onIdleInterval: NodeJS.Timeout | undefined - readonly #startErrorRate = new Rate() - #spawnedProcs = 0 #endPromise?: Deferred - #internalErrorCount = 0 - readonly #childEndCounts = new Map() readonly emitter = new events.EventEmitter() as BatchClusterEmitter constructor( @@ -96,54 +71,29 @@ export class BatchCluster { BatchProcessOptions & ChildProcessFactory, ) { + // Validate that required process listing commands are available + validateProcpsAvailable() + this.options = verifyOptions({ ...opts, observer: this.emitter }) + this.#logger = this.options.logger - this.on("childEnd", (bp, why) => { - this.#tasksPerProc.push(bp.taskCount) - this.#childEndCounts.set(why, (this.#childEndCounts.get(why) ?? 0) + 1) - this.#onIdleLater() - }) - - this.on("internalError", (error) => { - this.#logger().error("BatchCluster: INTERNAL ERROR: " + error) - this.#internalErrorCount++ - }) - - this.on("noTaskData", (stdout, stderr, proc) => { - this.#logger().warn( - "BatchCluster: child process emitted data with no current task. Consider setting streamFlushMillis to a higher value.", - { - streamFlushMillis: this.options.streamFlushMillis, - stdout: toS(stdout), - stderr: toS(stderr), - proc_pid: proc?.pid, - }, - ) - this.#internalErrorCount++ - }) - - this.on("startError", (error) => { - this.#logger().warn("BatchCluster.onStartError(): " + error) - this.#startErrorRate.onEvent() - if ( - this.options.maxReasonableProcessFailuresPerMinute > 0 && - this.#startErrorRate.eventsPerMinute > - this.options.maxReasonableProcessFailuresPerMinute - ) { - this.emitter.emit( - "fatalError", - new Error( - error + - "(start errors/min: " + - this.#startErrorRate.eventsPerMinute.toFixed(2) + - ")", - ), - ) - this.end() - } else { - this.#onIdleLater() - } - }) + // Initialize the managers + this.#processPool = new ProcessPoolManager(this.options, this.emitter, () => + this.#onIdleLater(), + ) + this.#taskQueue = new TaskQueueManager(this.#logger, this.emitter) + + // Initialize event coordinator to handle all event processing + this.#eventCoordinator = new BatchClusterEventCoordinator( + this.emitter, + { + streamFlushMillis: this.options.streamFlushMillis, + maxReasonableProcessFailuresPerMinute: this.options.maxReasonableProcessFailuresPerMinute, + logger: this.#logger, + }, + () => this.#onIdleLater(), + () => void this.end(), + ) if (this.options.onIdleIntervalMillis > 0) { this.#onIdleInterval = timers.setInterval( @@ -169,8 +119,12 @@ export class BatchCluster { */ readonly off = this.emitter.off.bind(this.emitter) - readonly #beforeExitListener = () => this.end(true) - readonly #exitListener = () => this.end(false) + readonly #beforeExitListener = () => { + void this.end(true) + } + readonly #exitListener = () => { + void this.end(false) + } get ended(): boolean { return this.#endPromise != null @@ -187,7 +141,8 @@ export class BatchCluster { if (this.#endPromise == null) { this.emitter.emit("beforeEnd") - map(this.#onIdleInterval, timers.clearInterval) + if (this.#onIdleInterval != null) + timers.clearInterval(this.#onIdleInterval) this.#onIdleInterval = undefined process.removeListener("beforeExit", this.#beforeExitListener) process.removeListener("exit", this.#exitListener) @@ -213,7 +168,7 @@ export class BatchCluster { new Error("BatchCluster has ended, cannot enqueue " + task.command), ) } - this.#tasks.push(task) + this.#taskQueue.enqueue(task as Task) // Run #onIdle now (not later), to make sure the task gets enqueued asap if // possible @@ -236,70 +191,60 @@ export class BatchCluster { * @return the number of pending tasks */ get pendingTaskCount(): number { - return this.#tasks.length + return this.#taskQueue.pendingTaskCount } /** * @returns {number} the mean number of tasks completed by child processes */ get meanTasksPerProc(): number { - return this.#tasksPerProc.mean + return this.#eventCoordinator.meanTasksPerProc } /** * @return the total number of child processes created by this instance */ get spawnedProcCount(): number { - return this.#spawnedProcs + return this.#processPool.spawnedProcCount } /** * @return the current number of spawned child processes. Some (or all) may be idle. */ get procCount(): number { - return this.#procs.length + return this.#processPool.processCount } /** * @return the current number of child processes currently servicing tasks */ get busyProcCount(): number { - return count( - this.#procs, - // don't count procs that are starting up as "busy": - (ea) => !ea.starting && !ea.ending && !ea.idle, - ) + return this.#processPool.busyProcCount } get startingProcCount(): number { - return count( - this.#procs, - // don't count procs that are starting up as "busy": - (ea) => ea.starting && !ea.ending, - ) + return this.#processPool.startingProcCount } /** * @return the current pending Tasks (mostly for testing) */ get pendingTasks() { - return this.#tasks + return this.#taskQueue.pendingTasks } /** * @return the current running Tasks (mostly for testing) */ get currentTasks(): Task[] { - return this.#procs - .map((ea) => ea.currentTask) - .filter((ea) => ea != null) as Task[] + return this.#processPool.currentTasks() } /** * For integration tests: */ get internalErrorCount(): number { - return this.#internalErrorCount + return this.#eventCoordinator.internalErrorCount } /** @@ -308,28 +253,21 @@ export class BatchCluster { * @return the spawned PIDs that are still in the process table. */ pids(): number[] { - const arr: number[] = [] - for (const proc of [...this.#procs]) { - if (proc != null && proc.running()) { - arr.push(proc.pid) - } - } - return arr + return this.#processPool.pids() } /** * For diagnostics. Contents may change. */ - stats() { - const readyProcCount = count(this.#procs, (ea) => ea.ready) + stats(): BatchClusterStats { return { - pendingTaskCount: this.#tasks.length, - currentProcCount: this.#procs.length, - readyProcCount, + pendingTaskCount: this.pendingTaskCount, + currentProcCount: this.procCount, + readyProcCount: this.#processPool.readyProcCount, maxProcCount: this.options.maxProcs, - internalErrorCount: this.#internalErrorCount, - startErrorRatePerMinute: this.#startErrorRate.eventsPerMinute, - msBeforeNextSpawn: Math.max(0, this.#nextSpawnTime - Date.now()), + internalErrorCount: this.#eventCoordinator.internalErrorCount, + startErrorRatePerMinute: this.#eventCoordinator.startErrorRatePerMinute, + msBeforeNextSpawn: this.#processPool.msBeforeNextSpawn, spawnedProcCount: this.spawnedProcCount, childEndCounts: this.childEndCounts, ending: this.#endPromise != null, @@ -341,27 +279,19 @@ export class BatchCluster { * Get ended process counts (used for tests) */ countEndedChildProcs(why: ChildEndReason): number { - return this.#childEndCounts.get(why) ?? 0 + return this.#eventCoordinator.countEndedChildProcs(why) } - get childEndCounts(): { [key in NonNullable]: number } { - return fromEntries([...this.#childEndCounts.entries()]) + get childEndCounts(): Record, number> { + return this.#eventCoordinator.childEndCounts } /** * Shut down any currently-running child processes. New child processes will * be started automatically to handle new tasks. */ - async closeChildProcesses(gracefully = true) { - const procs = [...this.#procs] - this.#procs.length = 0 - await Promise.all( - procs.map((proc) => - proc - .end(gracefully, "ending") - .catch((err) => this.emitter.emit("endError", asError(err), proc)), - ), - ) + async closeChildProcesses(gracefully = true): Promise { + return this.#processPool.closeChildProcesses(gracefully) } /** @@ -370,7 +300,7 @@ export class BatchCluster { * completed. */ setMaxProcs(maxProcs: number) { - this.options.maxProcs = maxProcs + this.#processPool.setMaxProcs(maxProcs) // we may now be able to handle an enqueued task. Vacuum pids and see: this.#onIdleLater() } @@ -385,22 +315,11 @@ export class BatchCluster { // NOT ASYNC: updates internal state: #onIdle() { this.#onIdleRequested = false - this.vacuumProcs() + void this.vacuumProcs() while (this.#execNextTask()) { // } - this.#maybeSpawnProcs() - } - - #maybeCheckPids() { - if ( - this.options.cleanupChildProcs && - this.options.pidCheckIntervalMillis > 0 && - this.#lastPidsCheckTime + this.options.pidCheckIntervalMillis < Date.now() - ) { - this.#lastPidsCheckTime = Date.now() - void this.pids() - } + void this.#maybeSpawnProcs() } /** @@ -411,28 +330,7 @@ export class BatchCluster { */ // NOT ASYNC: updates internal state. only exported for tests. vacuumProcs() { - this.#maybeCheckPids() - const endPromises: Promise[] = [] - let pidsToReap = Math.max(0, this.#procs.length - this.options.maxProcs) - filterInPlace(this.#procs, (proc) => { - // Only check `.idle` (not `.ready`) procs. We don't want to reap busy - // procs unless we're ending, and unhealthy procs (that we want to reap) - // won't be `.ready`. - if (proc.idle) { - // don't reap more than pidsToReap pids. We can't use #procs.length - // within filterInPlace because #procs.length only changes at iteration - // completion: the prior impl resulted in all idle pids getting reaped - // when maxProcs was reduced. - const why = proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null) - if (why != null) { - endPromises.push(proc.end(true, why)) - return false - } - proc.maybeRunHealthcheck() - } - return true - }) - return Promise.all(endPromises) + return this.#processPool.vacuumProcs() } /** @@ -440,133 +338,15 @@ export class BatchCluster { * @return true iff a task was submitted to a child process */ #execNextTask(retries = 1): boolean { - if (this.#tasks.length === 0 || this.ended || retries < 0) return false - const readyProc = this.#procs.find((ea) => ea.ready) - // no procs are idle and healthy :( - if (readyProc == null) { - return false - } - - const task = this.#tasks.shift() - if (task == null) { - this.emitter.emit("internalError", new Error("unexpected null task")) - return false - } - - const submitted = readyProc.execTask(task) - if (!submitted) { - // This isn't an internal error: the proc may have needed to run a health - // check. Let's reschedule the task and try again: - this.#tasks.push(task) - // We don't want to return false here (it'll stop the onIdle loop) unless - // we actually can't submit the task: - return this.#execNextTask(retries--) - } - this.#logger().trace("BatchCluster.#execNextTask(): submitted task", { - child_pid: readyProc.pid, - task, - }) - - return submitted - } - - #maxSpawnDelay() { - // 10s delay is certainly long enough for .spawn() to return, even on a - // loaded windows machine. - return Math.max(10_000, this.options.spawnTimeoutMillis) - } - - #procsToSpawn() { - const remainingCapacity = this.options.maxProcs - this.#procs.length - - // take into account starting procs, so one task doesn't result in multiple - // processes being spawned: - const requestedCapacity = this.#tasks.length - this.startingProcCount - - const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity)) - - return this.options.minDelayBetweenSpawnMillis === 0 - ? // we can spin up multiple processes in parallel. - atLeast0 - : // Don't spin up more than 1: - Math.min(1, atLeast0) + if (this.ended) return false + const readyProc = this.#processPool.findReadyProcess() + return this.#taskQueue.tryAssignNextTask(readyProc, retries) } async #maybeSpawnProcs() { - let procsToSpawn = this.#procsToSpawn() - - if (this.ended || this.#nextSpawnTime > Date.now() || procsToSpawn === 0) { - return - } - - // prevent concurrent runs: - this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() - - for (let i = 0; i < procsToSpawn; i++) { - if (this.ended) { - break - } - - // Kick the lock down the road: - this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() - this.#spawnedProcs++ - - try { - const proc = this.#spawnNewProc() - const result = await thenOrTimeout( - proc, - this.options.spawnTimeoutMillis, - ) - if (result === Timeout) { - void proc - .then((bp) => { - void bp.end(false, "startError") - this.emitter.emit( - "startError", - asError( - "Failed to spawn process in " + - this.options.spawnTimeoutMillis + - "ms", - ), - bp, - ) - }) - .catch((err) => { - // this should only happen if the processFactory throws a - // rejection: - this.emitter.emit("startError", asError(err)) - }) - } else { - this.#logger().debug( - "BatchCluster.#maybeSpawnProcs() started healthy child process", - { pid: result.pid }, - ) - } - - // tasks may have been popped off or setMaxProcs may have reduced - // maxProcs. Do this at the end so the for loop ends properly. - procsToSpawn = Math.min(this.#procsToSpawn(), procsToSpawn) - } catch (err) { - this.emitter.emit("startError", asError(err)) - } - } - - // YAY WE MADE IT. - // Only let more children get spawned after minDelay: - const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis) - this.#nextSpawnTime = Date.now() + delay - - // And schedule #onIdle for that time: - timers.setTimeout(this.#onIdleLater, delay).unref() - } - - // must only be called by this.#maybeSpawnProcs() - async #spawnNewProc() { - // no matter how long it takes to spawn, always push the result into #procs - // so we don't leak child processes: - const proc = await this.options.processFactory() - const result = new BatchProcess(proc, this.options, this.#onIdleLater) - this.#procs.push(result) - return result + return this.#processPool.maybeSpawnProcs( + this.#taskQueue.pendingTaskCount, + this.ended, + ) } } diff --git a/src/BatchClusterEmitter.ts b/src/BatchClusterEmitter.ts index ec186ee..b19668f 100644 --- a/src/BatchClusterEmitter.ts +++ b/src/BatchClusterEmitter.ts @@ -1,5 +1,6 @@ -import { BatchProcess, WhyNotHealthy } from "./BatchProcess" +import { BatchProcess } from "./BatchProcess" import { Task } from "./Task" +import { WhyNotHealthy } from "./WhyNotHealthy" type Args = T extends (...args: infer A) => void ? A : never @@ -23,7 +24,7 @@ export interface TypedEventEmitter { ): this emit(eventName: E, ...args: Args): boolean - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type listeners(event: E): Function[] removeAllListeners(eventName?: keyof T): this @@ -70,24 +71,28 @@ export interface BatchClusterEvents { */ taskData: ( data: Buffer | string, - task: Task | undefined, + task: Task | undefined, proc: BatchProcess, ) => void /** * Emitted when a task has been resolved */ - taskResolved: (task: Task, proc: BatchProcess) => void + taskResolved: (task: Task, proc: BatchProcess) => void /** * Emitted when a task times out. Note that a `taskError` event always succeeds these events. */ - taskTimeout: (timeoutMs: number, task: Task, proc: BatchProcess) => void + taskTimeout: ( + timeoutMs: number, + task: Task, + proc: BatchProcess, + ) => void /** * Emitted when a task has an error */ - taskError: (error: Error, task: Task, proc: BatchProcess) => void + taskError: (error: Error, task: Task, proc: BatchProcess) => void /** * Emitted when child processes write to stdout or stderr without a current diff --git a/src/BatchClusterEventCoordinator.spec.ts b/src/BatchClusterEventCoordinator.spec.ts new file mode 100644 index 0000000..07bac7d --- /dev/null +++ b/src/BatchClusterEventCoordinator.spec.ts @@ -0,0 +1,361 @@ +import events from "node:events" +import { expect } from "./_chai.spec" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { + BatchClusterEventCoordinator, + EventCoordinatorOptions, +} from "./BatchClusterEventCoordinator" +import { BatchProcess } from "./BatchProcess" +import { logger } from "./Logger" +import { Task } from "./Task" + +describe("BatchClusterEventCoordinator", function () { + let eventCoordinator: BatchClusterEventCoordinator + let emitter: BatchClusterEmitter + let onIdleCalledCount = 0 + let endClusterCalledCount = 0 + + const options: EventCoordinatorOptions = { + streamFlushMillis: 100, + maxReasonableProcessFailuresPerMinute: 5, + logger, + } + + const onIdleLater = () => { + onIdleCalledCount++ + } + + const endCluster = () => { + endClusterCalledCount++ + } + + beforeEach(function () { + emitter = new events.EventEmitter() as BatchClusterEmitter + eventCoordinator = new BatchClusterEventCoordinator( + emitter, + options, + onIdleLater, + endCluster, + ) + onIdleCalledCount = 0 + endClusterCalledCount = 0 + }) + + describe("initial state", function () { + it("should start with clean statistics", function () { + expect(eventCoordinator.meanTasksPerProc).to.eql(0) + expect(eventCoordinator.internalErrorCount).to.eql(0) + expect(eventCoordinator.startErrorRatePerMinute).to.eql(0) + expect(eventCoordinator.countEndedChildProcs("ended")).to.eql(0) + expect(eventCoordinator.childEndCounts).to.eql({}) + }) + + it("should provide clean event statistics", function () { + const stats = eventCoordinator.getEventStats() + expect(stats.meanTasksPerProc).to.eql(0) + expect(stats.internalErrorCount).to.eql(0) + expect(stats.startErrorRatePerMinute).to.eql(0) + expect(stats.totalChildEndEvents).to.eql(0) + expect(stats.childEndReasons).to.eql([]) + }) + }) + + describe("childEnd event handling", function () { + it("should handle childEnd events and update statistics", function () { + const mockProcess = { + taskCount: 5, + pid: 12345, + } as BatchProcess + + // Emit childEnd event + emitter.emit("childEnd", mockProcess, "worn") + + expect(eventCoordinator.meanTasksPerProc).to.eql(5) + expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(1) + expect(eventCoordinator.childEndCounts.worn).to.eql(1) + expect(onIdleCalledCount).to.eql(1) + }) + + it("should track multiple childEnd events", function () { + const mockProcess1 = { taskCount: 3 } as BatchProcess + const mockProcess2 = { taskCount: 7 } as BatchProcess + const mockProcess3 = { taskCount: 5 } as BatchProcess + + emitter.emit("childEnd", mockProcess1, "worn") + emitter.emit("childEnd", mockProcess2, "old") + emitter.emit("childEnd", mockProcess3, "worn") + + expect(eventCoordinator.meanTasksPerProc).to.eql(5) // (3+7+5)/3 + expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2) + expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1) + expect(eventCoordinator.childEndCounts.worn).to.eql(2) + expect(eventCoordinator.childEndCounts.old).to.eql(1) + expect(onIdleCalledCount).to.eql(3) + }) + }) + + describe("internalError event handling", function () { + it("should handle internalError events and increment counter", function () { + const error = new Error("Internal error occurred") + + emitter.emit("internalError", error) + + expect(eventCoordinator.internalErrorCount).to.eql(1) + }) + + it("should handle multiple internalError events", function () { + emitter.emit("internalError", new Error("Error 1")) + emitter.emit("internalError", new Error("Error 2")) + emitter.emit("internalError", new Error("Error 3")) + + expect(eventCoordinator.internalErrorCount).to.eql(3) + }) + }) + + describe("noTaskData event handling", function () { + it("should handle noTaskData events and increment internal error count", function () { + const mockProcess = { pid: 12345 } as BatchProcess + + emitter.emit("noTaskData", "some stdout", "some stderr", mockProcess) + + expect(eventCoordinator.internalErrorCount).to.eql(1) + }) + + it("should handle noTaskData with null data", function () { + const mockProcess = { pid: 12345 } as BatchProcess + + emitter.emit("noTaskData", null, null, mockProcess) + + expect(eventCoordinator.internalErrorCount).to.eql(1) + }) + + it("should handle noTaskData with buffer data", function () { + const mockProcess = { pid: 12345 } as BatchProcess + const bufferData = Buffer.from("test data") + + emitter.emit("noTaskData", bufferData, null, mockProcess) + + expect(eventCoordinator.internalErrorCount).to.eql(1) + }) + }) + + describe("startError event handling", function () { + it("should handle startError events without triggering fatal error", function () { + const error = new Error("Start error") + + emitter.emit("startError", error) + + // Rate might be 0 initially due to warmup period + expect(eventCoordinator.startErrorRatePerMinute).to.be.greaterThanOrEqual( + 0, + ) + expect(endClusterCalledCount).to.eql(0) + expect(onIdleCalledCount).to.eql(1) + }) + + it("should have logic to trigger fatal error based on rate", function () { + // This test verifies the logic exists, but doesn't test timing-dependent rate calculation + // which depends on the Rate class's warmup period + + const testOptions: EventCoordinatorOptions = { + ...options, + maxReasonableProcessFailuresPerMinute: 5, + } + + const testCoordinator = new BatchClusterEventCoordinator( + emitter, + testOptions, + onIdleLater, + endCluster, + ) + + // Verify that start error rate tracking is working + emitter.emit("startError", new Error("Test error")) + expect(testCoordinator.startErrorRatePerMinute).to.be.greaterThanOrEqual( + 0, + ) + + // The actual fatal error triggering depends on Rate class timing + // which is tested in the Rate class's own tests + }) + + it("should not trigger fatal error when rate limit is disabled", function () { + const noLimitOptions: EventCoordinatorOptions = { + ...options, + maxReasonableProcessFailuresPerMinute: 0, // Disabled + } + + new BatchClusterEventCoordinator( + emitter, + noLimitOptions, + onIdleLater, + endCluster, + ) + + let fatalErrorEmitted = false + emitter.on("fatalError", () => { + fatalErrorEmitted = true + }) + + // Emit many start errors + for (let i = 0; i < 20; i++) { + emitter.emit("startError", new Error(`Start error ${i}`)) + } + + expect(fatalErrorEmitted).to.be.false + expect(endClusterCalledCount).to.eql(0) + }) + }) + + describe("event access", function () { + it("should provide access to the underlying emitter", function () { + expect(eventCoordinator.events).to.equal(emitter) + }) + + it("should allow direct event emission through events property", function () { + let eventReceived = false + let receivedData: any + + emitter.on("taskData", (data, task, proc) => { + eventReceived = true + receivedData = { data, task, proc } + }) + + const mockTask = {} as Task + const mockProcess = {} as BatchProcess + const testData = "test data" + + const result = eventCoordinator.events.emit( + "taskData", + testData, + mockTask, + mockProcess, + ) + + expect(result).to.be.true + expect(eventReceived).to.be.true + expect(receivedData.data).to.eql(testData) + expect(receivedData.task).to.eql(mockTask) + expect(receivedData.proc).to.eql(mockProcess) + }) + + it("should allow direct event listener management through events property", function () { + let eventReceived = false + + const listener = () => { + eventReceived = true + } + + eventCoordinator.events.on("beforeEnd", listener) + emitter.emit("beforeEnd") + expect(eventReceived).to.be.true + + eventReceived = false + eventCoordinator.events.off("beforeEnd", listener) + emitter.emit("beforeEnd") + expect(eventReceived).to.be.false + }) + }) + + describe("statistics and monitoring", function () { + beforeEach(function () { + // Set up some test data + const mockProcess1 = { taskCount: 10 } as BatchProcess + const mockProcess2 = { taskCount: 20 } as BatchProcess + + emitter.emit("childEnd", mockProcess1, "worn") + emitter.emit("childEnd", mockProcess2, "old") + emitter.emit("internalError", new Error("Test error")) + emitter.emit("startError", new Error("Start error")) + }) + + it("should provide comprehensive event statistics", function () { + const stats = eventCoordinator.getEventStats() + + expect(stats.meanTasksPerProc).to.eql(15) // (10+20)/2 + expect(stats.internalErrorCount).to.eql(1) + expect(stats.startErrorRatePerMinute).to.be.greaterThanOrEqual(0) // Rate might be 0 due to warmup + expect(stats.totalChildEndEvents).to.eql(2) + expect(stats.childEndReasons).to.include("worn") + expect(stats.childEndReasons).to.include("old") + }) + + it("should reset statistics correctly", function () { + // Verify we have some data + expect(eventCoordinator.meanTasksPerProc).to.eql(15) + expect(eventCoordinator.internalErrorCount).to.eql(1) + + eventCoordinator.resetStats() + + // Verify everything is reset + expect(eventCoordinator.meanTasksPerProc).to.eql(0) + expect(eventCoordinator.internalErrorCount).to.eql(0) + expect(eventCoordinator.startErrorRatePerMinute).to.eql(0) + expect(eventCoordinator.childEndCounts).to.eql({}) + + const stats = eventCoordinator.getEventStats() + expect(stats.totalChildEndEvents).to.eql(0) + expect(stats.childEndReasons).to.eql([]) + }) + + it("should track child end counts accurately", function () { + // Add more events of different types + const mockProcess3 = { taskCount: 5 } as BatchProcess + const mockProcess4 = { taskCount: 8 } as BatchProcess + + emitter.emit("childEnd", mockProcess3, "worn") // Second worn + emitter.emit("childEnd", mockProcess4, "broken") // New type + + expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2) + expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1) + expect(eventCoordinator.countEndedChildProcs("broken")).to.eql(1) + expect(eventCoordinator.countEndedChildProcs("timeout")).to.eql(0) + + const childEndCounts = eventCoordinator.childEndCounts + expect(childEndCounts.worn).to.eql(2) + expect(childEndCounts.old).to.eql(1) + expect(childEndCounts.broken).to.eql(1) + }) + }) + + describe("callback integration", function () { + it("should call onIdleLater for appropriate events", function () { + const initialCount = onIdleCalledCount + + // Events that should trigger onIdleLater + emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn") + emitter.emit("startError", new Error("Start error")) + + expect(onIdleCalledCount).to.eql(initialCount + 2) + }) + + it("should have callback integration for endCluster", function () { + // This test verifies that the endCluster callback is properly integrated + // The actual triggering depends on Rate class timing which is complex to test + + const testCoordinator = new BatchClusterEventCoordinator( + emitter, + options, + onIdleLater, + endCluster, + ) + + // Verify the coordinator is set up and callbacks are connected + expect(testCoordinator.events).to.equal(emitter) + + // The endCluster callback integration is verified through the logic + // The actual rate-based triggering is tested in integration scenarios + }) + + it("should not call endCluster for non-fatal events", function () { + const initialCount = endClusterCalledCount + + // Events that should not trigger endCluster + emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn") + emitter.emit("internalError", new Error("Internal error")) + emitter.emit("noTaskData", "data", null, {} as BatchProcess) + + expect(endClusterCalledCount).to.eql(initialCount) + }) + }) +}) diff --git a/src/BatchClusterEventCoordinator.ts b/src/BatchClusterEventCoordinator.ts new file mode 100644 index 0000000..0e09989 --- /dev/null +++ b/src/BatchClusterEventCoordinator.ts @@ -0,0 +1,190 @@ +import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter" +import { BatchProcess } from "./BatchProcess" +import { Logger } from "./Logger" +import { Mean } from "./Mean" +import { Rate } from "./Rate" +import { toS } from "./String" + +/** + * Configuration for event handling behavior + */ +export interface EventCoordinatorOptions { + readonly streamFlushMillis: number + readonly maxReasonableProcessFailuresPerMinute: number + readonly logger: () => Logger +} + +/** + * Centralized coordinator for BatchCluster events. + * Handles event processing, statistics tracking, and automated responses to events. + */ +export class BatchClusterEventCoordinator { + readonly #logger: () => Logger + #tasksPerProc = new Mean() + #startErrorRate = new Rate() + readonly #childEndCounts = new Map() + #internalErrorCount = 0 + + constructor( + private readonly emitter: BatchClusterEmitter, + private readonly options: EventCoordinatorOptions, + private readonly onIdleLater: () => void, + private readonly endCluster: () => void, + ) { + this.#logger = options.logger + this.#setupEventHandlers() + } + + /** + * Set up all event handlers for the BatchCluster + */ + #setupEventHandlers(): void { + this.emitter.on("childEnd", (bp, why) => this.#handleChildEnd(bp, why)) + this.emitter.on("internalError", (error) => + this.#handleInternalError(error), + ) + this.emitter.on("noTaskData", (stdout, stderr, proc) => + this.#handleNoTaskData(stdout, stderr, proc), + ) + this.emitter.on("startError", (error) => this.#handleStartError(error)) + } + + /** + * Handle child process end events + */ + #handleChildEnd(process: BatchProcess, reason: ChildEndReason): void { + this.#tasksPerProc.push(process.taskCount) + this.#childEndCounts.set( + reason, + (this.#childEndCounts.get(reason) ?? 0) + 1, + ) + this.onIdleLater() + } + + /** + * Handle internal error events + */ + #handleInternalError(error: Error): void { + this.#logger().error("BatchCluster: INTERNAL ERROR: " + String(error)) + this.#internalErrorCount++ + } + + /** + * Handle no task data events (data received without current task) + */ + #handleNoTaskData( + stdout: string | Buffer | null, + stderr: string | Buffer | null, + proc: BatchProcess, + ): void { + this.#logger().warn( + "BatchCluster: child process emitted data with no current task. Consider setting streamFlushMillis to a higher value.", + { + streamFlushMillis: this.options.streamFlushMillis, + stdout: toS(stdout), + stderr: toS(stderr), + proc_pid: proc?.pid, + }, + ) + this.#internalErrorCount++ + } + + /** + * Handle start error events + */ + #handleStartError(error: Error): void { + this.#logger().warn("BatchCluster.onStartError(): " + String(error)) + this.#startErrorRate.onEvent() + + if ( + this.options.maxReasonableProcessFailuresPerMinute > 0 && + this.#startErrorRate.eventsPerMinute > + this.options.maxReasonableProcessFailuresPerMinute + ) { + this.emitter.emit( + "fatalError", + new Error( + String(error) + + "(start errors/min: " + + this.#startErrorRate.eventsPerMinute.toFixed(2) + + ")", + ), + ) + this.endCluster() + } else { + this.onIdleLater() + } + } + + /** + * Get the mean number of tasks completed by child processes + */ + get meanTasksPerProc(): number { + const mean = this.#tasksPerProc.mean + return isNaN(mean) ? 0 : mean + } + + /** + * Get internal error count + */ + get internalErrorCount(): number { + return this.#internalErrorCount + } + + /** + * Get start error rate per minute + */ + get startErrorRatePerMinute(): number { + return this.#startErrorRate.eventsPerMinute + } + + /** + * Get count of ended child processes by reason + */ + countEndedChildProcs(reason: ChildEndReason): number { + return this.#childEndCounts.get(reason) ?? 0 + } + + /** + * Get all child end counts + */ + get childEndCounts(): Record, number> { + return Object.fromEntries([...this.#childEndCounts.entries()]) as Record< + NonNullable, + number + > + } + + /** + * Get event statistics for monitoring + */ + getEventStats() { + return { + meanTasksPerProc: this.meanTasksPerProc, + internalErrorCount: this.internalErrorCount, + startErrorRatePerMinute: this.startErrorRatePerMinute, + totalChildEndEvents: [...this.#childEndCounts.values()].reduce( + (sum, count) => sum + count, + 0, + ), + childEndReasons: Object.keys(this.childEndCounts), + } + } + + /** + * Reset event statistics (useful for testing) + */ + resetStats(): void { + this.#tasksPerProc = new Mean() + this.#startErrorRate = new Rate() + this.#childEndCounts.clear() + this.#internalErrorCount = 0 + } + + /** + * Get the underlying emitter for direct event access + */ + get events(): BatchClusterEmitter { + return this.emitter + } +} diff --git a/src/BatchClusterOptions.spec.ts b/src/BatchClusterOptions.spec.ts index dfcfc50..31f1c8f 100644 --- a/src/BatchClusterOptions.spec.ts +++ b/src/BatchClusterOptions.spec.ts @@ -1,14 +1,14 @@ import { BatchCluster } from "./BatchCluster" -import { verifyOptions } from "./BatchClusterOptions" import { DefaultTestOptions } from "./DefaultTestOptions.spec" +import { verifyOptions } from "./OptionsVerifier" import { expect, processFactory } from "./_chai.spec" describe("BatchClusterOptions", () => { let bc: BatchCluster afterEach(() => bc?.end(false)) describe("verifyOptions()", () => { - function errToArr(err: any) { - return err.toString().split(/\s*[:;]\s*/) + function errToArr(err: unknown): string[] { + return String(err).split(/\s*[:;]\s*/) } it("allows 0 maxProcAgeMillis", () => { diff --git a/src/BatchClusterOptions.ts b/src/BatchClusterOptions.ts index c13ca6e..64f9710 100644 --- a/src/BatchClusterOptions.ts +++ b/src/BatchClusterOptions.ts @@ -1,10 +1,6 @@ -import { ChildProcessFactory } from "./BatchCluster" import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { BatchProcessOptions } from "./BatchProcessOptions" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" import { logger, Logger } from "./Logger" import { isMac, isWin } from "./Platform" -import { blank, toS } from "./String" export const secondMs = 1000 export const minuteMs = 60 * secondMs @@ -181,83 +177,3 @@ export class BatchClusterOptions { export interface WithObserver { observer: BatchClusterEmitter } - -export type AllOpts = BatchClusterOptions & - InternalBatchProcessOptions & - ChildProcessFactory & - WithObserver - -function escapeRegExp(s: string) { - return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&") -} - -function toRe(s: string | RegExp) { - return s instanceof RegExp - ? s - : new RegExp("(?:\\n|^)" + escapeRegExp(s) + "(?:\\r?\\n|$)") -} - -export function verifyOptions( - opts: Partial & - BatchProcessOptions & - ChildProcessFactory & - WithObserver, -): AllOpts { - const result = { - ...new BatchClusterOptions(), - ...opts, - passRE: toRe(opts.pass), - failRE: toRe(opts.fail), - } - - const errors: string[] = [] - - function notBlank(fieldName: keyof AllOpts) { - const v = toS(result[fieldName]) - if (blank(v)) { - errors.push(fieldName + " must not be blank") - } - } - - function gte(fieldName: keyof AllOpts, value: number, why?: string) { - const v = result[fieldName] as number - if (v < value) { - const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}` - errors.push(msg) - } - } - - notBlank("versionCommand") - notBlank("pass") - notBlank("fail") - - gte("maxTasksPerProcess", 1) - - gte("maxProcs", 1) - - if ( - opts.maxProcAgeMillis != null && - opts.maxProcAgeMillis > 0 && - result.taskTimeoutMillis - ) { - gte( - "maxProcAgeMillis", - Math.max(result.spawnTimeoutMillis, result.taskTimeoutMillis), - `the max value of spawnTimeoutMillis (${result.spawnTimeoutMillis}) and taskTimeoutMillis (${result.taskTimeoutMillis})`, - ) - } - // 0 disables: - gte("minDelayBetweenSpawnMillis", 0) - gte("onIdleIntervalMillis", 0) - gte("endGracefulWaitTimeMillis", 0) - gte("maxReasonableProcessFailuresPerMinute", 0) - gte("streamFlushMillis", 0) - - if (errors.length > 0) { - throw new Error( - "BatchCluster was given invalid options: " + errors.join("; "), - ) - } - - return result -} diff --git a/src/BatchClusterStats.ts b/src/BatchClusterStats.ts new file mode 100644 index 0000000..ea9adf6 --- /dev/null +++ b/src/BatchClusterStats.ts @@ -0,0 +1,16 @@ +import { ChildEndReason } from "./BatchClusterEmitter" + +export interface BatchClusterStats { + pendingTaskCount: number + currentProcCount: number + readyProcCount: number + maxProcCount: number + internalErrorCount: number + startErrorRatePerMinute: number + msBeforeNextSpawn: number + spawnedProcCount: number + childEndCounts: Record, number> + ending: boolean + ended: boolean + [key: string]: unknown +} diff --git a/src/BatchProcess.ts b/src/BatchProcess.ts index a6d7eb0..390761b 100644 --- a/src/BatchProcess.ts +++ b/src/BatchProcess.ts @@ -1,40 +1,18 @@ import child_process from "node:child_process" import timers from "node:timers" -import { until } from "./Async" import { Deferred } from "./Deferred" import { cleanError } from "./Error" import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" import { Logger } from "./Logger" import { map } from "./Object" import { SimpleParser } from "./Parser" -import { kill, pidExists } from "./Pids" -import { destroy } from "./Stream" -import { blank, ensureSuffix } from "./String" +import { pidExists } from "./Pids" +import { ProcessHealthMonitor } from "./ProcessHealthMonitor" +import { ProcessTerminator } from "./ProcessTerminator" +import { StreamContext, StreamHandler } from "./StreamHandler" +import { ensureSuffix } from "./String" import { Task } from "./Task" -import { thenOrTimeout } from "./Timeout" - -export type WhyNotHealthy = - | "broken" - | "closed" - | "ending" - | "ended" - | "idle" - | "old" - | "proc.close" - | "proc.disconnect" - | "proc.error" - | "proc.exit" - | "stderr.error" - | "stderr" - | "stdin.error" - | "stdout.error" - | "timeout" - | "tooMany" // < only sent by BatchCluster when maxProcs is reduced - | "startError" - | "unhealthy" - | "worn" - -export type WhyNotReady = WhyNotHealthy | "busy" +import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" /** * BatchProcess manages the care and feeding of a single child process. @@ -43,13 +21,13 @@ export class BatchProcess { readonly name: string readonly pid: number readonly start = Date.now() - #lastHealthCheck = Date.now() - #healthCheckFailures = 0 readonly startupTaskId: number readonly #logger: () => Logger + readonly #terminator: ProcessTerminator + readonly #healthMonitor: ProcessHealthMonitor + readonly #streamHandler: StreamHandler #lastJobFinshedAt = Date.now() - #lastJobFailed = false // Only set to true when `proc.pid` is no longer in the process table. #starting = true @@ -66,7 +44,29 @@ export class BatchProcess { /** * Should be undefined if this instance is not currently processing a task. */ - #currentTask: Task | undefined + #currentTask: Task | undefined + + /** + * Getter for current task (required by StreamContext interface) + */ + get currentTask(): Task | undefined { + return this.#currentTask + } + + /** + * Create a StreamContext adapter for this BatchProcess + */ + #createStreamContext = (): StreamContext => { + return { + name: this.name, + isEnding: () => this.ending, + getCurrentTask: () => this.#currentTask, + onError: (reason: string, error: Error) => + this.#onError(reason as WhyNotHealthy, error), + end: (gracefully: boolean, reason: string) => + void this.end(gracefully, reason as WhyNotHealthy), + } + } #currentTaskTimeout: NodeJS.Timeout | undefined #endPromise: undefined | Deferred @@ -79,9 +79,17 @@ export class BatchProcess { readonly proc: child_process.ChildProcess, readonly opts: InternalBatchProcessOptions, private readonly onIdle: () => void, + healthMonitor?: ProcessHealthMonitor, ) { this.name = "BatchProcess(" + proc.pid + ")" this.#logger = opts.logger + this.#terminator = new ProcessTerminator(opts) + this.#healthMonitor = + healthMonitor ?? new ProcessHealthMonitor(opts, opts.observer) + this.#streamHandler = new StreamHandler( + { logger: this.#logger }, + opts.observer, + ) // don't let node count the child processes as a reason to stay alive this.proc.unref() @@ -92,23 +100,21 @@ export class BatchProcess { this.pid = proc.pid this.proc.on("error", (err) => this.#onError("proc.error", err)) - this.proc.on("close", () => this.end(false, "proc.close")) - this.proc.on("exit", () => this.end(false, "proc.exit")) - this.proc.on("disconnect", () => this.end(false, "proc.disconnect")) - - const stdin = this.proc.stdin - if (stdin == null) throw new Error("Given proc had no stdin") - stdin.on("error", (err) => this.#onError("stdin.error", err)) - - const stdout = this.proc.stdout - if (stdout == null) throw new Error("Given proc had no stdout") - stdout.on("error", (err) => this.#onError("stdout.error", err)) - stdout.on("data", (d) => this.#onStdout(d)) - - map(this.proc.stderr, (stderr) => { - stderr.on("error", (err) => this.#onError("stderr.error", err)) - stderr.on("data", (err) => this.#onStderr(err)) + this.proc.on("close", () => { + void this.end(false, "proc.close") + }) + this.proc.on("exit", () => { + void this.end(false, "proc.exit") }) + this.proc.on("disconnect", () => { + void this.end(false, "proc.disconnect") + }) + + // Set up stream handlers using StreamHandler + this.#streamHandler.setupStreamListeners( + this.proc, + this.#createStreamContext(), + ) const startupTask = new Task(opts.versionCommand, SimpleParser) this.startupTaskId = startupTask.taskId @@ -119,15 +125,15 @@ export class BatchProcess { new Error(this.name + " startup task was not submitted"), ) } + + // Initialize health monitoring for this process + this.#healthMonitor.initializeProcess(this.pid) + // this needs to be at the end of the constructor, to ensure everything is // set up on `this` this.opts.observer.emit("childStart", this) } - get currentTask(): Task | undefined { - return this.#currentTask - } - get taskCount(): number { return this.#taskCount } @@ -171,43 +177,7 @@ export class BatchProcess { * know if a process can handle a new task. */ get whyNotHealthy(): WhyNotHealthy | null { - if (this.#whyNotHealthy != null) return this.#whyNotHealthy - if (this.ended) { - return "ended" - } else if (this.ending) { - return "ending" - } else if (this.#healthCheckFailures > 0) { - return "unhealthy" - } else if (this.proc.stdin == null || this.proc.stdin.destroyed) { - return "closed" - } else if ( - this.opts.maxTasksPerProcess > 0 && - this.taskCount >= this.opts.maxTasksPerProcess - ) { - return "worn" - } else if ( - this.opts.maxIdleMsPerProcess > 0 && - this.idleMs > this.opts.maxIdleMsPerProcess - ) { - return "idle" - } else if ( - this.opts.maxFailedTasksPerProcess > 0 && - this.failedTaskCount >= this.opts.maxFailedTasksPerProcess - ) { - return "broken" - } else if ( - this.opts.maxProcAgeMillis > 0 && - this.start + this.opts.maxProcAgeMillis < Date.now() - ) { - return "old" - } else if ( - (this.opts.taskTimeoutMillis > 0 && this.#currentTask?.runtimeMs) ?? - 0 > this.opts.taskTimeoutMillis - ) { - return "timeout" - } else { - return null - } + return this.#healthMonitor.assessHealth(this, this.#whyNotHealthy) } /** @@ -255,7 +225,7 @@ export class BatchProcess { if (!alive) { this.#exited = true // once a PID leaves the process table, it's gone for good. - this.end(false, "proc.exit") + void this.end(false, "proc.exit") } return alive } @@ -264,48 +234,21 @@ export class BatchProcess { return !this.running() } - maybeRunHealthcheck(): Task | undefined { - const hcc = this.opts.healthCheckCommand - // if there's no health check command, no-op. - if (hcc == null || blank(hcc)) return - - // if the prior health check failed, .ready will be false - if (!this.ready) return - - if ( - this.#lastJobFailed || - (this.opts.healthCheckIntervalMillis > 0 && - Date.now() - this.#lastHealthCheck > - this.opts.healthCheckIntervalMillis) - ) { - this.#lastHealthCheck = Date.now() - const t = new Task(hcc, SimpleParser) - t.promise - .catch((err) => { - this.opts.observer.emit("healthCheckError", err, this) - this.#healthCheckFailures++ - // BatchCluster will see we're unhealthy and reap us later - }) - .finally(() => { - this.#lastHealthCheck = Date.now() - }) - this.#execTask(t) - return t - } - return + maybeRunHealthcheck(): Task | undefined { + return this.#healthMonitor.maybeRunHealthcheck(this) } // This must not be async, or new instances aren't started as busy (until the // startup task is complete) - execTask(task: Task): boolean { + execTask(task: Task): boolean { return this.ready ? this.#execTask(task) : false } - #execTask(task: Task): boolean { + #execTask(task: Task): boolean { if (this.ending) return false this.#taskCount++ - this.#currentTask = task + this.#currentTask = task as Task const cmd = ensureSuffix(task.command, "\n") const isStartupTask = task.taskId === this.startupTaskId const taskTimeoutMs = isStartupTask @@ -315,7 +258,7 @@ export class BatchProcess { // add the stream flush millis to the taskTimeoutMs, because that time // should not be counted against the task. this.#currentTaskTimeout = timers.setTimeout( - () => this.#onTimeout(task, taskTimeoutMs), + () => this.#onTimeout(task as Task, taskTimeoutMs), taskTimeoutMs + this.opts.streamFlushMillis, ) } @@ -323,27 +266,35 @@ export class BatchProcess { // rejections: void task.promise.then( () => { - this.#clearCurrentTask(task) + this.#clearCurrentTask(task as Task) // this.#logger().trace("task completed", { task }) if (isStartupTask) { // no need to emit taskResolved for startup tasks. this.#starting = false } else { - this.opts.observer.emit("taskResolved", task, this) + this.opts.observer.emit("taskResolved", task as Task, this) } // Call _after_ we've cleared the current task: this.onIdle() }, (error) => { - this.#clearCurrentTask(task) + this.#clearCurrentTask(task as Task) // this.#logger().trace("task failed", { task, err: error }) if (isStartupTask) { - this.opts.observer.emit("startError", error) - this.end(false, "startError") + this.opts.observer.emit( + "startError", + error instanceof Error ? error : new Error(String(error)), + ) + void this.end(false, "startError") } else { - this.opts.observer.emit("taskError", error, task, this) + this.opts.observer.emit( + "taskError", + error instanceof Error ? error : new Error(String(error)), + task as Task, + this, + ) } // Call _after_ we've cleared the current task: @@ -365,9 +316,9 @@ export class BatchProcess { }) return true } - } catch (err) { + } catch { // child process went away. We should too. - this.end(false, "stdin.error") + void this.end(false, "stdin.error") return false } } @@ -395,106 +346,30 @@ export class BatchProcess { const lastTask = this.#currentTask this.#clearCurrentTask() - // NOTE: We wait on all tasks (even startup tasks) so we can assert that - // BatchCluster is idle (and this proc is idle) when the end promise is - // resolved. - - // NOTE: holy crap there are a lot of notes here. - - // We don't need to wait for the startup task to complete, and we certainly - // don't need to fuss about ending when we're just getting started. - if (lastTask != null && lastTask.taskId !== this.startupTaskId) { - try { - // Let's wait for the process to complete and the streams to flush, as - // that may actually allow the task to complete successfully. Let's not - // wait forever, though. - await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250) - } catch { - // - } - if (lastTask.pending) { - lastTask.reject( - new Error( - `end() called before task completed (${JSON.stringify({ - gracefully, - lastTask, - })})`, - ), - ) - } - } - - // Ignore EPIPE on .end(): if the process immediately ends after the exit - // command, we'll get an EPIPE, so, shush error events *before* we tell the - // child process to exit. See https://github.com/nodejs/node/issues/26828 - for (const ea of [ + await this.#terminator.terminate( this.proc, - this.proc.stdin, - this.proc.stdout, - this.proc.stderr, - ]) { - ea?.removeAllListeners("error") - } - - if (true === this.proc.stdin?.writable) { - const exitCmd = - this.opts.exitCommand == null - ? null - : ensureSuffix(this.opts.exitCommand, "\n") - try { - this.proc.stdin?.end(exitCmd) - } catch { - // don't care - } - } + this.name, + lastTask, + this.startupTaskId, + gracefully, + this.#exited, + () => this.running(), + ) - // None of this *should* be necessary, but we're trying to be as hygienic as - // we can to avoid process zombification. - destroy(this.proc.stdin) - destroy(this.proc.stdout) - destroy(this.proc.stderr) - - if ( - this.opts.cleanupChildProcs && - gracefully && - this.opts.endGracefulWaitTimeMillis > 0 && - !this.#exited - ) { - // Wait for the exit command to take effect: - await this.#awaitNotRunning(this.opts.endGracefulWaitTimeMillis / 2) - // If it's still running, send the pid a signal: - if (this.running() && this.proc.pid != null) this.proc.kill() - // Wait for the signal handler to work: - await this.#awaitNotRunning(this.opts.endGracefulWaitTimeMillis / 2) - } + // Clean up health monitoring for this process + this.#healthMonitor.cleanupProcess(this.pid) - if ( - this.opts.cleanupChildProcs && - this.proc.pid != null && - this.running() - ) { - this.#logger().warn( - this.name + ".end(): force-killing still-running child.", - ) - kill(this.proc.pid, true) - } - // disconnect may not be a function on proc! - this.proc.disconnect?.() this.opts.observer.emit("childEnd", this, reason) } - #awaitNotRunning(timeout: number) { - return until(() => this.notRunning(), timeout) - } - - #onTimeout(task: Task, timeoutMs: number): void { + #onTimeout(task: Task, timeoutMs: number): void { if (task.pending) { this.opts.observer.emit("taskTimeout", timeoutMs, task, this) this.#onError("timeout", new Error("waited " + timeoutMs + "ms"), task) } } - #onError(reason: WhyNotHealthy, error: Error, task?: Task) { + #onError(reason: WhyNotHealthy, error: Error, task?: Task) { if (task == null) { task = this.#currentTask } @@ -521,7 +396,7 @@ export class BatchProcess { if (task != null && this.taskCount === 1) { this.#logger().warn( - this.name + ".onError(): startup task failed: " + cleanedError, + this.name + ".onError(): startup task failed: " + String(cleanedError), ) this.opts.observer.emit("startError", cleanedError) } @@ -540,35 +415,14 @@ export class BatchProcess { } } - #onStderr(data: string | Buffer) { - if (blank(data)) return - this.#logger().warn(this.name + ".onStderr(): " + data) - const task = this.#currentTask - if (task != null && task.pending) { - task.onStderr(data) - } else if (!this.ending) { - // If we're ending and there isn't a task, don't worry about it. - this.opts.observer.emit("noTaskData", null, data, this) - void this.end(false, "stderr") + #clearCurrentTask(task?: Task) { + const taskFailed = task?.state === "rejected" + if (taskFailed) { + this.#healthMonitor.recordJobFailure(this.pid) + } else if (task != null) { + this.#healthMonitor.recordJobSuccess(this.pid) } - } - - #onStdout(data: string | Buffer) { - if (data == null) return - const task = this.#currentTask - if (task != null && task.pending) { - this.opts.observer.emit("taskData", data, task, this) - task.onStdout(data) - } else if (this.ending) { - // don't care if we're already being shut down. - } else if (!blank(data)) { - this.opts.observer.emit("noTaskData", data, null, this) - void this.end(false, "stdout.error") - } - } - #clearCurrentTask(task?: Task) { - this.#lastJobFailed = task?.state === "rejected" if (task != null && task.taskId !== this.#currentTask?.taskId) return map(this.#currentTaskTimeout, (ea) => clearTimeout(ea)) this.#currentTaskTimeout = undefined diff --git a/src/ChildProcessFactory.ts b/src/ChildProcessFactory.ts new file mode 100644 index 0000000..fa03034 --- /dev/null +++ b/src/ChildProcessFactory.ts @@ -0,0 +1,20 @@ +import child_process from "node:child_process" + +/** + * These are required parameters for a given BatchCluster. + */ + +export interface ChildProcessFactory { + /** + * Expected to be a simple call to execFile. Platform-specific code is the + * responsibility of this thunk. Error handlers will be registered as + * appropriate. + * + * If this function throws an error or rejects the promise _after_ you've + * spawned a child process, **the child process may continue to run** and leak + * system resources. + */ + readonly processFactory: () => + | child_process.ChildProcess + | Promise +} diff --git a/src/CombinedBatchProcessOptions.ts b/src/CombinedBatchProcessOptions.ts new file mode 100644 index 0000000..9321e5d --- /dev/null +++ b/src/CombinedBatchProcessOptions.ts @@ -0,0 +1,8 @@ +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" +import { ChildProcessFactory } from "./ChildProcessFactory" +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" + +export type CombinedBatchProcessOptions = BatchClusterOptions & + InternalBatchProcessOptions & + ChildProcessFactory & + WithObserver diff --git a/src/Deferred.ts b/src/Deferred.ts index 5bc1b8b..796959f 100644 --- a/src/Deferred.ts +++ b/src/Deferred.ts @@ -16,7 +16,7 @@ export class Deferred implements PromiseLike { readonly [Symbol.toStringTag] = "Deferred" readonly promise: Promise #resolve!: (value: T | PromiseLike) => void - #reject!: (reason?: any) => void + #reject!: (reason?: unknown) => void #state: State = State.pending constructor() { @@ -55,23 +55,14 @@ export class Deferred implements PromiseLike { } then( - onfulfilled?: - | ((value: T) => TResult1 | PromiseLike) - | undefined - | null, - onrejected?: - | ((reason: any) => TResult2 | PromiseLike) - | undefined - | null, + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ): Promise { return this.promise.then(onfulfilled, onrejected) } catch( - onrejected?: - | ((reason: any) => TResult | PromiseLike) - | undefined - | null, + onrejected?: ((reason: unknown) => TResult | PromiseLike) | null, ): Promise { return this.promise.catch(onrejected) } @@ -107,15 +98,15 @@ export class Deferred implements PromiseLike { observeQuietly(p: Promise): Deferred { void observeQuietly(this, p) - return this as any + return this as Deferred } } async function observe(d: Deferred, p: Promise) { try { d.resolve(await p) - } catch (err: any) { - d.reject(err) + } catch (err: unknown) { + d.reject(err instanceof Error ? err : new Error(String(err))) } } @@ -123,6 +114,6 @@ async function observeQuietly(d: Deferred, p: Promise) { try { d.resolve(await p) } catch { - d.resolve(undefined as any) + d.resolve(undefined as T) } } diff --git a/src/Error.ts b/src/Error.ts index 9100aa8..853753d 100644 --- a/src/Error.ts +++ b/src/Error.ts @@ -1,4 +1,4 @@ -import { blank, toS } from "./String" +import { toNotBlank } from "./String" /** * When we wrap errors, an Error always prefixes the toString() and stack with @@ -8,20 +8,28 @@ export function tryEach(arr: (() => void)[]): void { for (const f of arr) { try { f() - } catch (_) { + } catch { // } } } -export function cleanError(s: any): string { +export function cleanError(s: unknown): string { return String(s) .trim() .replace(/^error: /i, "") } -export function asError(err: any): Error { +export function asError(err: unknown): Error { return err instanceof Error ? err - : new Error(blank(err) ? "(unknown)" : toS(err)) + : new Error( + toNotBlank( + err != null && typeof err === "object" && "message" in err + ? err?.message + : undefined, + ) ?? + toNotBlank(err) ?? + "(unknown)", + ) } diff --git a/src/HealthCheckStrategy.ts b/src/HealthCheckStrategy.ts new file mode 100644 index 0000000..eeb0b28 --- /dev/null +++ b/src/HealthCheckStrategy.ts @@ -0,0 +1,157 @@ +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { HealthCheckable } from "./ProcessHealthMonitor" +import { WhyNotHealthy } from "./WhyNotHealthy" + +/** + * Strategy interface for different health check approaches + */ +export interface HealthCheckStrategy { + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null +} + +/** + * Checks if process has ended or is ending + */ +export class LifecycleHealthCheck implements HealthCheckStrategy { + assess(process: HealthCheckable): WhyNotHealthy | null { + if (process.ended) { + return "ended" + } else if (process.ending) { + return "ending" + } + return null + } +} + +/** + * Checks if process stdin is available + */ +export class StreamHealthCheck implements HealthCheckStrategy { + assess(process: HealthCheckable): WhyNotHealthy | null { + if (process.proc.stdin == null || process.proc.stdin.destroyed) { + return "closed" + } + return null + } +} + +/** + * Checks if process has exceeded task limits + */ +export class TaskLimitHealthCheck implements HealthCheckStrategy { + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null { + if ( + options.maxTasksPerProcess > 0 && + process.taskCount >= options.maxTasksPerProcess + ) { + return "worn" + } + return null + } +} + +/** + * Checks if process has been idle too long + */ +export class IdleTimeHealthCheck implements HealthCheckStrategy { + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null { + if ( + options.maxIdleMsPerProcess > 0 && + process.idleMs > options.maxIdleMsPerProcess + ) { + return "idle" + } + return null + } +} + +/** + * Checks if process has too many failed tasks + */ +export class FailureCountHealthCheck implements HealthCheckStrategy { + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null { + if ( + options.maxFailedTasksPerProcess > 0 && + process.failedTaskCount >= options.maxFailedTasksPerProcess + ) { + return "broken" + } + return null + } +} + +/** + * Checks if process is too old + */ +export class AgeHealthCheck implements HealthCheckStrategy { + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null { + if ( + options.maxProcAgeMillis > 0 && + process.start + options.maxProcAgeMillis < Date.now() + ) { + return "old" + } + return null + } +} + +/** + * Checks if current task has timed out + */ +export class TaskTimeoutHealthCheck implements HealthCheckStrategy { + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null { + if ( + options.taskTimeoutMillis > 0 && + (process.currentTask?.runtimeMs ?? 0) > options.taskTimeoutMillis + ) { + return "timeout" + } + return null + } +} + +/** + * Composite strategy that runs all health checks in order of priority + */ +export class CompositeHealthCheckStrategy implements HealthCheckStrategy { + private readonly strategies: HealthCheckStrategy[] = [ + new LifecycleHealthCheck(), + new StreamHealthCheck(), + new TaskLimitHealthCheck(), + new IdleTimeHealthCheck(), + new FailureCountHealthCheck(), + new AgeHealthCheck(), + new TaskTimeoutHealthCheck(), + ] + + assess( + process: HealthCheckable, + options: InternalBatchProcessOptions, + ): WhyNotHealthy | null { + for (const strategy of this.strategies) { + const result = strategy.assess(process, options) + if (result != null) { + return result + } + } + return null + } +} diff --git a/src/Logger.ts b/src/Logger.ts index a91caf7..2c4bd54 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -2,7 +2,7 @@ import util from "node:util" import { map } from "./Object" import { notBlank } from "./String" -type LogFunc = (message: string, ...optionalParams: any[]) => void +type LogFunc = (message: string, ...optionalParams: unknown[]) => void /** * Simple interface for logging. @@ -56,11 +56,17 @@ export const ConsoleLogger: Logger = Object.freeze({ /** * Delegates to `console.warn` */ - warn: console.warn, + warn: (...args: unknown[]) => { + // eslint-disable-next-line no-console + console.warn(...args) + }, /** * Delegates to `console.error` */ - error: console.error, + error: (...args: unknown[]) => { + // eslint-disable-next-line no-console + console.error(...args) + }, }) /** @@ -74,11 +80,11 @@ export const NoLogger: Logger = Object.freeze({ error: noop, }) -let _logger: Logger = NoLogger +let _logger: Logger = _debuglog.enabled ? ConsoleLogger : NoLogger export function setLogger(l: Logger): void { if (LogLevels.some((ea) => typeof l[ea] !== "function")) { - throw new Error("invalid logger, must implement " + LogLevels) + throw new Error("invalid logger, must implement " + LogLevels.join(", ")) } _logger = l } @@ -89,12 +95,12 @@ export function logger(): Logger { export const Log = { withLevels: (delegate: Logger): Logger => { - const timestamped: any = {} + const timestamped: Logger = {} as Logger LogLevels.forEach((ea) => { const prefix = (ea + " ").substring(0, 5) + " | " - timestamped[ea] = (message?: any, ...optionalParams: any[]) => { - if (notBlank(message)) { - delegate[ea](prefix + message, ...optionalParams) + timestamped[ea] = (message?: unknown, ...optionalParams: unknown[]) => { + if (notBlank(String(message))) { + delegate[ea](prefix + String(message), ...optionalParams) } } }) @@ -102,13 +108,16 @@ export const Log = { }, withTimestamps: (delegate: Logger) => { - const timestamped: any = {} + const timestamped: Logger = {} as Logger LogLevels.forEach( (level) => - (timestamped[level] = (message?: any, ...optionalParams: any[]) => + (timestamped[level] = ( + message?: unknown, + ...optionalParams: unknown[] + ) => map(message, (ea) => delegate[level]( - new Date().toISOString() + " | " + ea, + new Date().toISOString() + " | " + String(ea), ...optionalParams, ), )), @@ -118,7 +127,7 @@ export const Log = { filterLevels: (l: Logger, minLogLevel: keyof Logger) => { const minLogLevelIndex = LogLevels.indexOf(minLogLevel) - const filtered: any = {} + const filtered: Logger = {} as Logger LogLevels.forEach( (ea, idx) => (filtered[ea] = idx < minLogLevelIndex ? noop : l[ea].bind(l)), diff --git a/src/Mutex.ts b/src/Mutex.ts index a572c14..accab3a 100644 --- a/src/Mutex.ts +++ b/src/Mutex.ts @@ -6,7 +6,7 @@ import { Deferred } from "./Deferred" */ export class Mutex { private _pushCount = 0 - private readonly _arr: Deferred[] = [] + private readonly _arr: Deferred[] = [] private get arr() { filterInPlace(this._arr, (ea) => ea.pending) diff --git a/src/Object.ts b/src/Object.ts index c1a2b3c..7bb4839 100644 --- a/src/Object.ts +++ b/src/Object.ts @@ -9,20 +9,14 @@ export function map( return obj != null ? f(obj) : undefined } -export function isFunction(obj: any): obj is () => any { +export function isFunction(obj: unknown): obj is () => unknown { return typeof obj === "function" } -export function orElse(obj: T | undefined, defaultValue: T | (() => T)): T { - return obj != null - ? obj - : isFunction(defaultValue) - ? defaultValue() - : defaultValue -} - -export function fromEntries(arr: [string | undefined, any][]) { - const o: any = {} +export function fromEntries( + arr: [string | undefined, unknown][], +): Record { + const o: Record = {} for (const [key, value] of arr) { if (key != null) { o[key] = value @@ -31,7 +25,7 @@ export function fromEntries(arr: [string | undefined, any][]) { return o } -export function omit, S extends keyof T>( +export function omit, S extends keyof T>( t: T, ...keysToOmit: S[] ): Omit { diff --git a/src/OptionsVerifier.ts b/src/OptionsVerifier.ts new file mode 100644 index 0000000..bd776fd --- /dev/null +++ b/src/OptionsVerifier.ts @@ -0,0 +1,102 @@ +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" +import { BatchProcessOptions } from "./BatchProcessOptions" +import { ChildProcessFactory } from "./ChildProcessFactory" +import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" +import { blank, toS } from "./String" + +/** + * Verifies and sanitizes the provided options for BatchCluster. + * + * It merges partial options with default BatchClusterOptions, + * converts pass/fail strings to RegExp, and validates various constraints. + * + * @param opts - The partial options to verify. These are merged with default + * BatchClusterOptions. + * @returns The fully verified and sanitized options. + * @throws Error if any options are invalid. + */ +export function verifyOptions( + opts: Partial & + BatchProcessOptions & + ChildProcessFactory & + WithObserver, +): CombinedBatchProcessOptions { + const result: CombinedBatchProcessOptions = { + ...new BatchClusterOptions(), + ...opts, + passRE: toRe(opts.pass), + failRE: toRe(opts.fail), + } as CombinedBatchProcessOptions + + const errors: string[] = [] + + function notBlank(fieldName: keyof CombinedBatchProcessOptions) { + const v = toS(result[fieldName]) + if (blank(v)) { + errors.push(fieldName + " must not be blank") + } + } + + function gte( + fieldName: keyof CombinedBatchProcessOptions, + value: number, + why?: string, + ) { + const v = result[fieldName] as number + if (v < value) { + const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}` + errors.push(msg) + } + } + + notBlank("versionCommand") + notBlank("pass") + notBlank("fail") + + gte("maxTasksPerProcess", 1) + + gte("maxProcs", 1) + + if ( + opts.maxProcAgeMillis != null && + opts.maxProcAgeMillis > 0 && + result.taskTimeoutMillis + ) { + gte( + "maxProcAgeMillis", + Math.max(result.spawnTimeoutMillis, result.taskTimeoutMillis), + `the max value of spawnTimeoutMillis (${result.spawnTimeoutMillis}) and taskTimeoutMillis (${result.taskTimeoutMillis})`, + ) + } + // 0 disables: + gte("minDelayBetweenSpawnMillis", 0) + gte("onIdleIntervalMillis", 0) + gte("endGracefulWaitTimeMillis", 0) + gte("maxReasonableProcessFailuresPerMinute", 0) + gte("streamFlushMillis", 0) + + if (errors.length > 0) { + throw new Error( + "BatchCluster was given invalid options: " + errors.join("; "), + ) + } + + return result +} +function escapeRegExp(s: string) { + return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&") +} +function toRe(s: string | RegExp) { + return s instanceof RegExp + ? s + : blank(s) + ? /$./ + : s.includes("*") + ? new RegExp( + s + .split("*") + .map((ea) => escapeRegExp(ea)) + .join(".*"), + ) + : new RegExp(escapeRegExp(s)) +} diff --git a/src/Parser.ts b/src/Parser.ts index 2ebbee0..8c2b011 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,24 +5,26 @@ import { notBlank } from "./String" * process to a more useable format. This can be a no-op passthrough if no * parsing is necessary. */ -export interface Parser { - /** - * Invoked once per task. - * - * @param stdout the concatenated stream from `stdin`, stripped of the `PASS` - * or `FAIL` tokens from `BatchProcessOptions`. - * - * @param stderr if defined, includes all text emitted to stderr. - * - * @param passed `true` iff the `PASS` pattern was found in stdout. - * - * @throws an error if the Parser implementation wants to reject the task. It - * is valid to raise Errors if stderr is undefined. - * - * @see BatchProcessOptions - */ - (stdout: string, stderr: string | undefined, passed: boolean): T | Promise -} +/** + * Invoked once per task. + * + * @param stdout the concatenated stream from `stdin`, stripped of the `PASS` + * or `FAIL` tokens from `BatchProcessOptions`. + * + * @param stderr if defined, includes all text emitted to stderr. + * + * @param passed `true` iff the `PASS` pattern was found in stdout. + * + * @throws an error if the Parser implementation wants to reject the task. It + * is valid to raise Errors if stderr is undefined. + * + * @see BatchProcessOptions + */ +export type Parser = ( + stdout: string, + stderr: string | undefined, + passed: boolean, +) => T | Promise export const SimpleParser: Parser = ( stdout: string, diff --git a/src/Pids.ts b/src/Pids.ts index ddf3034..5cff0ec 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -1,6 +1,7 @@ import child_process from "node:child_process" -import process from "node:process" -import { map } from "./Object" +import { existsSync } from "node:fs" +import { readdir } from "node:fs/promises" +import { asError } from "./Error" import { isWin } from "./Platform" /** @@ -14,11 +15,11 @@ export function pidExists(pid: number | undefined): boolean { // signal 0 can be used to test for the existence of a process // see https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processkillpid-signal return process.kill(pid, 0) - } catch (err: any) { + } catch (err: unknown) { // We're expecting err.code to be either "EPERM" (if we don't have // permission to send `pid` and message), or "ESRCH" if that pid doesn't // exist. EPERM means it _does_ exist! - if (err?.code === "EPERM") return true + if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true // failed to get priority--assume the pid is gone. return false @@ -44,35 +45,44 @@ export function kill(pid: number | undefined, force = false): boolean { } } -const winRe = /^".+?","(\d+)"/ -const posixRe = /^\s*(\d+)/ - /** * Only used by tests * * @returns {Promise} all the Process IDs in the process table. */ -export function pids(): Promise { - return new Promise((resolve, reject) => { - child_process.execFile( - isWin ? "tasklist" : "ps", - // NoHeader, FOrmat CSV - isWin ? ["/NH", "/FO", "CSV"] : ["-e"], - (error: Error | null, stdout: string, stderr: string) => { - if (error != null) { - reject(error) - } else if (("" + stderr).trim().length > 0) { - reject(new Error(stderr)) - } else - resolve( - ("" + stdout) - .trim() - .split(/[\n\r]+/) - .map((ea) => ea.match(isWin ? winRe : posixRe)) - .map((m) => map(m?.[0], parseInt)) - .filter((ea) => ea != null) as number[], - ) - }, - ) +export async function pids(): Promise { + // Linux‐style: read /proc + if (!isWin && existsSync("/proc")) { + const names = await readdir("/proc") + return names.filter((d) => /^\d+$/.test(d)).map((d) => parseInt(d, 10)) + } + + // fallback: ps or tasklist + const cmd = isWin ? "tasklist" : "ps" + const args = isWin ? ["/NH", "/FO", "CSV"] : ["-e", "-o", "pid="] + + return new Promise((resolve, reject) => { + child_process.execFile(cmd, args, (err, stdout, stderr) => { + if (err) return reject(asError(err)) + if (stderr.trim()) return reject(new Error(stderr)) + + const pids = stdout + .trim() + .split(/[\r\n]+/) + .map((line) => { + if (isWin) { + // "Image","PID",… + // split on "," and strip outer quotes: + const cols = line.split('","') + const pidStr = cols[1]?.replace(/"/g, "") + return Number(pidStr) + } + // ps -o pid= gives you just the number + return Number(line.trim()) + }) + .filter((n) => Number.isFinite(n) && n > 0) + + resolve(pids) + }) }) } diff --git a/src/ProcessHealthMonitor.spec.ts b/src/ProcessHealthMonitor.spec.ts new file mode 100644 index 0000000..74b40b5 --- /dev/null +++ b/src/ProcessHealthMonitor.spec.ts @@ -0,0 +1,384 @@ +import events from "node:events" +import { expect, processFactory } from "./_chai.spec" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { DefaultTestOptions } from "./DefaultTestOptions.spec" +import { verifyOptions } from "./OptionsVerifier" +import { HealthCheckable, ProcessHealthMonitor } from "./ProcessHealthMonitor" +import { Task } from "./Task" + +describe("ProcessHealthMonitor", function () { + let healthMonitor: ProcessHealthMonitor + let emitter: BatchClusterEmitter + let mockProcess: HealthCheckable + + beforeEach(function () { + emitter = new events.EventEmitter() as BatchClusterEmitter + + const options = verifyOptions({ + ...DefaultTestOptions, + processFactory, + observer: emitter, + healthCheckCommand: "healthcheck", + healthCheckIntervalMillis: 1000, + maxTasksPerProcess: 5, + maxIdleMsPerProcess: 2000, + maxFailedTasksPerProcess: 3, + maxProcAgeMillis: 20000, // Must be > spawnTimeoutMillis + taskTimeoutMillis: 1000, + }) + + healthMonitor = new ProcessHealthMonitor(options, emitter) + + // Create a healthy mock process + mockProcess = { + pid: 12345, + start: Date.now(), + taskCount: 0, + failedTaskCount: 0, + idleMs: 0, + idle: true, + ending: false, + ended: false, + proc: { stdin: { destroyed: false } }, + currentTask: null, + } + }) + + describe("process lifecycle", function () { + it("should initialize process health monitoring", function () { + healthMonitor.initializeProcess(mockProcess.pid) + + const state = healthMonitor.getProcessHealthState(mockProcess.pid) + expect(state).to.not.be.undefined + expect(state?.healthCheckFailures).to.eql(0) + expect(state?.lastJobFailed).to.be.false + }) + + it("should cleanup process health monitoring", function () { + healthMonitor.initializeProcess(mockProcess.pid) + expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.not.be + .undefined + + healthMonitor.cleanupProcess(mockProcess.pid) + expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.be + .undefined + }) + }) + + describe("health assessment", function () { + beforeEach(function () { + healthMonitor.initializeProcess(mockProcess.pid) + }) + + it("should assess healthy process as healthy", function () { + const healthReason = healthMonitor.assessHealth(mockProcess) + expect(healthReason).to.be.null + expect(healthMonitor.isHealthy(mockProcess)).to.be.true + }) + + it("should detect ended process", function () { + const endedProcess = { ...mockProcess, ended: true } + + const healthReason = healthMonitor.assessHealth(endedProcess) + expect(healthReason).to.eql("ended") + expect(healthMonitor.isHealthy(endedProcess)).to.be.false + }) + + it("should detect ending process", function () { + const endingProcess = { ...mockProcess, ending: true } + + const healthReason = healthMonitor.assessHealth(endingProcess) + expect(healthReason).to.eql("ending") + expect(healthMonitor.isHealthy(endingProcess)).to.be.false + }) + + it("should detect closed stdin", function () { + const closedProcess = { + ...mockProcess, + proc: { stdin: { destroyed: true } }, + } + + const healthReason = healthMonitor.assessHealth(closedProcess) + expect(healthReason).to.eql("closed") + expect(healthMonitor.isHealthy(closedProcess)).to.be.false + }) + + it("should detect null stdin", function () { + const nullStdinProcess = { + ...mockProcess, + proc: { stdin: null }, + } + + const healthReason = healthMonitor.assessHealth(nullStdinProcess) + expect(healthReason).to.eql("closed") + expect(healthMonitor.isHealthy(nullStdinProcess)).to.be.false + }) + + it("should detect worn process (too many tasks)", function () { + const wornProcess = { ...mockProcess, taskCount: 5 } + + const healthReason = healthMonitor.assessHealth(wornProcess) + expect(healthReason).to.eql("worn") + expect(healthMonitor.isHealthy(wornProcess)).to.be.false + }) + + it("should detect idle process (idle too long)", function () { + const idleProcess = { ...mockProcess, idleMs: 3000 } + + const healthReason = healthMonitor.assessHealth(idleProcess) + expect(healthReason).to.eql("idle") + expect(healthMonitor.isHealthy(idleProcess)).to.be.false + }) + + it("should detect broken process (too many failed tasks)", function () { + const brokenProcess = { ...mockProcess, failedTaskCount: 3 } + + const healthReason = healthMonitor.assessHealth(brokenProcess) + expect(healthReason).to.eql("broken") + expect(healthMonitor.isHealthy(brokenProcess)).to.be.false + }) + + it("should detect old process", function () { + const oldProcess = { + ...mockProcess, + start: Date.now() - 25000, // 25 seconds ago (older than maxProcAgeMillis) + } + + const healthReason = healthMonitor.assessHealth(oldProcess) + expect(healthReason).to.eql("old") + expect(healthMonitor.isHealthy(oldProcess)).to.be.false + }) + + it("should detect timed out task", function () { + // Create a mock task that simulates a long runtime + const mockTask = { + runtimeMs: 1500, // longer than 1000ms timeout + } as Task + + const timedOutProcess = { + ...mockProcess, + currentTask: mockTask, + } + + const healthReason = healthMonitor.assessHealth(timedOutProcess) + expect(healthReason).to.eql("timeout") + expect(healthMonitor.isHealthy(timedOutProcess)).to.be.false + }) + + it("should respect override reason", function () { + const healthReason = healthMonitor.assessHealth(mockProcess, "startError") + expect(healthReason).to.eql("startError") + expect(healthMonitor.isHealthy(mockProcess, "startError")).to.be.false + }) + + it("should detect unhealthy process after health check failures", function () { + // Simulate a health check failure + const state = healthMonitor.getProcessHealthState(mockProcess.pid) + if (state != null) { + state.healthCheckFailures = 1 + } + + const healthReason = healthMonitor.assessHealth(mockProcess) + expect(healthReason).to.eql("unhealthy") + expect(healthMonitor.isHealthy(mockProcess)).to.be.false + }) + }) + + describe("readiness assessment", function () { + beforeEach(function () { + healthMonitor.initializeProcess(mockProcess.pid) + }) + + it("should assess idle healthy process as ready", function () { + const readinessReason = healthMonitor.assessReadiness(mockProcess) + expect(readinessReason).to.be.null + expect(healthMonitor.isReady(mockProcess)).to.be.true + }) + + it("should detect busy process", function () { + const busyProcess = { ...mockProcess, idle: false } + + const readinessReason = healthMonitor.assessReadiness(busyProcess) + expect(readinessReason).to.eql("busy") + expect(healthMonitor.isReady(busyProcess)).to.be.false + }) + + it("should detect unhealthy idle process", function () { + const unhealthyProcess = { ...mockProcess, ended: true } + + const readinessReason = healthMonitor.assessReadiness(unhealthyProcess) + expect(readinessReason).to.eql("ended") + expect(healthMonitor.isReady(unhealthyProcess)).to.be.false + }) + }) + + describe("job state tracking", function () { + beforeEach(function () { + healthMonitor.initializeProcess(mockProcess.pid) + }) + + it("should record job failures", function () { + healthMonitor.recordJobFailure(mockProcess.pid) + + const state = healthMonitor.getProcessHealthState(mockProcess.pid) + expect(state?.lastJobFailed).to.be.true + }) + + it("should record job successes", function () { + // First record a failure + healthMonitor.recordJobFailure(mockProcess.pid) + expect( + healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed, + ).to.be.true + + // Then record a success + healthMonitor.recordJobSuccess(mockProcess.pid) + expect( + healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed, + ).to.be.false + }) + + it("should handle recording for non-existent process gracefully", function () { + // Should not throw when recording for unknown PID + expect(() => { + healthMonitor.recordJobFailure(99999) + healthMonitor.recordJobSuccess(99999) + }).to.not.throw() + }) + }) + + describe("health check execution", function () { + let mockBatchProcess: HealthCheckable & { + execTask: (task: Task) => boolean + } + + beforeEach(function () { + healthMonitor.initializeProcess(mockProcess.pid) + + mockBatchProcess = { + ...mockProcess, + execTask: () => true, // Mock successful task execution + } + }) + + it("should skip health check when no command configured", function () { + // Create monitor with no health check command + const options = verifyOptions({ + ...DefaultTestOptions, + processFactory, + observer: emitter, + healthCheckCommand: "", + }) + const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter) + + const result = noHealthCheckMonitor.maybeRunHealthcheck(mockBatchProcess) + expect(result).to.be.undefined + }) + + it("should skip health check when process not ready", function () { + const unreadyProcess = { ...mockBatchProcess, idle: false } + + const result = healthMonitor.maybeRunHealthcheck(unreadyProcess) + expect(result).to.be.undefined + }) + + it("should run health check after job failure", function () { + healthMonitor.recordJobFailure(mockProcess.pid) + + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) + expect(result).to.not.be.undefined + expect(result?.command).to.eql("healthcheck") + }) + + it("should run health check after interval expires", function () { + // Mock an old health check + const state = healthMonitor.getProcessHealthState(mockProcess.pid) + if (state != null) { + state.lastHealthCheck = Date.now() - 2000 // 2 seconds ago + } + + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) + expect(result).to.not.be.undefined + expect(result?.command).to.eql("healthcheck") + }) + + it("should not run health check when interval hasn't expired", function () { + // Health check was just done + const state = healthMonitor.getProcessHealthState(mockProcess.pid) + if (state != null) { + state.lastHealthCheck = Date.now() + } + + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) + expect(result).to.be.undefined + }) + + it("should handle failed task execution gracefully", function () { + const failingProcess = { + ...mockBatchProcess, + execTask: () => false, // Mock failed task execution + } + + healthMonitor.recordJobFailure(mockProcess.pid) + + const result = healthMonitor.maybeRunHealthcheck(failingProcess) + expect(result).to.be.undefined + }) + }) + + describe("health statistics", function () { + it("should provide accurate health statistics", function () { + // Initialize multiple processes + healthMonitor.initializeProcess(1) + healthMonitor.initializeProcess(2) + healthMonitor.initializeProcess(3) + + // Add some failures + const state1 = healthMonitor.getProcessHealthState(1) + const state2 = healthMonitor.getProcessHealthState(2) + if (state1 != null) state1.healthCheckFailures = 2 + if (state2 != null) state2.healthCheckFailures = 1 + + const stats = healthMonitor.getHealthStats() + expect(stats.monitoredProcesses).to.eql(3) + expect(stats.totalHealthCheckFailures).to.eql(3) + expect(stats.processesWithFailures).to.eql(2) + }) + + it("should reset health check failures", function () { + healthMonitor.initializeProcess(mockProcess.pid) + + // Add some failures + const state = healthMonitor.getProcessHealthState(mockProcess.pid) + if (state != null) { + state.healthCheckFailures = 5 + } + + expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(5) + + healthMonitor.resetHealthCheckFailures(mockProcess.pid) + + expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(0) + expect(healthMonitor.isHealthy(mockProcess)).to.be.true + }) + }) + + describe("edge cases", function () { + it("should handle assessment of process without initialized state", function () { + // Don't initialize the process + const healthReason = healthMonitor.assessHealth(mockProcess) + expect(healthReason).to.be.null // Should still work, just no health check state + }) + + it("should handle health check on process without state", function () { + const mockBatchProcess = { + ...mockProcess, + execTask: () => true, + } + + // Don't initialize the process + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) + expect(result).to.be.undefined + }) + }) +}) diff --git a/src/ProcessHealthMonitor.ts b/src/ProcessHealthMonitor.ts new file mode 100644 index 0000000..0bc69d4 --- /dev/null +++ b/src/ProcessHealthMonitor.ts @@ -0,0 +1,221 @@ +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { + CompositeHealthCheckStrategy, + HealthCheckStrategy, +} from "./HealthCheckStrategy" +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { SimpleParser } from "./Parser" +import { blank } from "./String" +import { Task } from "./Task" +import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" + +/** + * Interface for objects that can be health checked + */ +export interface HealthCheckable { + readonly pid: number + readonly start: number + readonly taskCount: number + readonly failedTaskCount: number + readonly idleMs: number + readonly idle: boolean + readonly ending: boolean + readonly ended: boolean + readonly proc: { + stdin?: { destroyed?: boolean } | null + } + readonly currentTask?: Task | null | undefined +} + +/** + * Manages health checking logic for processes. + * Provides centralized health assessment and monitoring capabilities. + */ +export class ProcessHealthMonitor { + readonly #healthCheckStates = new Map< + number, + { + lastHealthCheck: number + healthCheckFailures: number + lastJobFailed: boolean + } + >() + + private readonly healthStrategy: HealthCheckStrategy + + constructor( + private readonly options: InternalBatchProcessOptions, + private readonly emitter: BatchClusterEmitter, + healthStrategy?: HealthCheckStrategy, + ) { + this.healthStrategy = healthStrategy ?? new CompositeHealthCheckStrategy() + } + + /** + * Initialize health monitoring for a process + */ + initializeProcess(pid: number): void { + this.#healthCheckStates.set(pid, { + lastHealthCheck: Date.now(), + healthCheckFailures: 0, + lastJobFailed: false, + }) + } + + /** + * Clean up health monitoring for a process + */ + cleanupProcess(pid: number): void { + this.#healthCheckStates.delete(pid) + } + + /** + * Record that a job failed for a process + */ + recordJobFailure(pid: number): void { + const state = this.#healthCheckStates.get(pid) + if (state != null) { + state.lastJobFailed = true + } + } + + /** + * Record that a job succeeded for a process + */ + recordJobSuccess(pid: number): void { + const state = this.#healthCheckStates.get(pid) + if (state != null) { + state.lastJobFailed = false + } + } + + /** + * Assess the health of a process and return why it's not healthy, or null if healthy + */ + assessHealth( + process: HealthCheckable, + overrideReason?: WhyNotHealthy, + ): WhyNotHealthy | null { + if (overrideReason != null) return overrideReason + + const state = this.#healthCheckStates.get(process.pid) + if (state != null && state.healthCheckFailures > 0) { + return "unhealthy" + } + + return this.healthStrategy.assess(process, this.options) + } + + /** + * Check if a process is healthy + */ + isHealthy(process: HealthCheckable, overrideReason?: WhyNotHealthy): boolean { + return this.assessHealth(process, overrideReason) == null + } + + /** + * Assess why a process is not ready (combines health and business) + */ + assessReadiness( + process: HealthCheckable, + overrideReason?: WhyNotHealthy, + ): WhyNotReady | null { + return !process.idle ? "busy" : this.assessHealth(process, overrideReason) + } + + /** + * Check if a process is ready to handle tasks + */ + isReady(process: HealthCheckable, overrideReason?: WhyNotHealthy): boolean { + return this.assessReadiness(process, overrideReason) == null + } + + /** + * Run a health check on a process if needed + */ + maybeRunHealthcheck( + process: HealthCheckable & { execTask: (task: Task) => boolean }, + ): Task | undefined { + const hcc = this.options.healthCheckCommand + // if there's no health check command, no-op. + if (hcc == null || blank(hcc)) return + + // if the prior health check failed, .ready will be false + if (!this.isReady(process)) return + + const state = this.#healthCheckStates.get(process.pid) + if (state == null) return + + if ( + state.lastJobFailed || + (this.options.healthCheckIntervalMillis > 0 && + Date.now() - state.lastHealthCheck > + this.options.healthCheckIntervalMillis) + ) { + state.lastHealthCheck = Date.now() + const t = new Task(hcc, SimpleParser) + t.promise + .catch((err) => { + this.emitter.emit( + "healthCheckError", + err instanceof Error ? err : new Error(String(err)), + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + process as any, // Type assertion for event emission + ) + state.healthCheckFailures++ + // BatchCluster will see we're unhealthy and reap us later + }) + .finally(() => { + state.lastHealthCheck = Date.now() + }) + + // Execute the health check task on the process + if (process.execTask(t as Task)) { + return t as Task + } + } + return + } + + /** + * Get health statistics for monitoring + */ + getHealthStats(): { + monitoredProcesses: number + totalHealthCheckFailures: number + processesWithFailures: number + } { + let totalFailures = 0 + let processesWithFailures = 0 + + for (const state of this.#healthCheckStates.values()) { + totalFailures += state.healthCheckFailures + if (state.healthCheckFailures > 0) { + processesWithFailures++ + } + } + + return { + monitoredProcesses: this.#healthCheckStates.size, + totalHealthCheckFailures: totalFailures, + processesWithFailures, + } + } + + /** + * Reset health check failures for a process (useful for recovery scenarios) + */ + resetHealthCheckFailures(pid: number): void { + const state = this.#healthCheckStates.get(pid) + if (state != null) { + state.healthCheckFailures = 0 + } + } + + /** + * Get health check state for a specific process + */ + getProcessHealthState(pid: number) { + return this.#healthCheckStates.get(pid) + } +} diff --git a/src/ProcessPoolManager.spec.ts b/src/ProcessPoolManager.spec.ts new file mode 100644 index 0000000..a372dc8 --- /dev/null +++ b/src/ProcessPoolManager.spec.ts @@ -0,0 +1,253 @@ +import events from "node:events" +import { + currentTestPids, + expect, + processFactory, + setFailratePct, + setIgnoreExit, +} from "./_chai.spec" +import { delay, until } from "./Async" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { DefaultTestOptions } from "./DefaultTestOptions.spec" +import { verifyOptions } from "./OptionsVerifier" +import { ProcessPoolManager } from "./ProcessPoolManager" + +describe("ProcessPoolManager", function () { + let poolManager: ProcessPoolManager + let emitter: BatchClusterEmitter + + const onIdle = () => { + // callback for when pool manager needs to signal idle state + } + + beforeEach(function () { + setFailratePct(0) // no failures for pool manager tests + setIgnoreExit(false) + emitter = new events.EventEmitter() as BatchClusterEmitter + + const options = verifyOptions({ + ...DefaultTestOptions, + processFactory, + observer: emitter, + }) + + poolManager = new ProcessPoolManager(options, emitter, onIdle) + }) + + afterEach(async function () { + if (poolManager != null) { + await poolManager.closeChildProcesses(false) + // Wait for processes to actually exit + await until(async () => (await currentTestPids()).length === 0, 5000) + } + }) + + describe("initial state", function () { + it("should start with no processes", function () { + expect(poolManager.procCount).to.eql(0) + expect(poolManager.busyProcCount).to.eql(0) + expect(poolManager.startingProcCount).to.eql(0) + expect(poolManager.spawnedProcCount).to.eql(0) + expect(poolManager.processes).to.eql([]) + expect(poolManager.findReadyProcess()).to.be.undefined + }) + + it("should return empty pids array", function () { + expect(poolManager.pids()).to.eql([]) + }) + }) + + describe("process spawning", function () { + it("should spawn processes when there are pending tasks", async function () { + const pendingTaskCount = 2 + await poolManager.maybeSpawnProcs(pendingTaskCount, false) + + expect(poolManager.procCount).to.be.greaterThan(0) + expect(poolManager.spawnedProcCount).to.be.greaterThan(0) + + // Wait for processes to be ready + await until(() => poolManager.findReadyProcess() != null, 2000) + expect(poolManager.findReadyProcess()).to.not.be.undefined + }) + + it("should not spawn more processes than maxProcs", async function () { + const maxProcs = 2 + poolManager.setMaxProcs(maxProcs) + + // Try to spawn more than maxProcs + await poolManager.maybeSpawnProcs(5, false) + + expect(poolManager.procCount).to.be.at.most(maxProcs) + }) + + it("should not spawn processes when ended", async function () { + await poolManager.maybeSpawnProcs(2, true) // ended = true + + expect(poolManager.procCount).to.eql(0) + expect(poolManager.spawnedProcCount).to.eql(0) + }) + + it("should spawn multiple processes for multiple pending tasks", async function () { + const pendingTaskCount = 3 + poolManager.setMaxProcs(4) + + await poolManager.maybeSpawnProcs(pendingTaskCount, false) + + // Should spawn up to the number of pending tasks or maxProcs + expect(poolManager.procCount).to.be.at.least(1) + expect(poolManager.procCount).to.be.at.most(Math.min(pendingTaskCount, 4)) + }) + }) + + describe("process management", function () { + beforeEach(async function () { + // Spawn some processes for testing + await poolManager.maybeSpawnProcs(2, false) + await until(() => poolManager.procCount >= 1, 2000) + }) + + it("should track process PIDs", function () { + const pids = poolManager.pids() + expect(pids.length).to.be.greaterThan(0) + expect(pids.every((pid) => typeof pid === "number" && pid > 0)).to.be.true + }) + + it("should find ready processes", async function () { + await until(() => poolManager.findReadyProcess() != null, 2000) + const readyProcess = poolManager.findReadyProcess() + expect(readyProcess).to.not.be.undefined + expect(readyProcess?.ready).to.be.true + }) + + it("should vacuum unhealthy processes", async function () { + // Wait for processes to be ready + await until(() => poolManager.findReadyProcess() != null, 2000) + + const initialCount = poolManager.procCount + expect(initialCount).to.be.greaterThan(0) + + // Vacuum should not remove healthy processes + await poolManager.vacuumProcs() + expect(poolManager.procCount).to.eql(initialCount) + }) + + it("should reduce process count when maxProcs is lowered", async function () { + // Ensure we have multiple processes + poolManager.setMaxProcs(3) + await poolManager.maybeSpawnProcs(3, false) + await until(() => poolManager.procCount >= 2, 2000) + + const initialCount = poolManager.procCount + + // Reduce maxProcs + poolManager.setMaxProcs(1) + await poolManager.vacuumProcs() + + // Should eventually reduce to 1 process (may take time for idle processes to be reaped) + await until(() => poolManager.procCount <= 1, 3000) + expect(poolManager.procCount).to.be.at.most(1) + expect(poolManager.procCount).to.be.lessThanOrEqual(initialCount) + }) + }) + + describe("process lifecycle", function () { + it("should close all processes gracefully", async function () { + await poolManager.maybeSpawnProcs(2, false) + await until(() => poolManager.procCount >= 1, 2000) + + const initialPids = poolManager.pids() + expect(initialPids.length).to.be.greaterThan(0) + + await poolManager.closeChildProcesses(true) + + expect(poolManager.procCount).to.eql(0) + + // Wait for processes to actually exit + await until(async () => { + const remainingPids = await currentTestPids() + return ( + remainingPids.filter((pid) => initialPids.includes(pid)).length === 0 + ) + }, 5000) + }) + + it("should close all processes forcefully", async function () { + await poolManager.maybeSpawnProcs(2, false) + await until(() => poolManager.procCount >= 1, 2000) + + const initialPids = poolManager.pids() + expect(initialPids.length).to.be.greaterThan(0) + + await poolManager.closeChildProcesses(false) + + expect(poolManager.procCount).to.eql(0) + + // Wait for processes to actually exit + await until(async () => { + const remainingPids = await currentTestPids() + return ( + remainingPids.filter((pid) => initialPids.includes(pid)).length === 0 + ) + }, 5000) + }) + }) + + describe("process counting", function () { + it("should track starting processes", async function () { + // Start spawning processes but don't wait for completion + const spawnPromise = poolManager.maybeSpawnProcs(2, false) + + // Should show starting processes initially + await delay(50) // Give it a moment to start + const totalProcs = poolManager.procCount + const startingProcs = poolManager.startingProcCount + + expect(totalProcs).to.be.greaterThan(0) + expect(startingProcs).to.be.greaterThan(0) + + await spawnPromise + + // Wait for processes to be ready + await until(() => poolManager.startingProcCount === 0, 2000) + expect(poolManager.startingProcCount).to.eql(0) + }) + + it("should track busy vs idle processes", async function () { + await poolManager.maybeSpawnProcs(1, false) + await until(() => poolManager.findReadyProcess() != null, 2000) + + // Initially all processes should be idle (not busy) + expect(poolManager.busyProcCount).to.eql(0) + + const readyProcess = poolManager.findReadyProcess() + expect(readyProcess).to.not.be.undefined + expect(readyProcess?.idle).to.be.true + }) + }) + + describe("event integration", function () { + it("should work with emitter for process lifecycle events", async function () { + const childStartEvents: any[] = [] + const childEndEvents: any[] = [] + + emitter.on("childStart", (proc) => { + childStartEvents.push(proc) + }) + + emitter.on("childEnd", (proc, reason) => { + childEndEvents.push({ proc, reason }) + }) + + await poolManager.maybeSpawnProcs(1, false) + await until(() => childStartEvents.length >= 1, 2000) + + expect(childStartEvents.length).to.be.greaterThan(0) + + await poolManager.closeChildProcesses(true) + await until(() => childEndEvents.length >= 1, 2000) + + expect(childEndEvents.length).to.be.greaterThan(0) + expect(childEndEvents[0].reason).to.eql("ending") + }) + }) +}) diff --git a/src/ProcessPoolManager.ts b/src/ProcessPoolManager.ts new file mode 100644 index 0000000..fd62fed --- /dev/null +++ b/src/ProcessPoolManager.ts @@ -0,0 +1,309 @@ +import timers from "node:timers" +import { count, filterInPlace } from "./Array" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { BatchProcess } from "./BatchProcess" +import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" +import { asError } from "./Error" +import { Logger } from "./Logger" +import { ProcessHealthMonitor } from "./ProcessHealthMonitor" +import { Task } from "./Task" +import { Timeout, thenOrTimeout } from "./Timeout" + +/** + * Manages the lifecycle of a pool of BatchProcess instances. + * Handles spawning, health monitoring, and cleanup of child processes. + */ +export class ProcessPoolManager { + readonly #procs: BatchProcess[] = [] + readonly #logger: () => Logger + readonly #healthMonitor: ProcessHealthMonitor + #nextSpawnTime = 0 + #lastPidsCheckTime = 0 + #spawnedProcs = 0 + + constructor( + private readonly options: CombinedBatchProcessOptions, + private readonly emitter: BatchClusterEmitter, + private readonly onIdle: () => void, + ) { + this.#logger = options.logger + this.#healthMonitor = new ProcessHealthMonitor(options, emitter) + } + + /** + * Get all current processes + */ + get processes(): readonly BatchProcess[] { + return this.#procs + } + + /** + * Get the current number of spawned child processes + */ + get procCount(): number { + return this.#procs.length + } + + /** + * Alias for procCount to match BatchCluster interface + */ + get processCount(): number { + return this.procCount + } + + /** + * Get the current number of child processes currently servicing tasks + */ + get busyProcCount(): number { + return count( + this.#procs, + // don't count procs that are starting up as "busy": + (ea) => !ea.starting && !ea.ending && !ea.idle, + ) + } + + /** + * Get the current number of starting processes + */ + get startingProcCount(): number { + return count( + this.#procs, + // don't count procs that are starting up as "busy": + (ea) => ea.starting && !ea.ending, + ) + } + + /** + * Get the current number of ready processes + */ + get readyProcCount(): number { + return count(this.#procs, (ea) => ea.ready) + } + + /** + * Get the total number of child processes created by this instance + */ + get spawnedProcCount(): number { + return this.#spawnedProcs + } + + /** + * Get the milliseconds until the next spawn is allowed + */ + get msBeforeNextSpawn(): number { + return Math.max(0, this.#nextSpawnTime - Date.now()) + } + + /** + * Get all currently running tasks from all processes + */ + currentTasks(): Task[] { + const tasks: Task[] = [] + for (const proc of this.#procs) { + if (proc.currentTask != null) { + tasks.push(proc.currentTask) + } + } + return tasks + } + + /** + * Find the first ready process that can handle a new task + */ + findReadyProcess(): BatchProcess | undefined { + return this.#procs.find((ea) => ea.ready) + } + + /** + * Verify that each BatchProcess PID is actually alive. + * @return the spawned PIDs that are still in the process table. + */ + pids(): number[] { + const arr: number[] = [] + for (const proc of [...this.#procs]) { + if (proc != null && proc.running()) { + arr.push(proc.pid) + } + } + return arr + } + + /** + * Shut down any currently-running child processes. + */ + async closeChildProcesses(gracefully = true): Promise { + const procs = [...this.#procs] + this.#procs.length = 0 + await Promise.all( + procs.map((proc) => + proc + .end(gracefully, "ending") + .catch((err) => this.emitter.emit("endError", asError(err), proc)), + ), + ) + } + + /** + * Run maintenance on currently spawned child processes. + * Removes unhealthy processes and enforces maxProcs limit. + */ + vacuumProcs(): Promise { + this.#maybeCheckPids() + const endPromises: Promise[] = [] + let pidsToReap = Math.max(0, this.#procs.length - this.options.maxProcs) + + filterInPlace(this.#procs, (proc) => { + // Only check `.idle` (not `.ready`) procs. We don't want to reap busy + // procs unless we're ending, and unhealthy procs (that we want to reap) + // won't be `.ready`. + if (proc.idle) { + // don't reap more than pidsToReap pids. We can't use #procs.length + // within filterInPlace because #procs.length only changes at iteration + // completion: the prior impl resulted in all idle pids getting reaped + // when maxProcs was reduced. + const why = proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null) + if (why != null) { + endPromises.push(proc.end(true, why)) + return false + } + proc.maybeRunHealthcheck() + } + return true + }) + + return Promise.all(endPromises) + } + + /** + * Spawn new processes if needed based on pending task count and capacity + */ + async maybeSpawnProcs( + pendingTaskCount: number, + ended: boolean, + ): Promise { + let procsToSpawn = this.#procsToSpawn(pendingTaskCount) + + if (ended || this.#nextSpawnTime > Date.now() || procsToSpawn === 0) { + return + } + + // prevent concurrent runs: + this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() + + for (let i = 0; i < procsToSpawn; i++) { + if (ended) { + break + } + + // Kick the lock down the road: + this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() + this.#spawnedProcs++ + + try { + const proc = this.#spawnNewProc() + const result = await thenOrTimeout( + proc, + this.options.spawnTimeoutMillis, + ) + if (result === Timeout) { + void proc + .then((bp) => { + void bp.end(false, "startError") + this.emitter.emit( + "startError", + asError( + "Failed to spawn process in " + + this.options.spawnTimeoutMillis + + "ms", + ), + bp, + ) + }) + .catch((err) => { + // this should only happen if the processFactory throws a + // rejection: + this.emitter.emit("startError", asError(err)) + }) + } else { + this.#logger().debug( + "ProcessPoolManager.maybeSpawnProcs() started healthy child process", + { pid: result.pid }, + ) + } + + // tasks may have been popped off or setMaxProcs may have reduced + // maxProcs. Do this at the end so the for loop ends properly. + procsToSpawn = Math.min( + this.#procsToSpawn(pendingTaskCount), + procsToSpawn, + ) + } catch (err) { + this.emitter.emit("startError", asError(err)) + } + } + + // YAY WE MADE IT. + // Only let more children get spawned after minDelay: + const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis) + this.#nextSpawnTime = Date.now() + delay + + // And schedule #onIdle for that time: + timers.setTimeout(this.onIdle, delay).unref() + } + + /** + * Update the maximum number of processes allowed + */ + setMaxProcs(maxProcs: number): void { + this.options.maxProcs = maxProcs + } + + #maybeCheckPids(): void { + if ( + this.options.cleanupChildProcs && + this.options.pidCheckIntervalMillis > 0 && + this.#lastPidsCheckTime + this.options.pidCheckIntervalMillis < Date.now() + ) { + this.#lastPidsCheckTime = Date.now() + void this.pids() + } + } + + #maxSpawnDelay(): number { + // 10s delay is certainly long enough for .spawn() to return, even on a + // loaded windows machine. + return Math.max(10_000, this.options.spawnTimeoutMillis) + } + + #procsToSpawn(pendingTaskCount: number): number { + const remainingCapacity = this.options.maxProcs - this.#procs.length + + // take into account starting procs, so one task doesn't result in multiple + // processes being spawned: + const requestedCapacity = pendingTaskCount - this.startingProcCount + + const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity)) + + return this.options.minDelayBetweenSpawnMillis === 0 + ? // we can spin up multiple processes in parallel. + atLeast0 + : // Don't spin up more than 1: + Math.min(1, atLeast0) + } + + // must only be called by this.maybeSpawnProcs() + async #spawnNewProc(): Promise { + // no matter how long it takes to spawn, always push the result into #procs + // so we don't leak child processes: + const procOrPromise = this.options.processFactory() + const proc = await procOrPromise + const result = new BatchProcess( + proc, + this.options, + this.onIdle, + this.#healthMonitor, + ) + this.#procs.push(result) + return result + } +} diff --git a/src/ProcessTerminator.spec.ts b/src/ProcessTerminator.spec.ts new file mode 100644 index 0000000..68f29f7 --- /dev/null +++ b/src/ProcessTerminator.spec.ts @@ -0,0 +1,687 @@ +import events from "node:events" +import stream from "node:stream" +import { expect } from "./_chai.spec" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { logger } from "./Logger" +import { SimpleParser } from "./Parser" +import { ProcessTerminator } from "./ProcessTerminator" +import { Task } from "./Task" + +describe("ProcessTerminator", function () { + let terminator: ProcessTerminator + let mockProcess: MockChildProcess + let emitter: BatchClusterEmitter + let options: InternalBatchProcessOptions + let isRunningResult: boolean + let childEndEvents: { process: any; reason: string }[] + + // Mock child process class + class MockChildProcess extends events.EventEmitter { + pid = 12345 + stdin = new MockWritableStream() + stdout = new MockReadableStream() + stderr = new MockReadableStream() + killed = false + disconnected = false + + kill() { + this.killed = true + return true + } + + disconnect() { + this.disconnected = true + } + + unref() { + // no-op for tests + } + } + + class MockWritableStream extends stream.Writable { + override destroyed = false + override writable = true + data: string[] = [] + + override _write(chunk: any, _encoding: any, callback: any) { + this.data.push(chunk.toString()) + callback() + } + + override end(data?: any): this { + if (data != null) { + this.data.push(data.toString()) + } + this.writable = false + super.end() + return this + } + + override destroy(): this { + this.destroyed = true + super.destroy() + return this + } + } + + class MockReadableStream extends stream.Readable { + override destroyed = false + + override _read() { + // no-op for tests + } + + override destroy(): this { + this.destroyed = true + super.destroy() + return this + } + } + + beforeEach(function () { + emitter = new events.EventEmitter() as BatchClusterEmitter + childEndEvents = [] + + // Track childEnd events + emitter.on("childEnd", (process: any, reason: string) => { + childEndEvents.push({ process, reason }) + }) + + options = { + logger, + observer: emitter, + cleanupChildProcs: true, + endGracefulWaitTimeMillis: 1000, + exitCommand: "exit", + spawnTimeoutMillis: 5000, + taskTimeoutMillis: 30000, + streamFlushMillis: 100, + versionCommand: "version", + healthCheckCommand: "healthcheck", + healthCheckIntervalMillis: 60000, + maxTasksPerProcess: 100, + maxIdleMsPerProcess: 300000, + maxFailedTasksPerProcess: 3, + maxProcAgeMillis: 600000, + pass: "PASS", + fail: "FAIL", + passRE: /PASS/, + failRE: /FAIL/, + maxProcs: 4, + onIdleIntervalMillis: 10, + maxReasonableProcessFailuresPerMinute: 10, + minDelayBetweenSpawnMillis: 100, + pidCheckIntervalMillis: 150, + } + + terminator = new ProcessTerminator(options) + mockProcess = new MockChildProcess() + isRunningResult = true + }) + + function createMockTask( + taskId = 1, + command = "test", + pending = true, + ): Task { + const task = new Task(command, SimpleParser) + if (!pending) { + // Simulate task completion by calling onStdout with PASS token + task.onStart(options) + task.onStdout("PASS") + } + // Override taskId for testing + Object.defineProperty(task, "taskId", { value: taskId, writable: true }) + return task as Task + } + + function mockIsRunning(): boolean { + return isRunningResult + } + + describe("basic termination", function () { + it("should terminate process without current task", async function () { + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, // no current task + 999, // startup task id + true, // graceful + false, // not exited + mockIsRunning, + ) + + // Should send exit command + expect(mockProcess.stdin.data).to.include("exit\n") + + // Should destroy streams + expect(mockProcess.stdin.destroyed).to.be.true + expect(mockProcess.stdout.destroyed).to.be.true + expect(mockProcess.stderr.destroyed).to.be.true + + // Should disconnect + expect(mockProcess.disconnected).to.be.true + }) + + it("should terminate process forcefully when not graceful", async function () { + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + false, // not graceful + false, + mockIsRunning, + ) + + expect(mockProcess.stdin.data).to.include("exit\n") + expect(mockProcess.disconnected).to.be.true + }) + + it("should handle process that is already exited", async function () { + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + true, // already exited + mockIsRunning, + ) + + expect(mockProcess.stdin.data).to.include("exit\n") + expect(mockProcess.disconnected).to.be.true + }) + }) + + describe("task completion handling", function () { + it("should wait for non-startup task to complete gracefully", async function () { + const task = createMockTask(1, "test command", true) + let taskCompleted = false + + // Simulate task completion after a delay + setTimeout(() => { + taskCompleted = true + task.onStart(options) + task.onStdout("PASS") // Complete the task + }, 50) + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + task, + 999, // different from task ID + true, // graceful + false, + mockIsRunning, + ) + + expect(taskCompleted).to.be.true + expect(task.state !== "pending").to.be.true + }) + + it("should reject pending task if termination timeout occurs", async function () { + const task = createMockTask(1, "slow task", true) + let taskRejected = false + let rejectionReason = "" + + task.promise.catch((err) => { + taskRejected = true + rejectionReason = err.message + }) + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + task, + 999, + false, // not graceful - shorter timeout + false, + mockIsRunning, + ) + + expect(taskRejected).to.be.true + expect(rejectionReason).to.include( + "Process terminated before task completed", + ) + }) + + it("should skip task completion wait for startup task", async function () { + const startupTask = createMockTask(999, "version", true) + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + startupTask, + 999, // same as task ID - is startup task + true, + false, + mockIsRunning, + ) + + // Should not wait for or reject startup task + expect(startupTask.pending).to.be.true + }) + + it("should skip task completion wait when no current task", async function () { + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, // no current task + 999, + true, + false, + mockIsRunning, + ) + + // Should complete without errors + expect(mockProcess.disconnected).to.be.true + }) + }) + + describe("stream handling", function () { + it("should remove error listeners from all streams", async function () { + // Add some error listeners + const errorHandler = () => { + // no-op for test + } + mockProcess.on("error", errorHandler) + mockProcess.stdin.on("error", errorHandler) + mockProcess.stdout.on("error", errorHandler) + mockProcess.stderr.on("error", errorHandler) + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + // Error listeners should be removed + expect(mockProcess.listenerCount("error")).to.equal(0) + expect(mockProcess.stdin.listenerCount("error")).to.equal(0) + expect(mockProcess.stdout.listenerCount("error")).to.equal(0) + expect(mockProcess.stderr.listenerCount("error")).to.equal(0) + }) + + it("should send exit command if stdin is writable", async function () { + mockProcess.stdin.writable = true + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.stdin.data).to.include("exit\n") + }) + + it("should skip exit command if stdin is not writable", async function () { + mockProcess.stdin.writable = false + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.stdin.data).to.be.empty + }) + + it("should handle missing exit command gracefully", async function () { + const optionsNoExit = { ...options, exitCommand: undefined } + const terminatorNoExit = new ProcessTerminator(optionsNoExit) + + await terminatorNoExit.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + // Should complete without sending exit command + expect(mockProcess.stdin.data).to.be.empty + expect(mockProcess.disconnected).to.be.true + }) + + it("should destroy all streams", async function () { + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.stdin.destroyed).to.be.true + expect(mockProcess.stdout.destroyed).to.be.true + expect(mockProcess.stderr.destroyed).to.be.true + }) + }) + + describe("graceful shutdown", function () { + it("should wait for process to exit gracefully", async function () { + let killCalled = false + mockProcess.kill = () => { + killCalled = true + return true + } + + // Simulate process still running initially, then stopping + let callCount = 0 + const mockIsRunningGraceful = () => { + callCount++ + if (callCount <= 2) { + return true // Still running for first few checks + } + return false // Then stops running + } + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, // graceful + false, // not already exited + mockIsRunningGraceful, + ) + + expect(killCalled).to.be.false // Should not need to kill + }) + + it("should send SIGTERM if process doesn't exit gracefully", async function () { + let killCalled = false + mockProcess.kill = () => { + killCalled = true + isRunningResult = false // Process stops after kill signal + return true + } + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, // graceful + false, // not already exited + mockIsRunning, // Always returns true until killed + ) + + expect(killCalled).to.be.true + }) + + it("should skip graceful shutdown when cleanup disabled", async function () { + const optionsNoCleanup = { ...options, cleanupChildProcs: false } + const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup) + + let killCalled = false + mockProcess.kill = () => { + killCalled = true + return true + } + + await terminatorNoCleanup.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(killCalled).to.be.false + }) + + it("should skip graceful shutdown when wait time is 0", async function () { + const optionsNoWait = { ...options, endGracefulWaitTimeMillis: 0 } + const terminatorNoWait = new ProcessTerminator(optionsNoWait) + + let killCalled = false + mockProcess.kill = () => { + killCalled = true + return true + } + + await terminatorNoWait.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(killCalled).to.be.false + }) + }) + + describe("force killing", function () { + it("should complete termination even with stubborn process", async function () { + // Process keeps running even after signals + const mockIsRunningStubborn = () => true + + // Should complete without throwing + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunningStubborn, + ) + + // Should still disconnect and destroy streams + expect(mockProcess.disconnected).to.be.true + expect(mockProcess.stdin.destroyed).to.be.true + }) + + it("should complete termination when cleanup disabled", async function () { + const optionsNoCleanup = { ...options, cleanupChildProcs: false } + const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup) + + await terminatorNoCleanup.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + () => true, // Always running + ) + + // Should still complete basic cleanup + expect(mockProcess.disconnected).to.be.true + expect(mockProcess.stdin.destroyed).to.be.true + }) + + it("should handle process with no PID gracefully", async function () { + ;(mockProcess as any).pid = undefined + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + () => true, + ) + + // Should complete without issues + expect(mockProcess.disconnected).to.be.true + expect(mockProcess.stdin.destroyed).to.be.true + }) + }) + + describe("error handling", function () { + it("should handle stdin.end() errors gracefully", async function () { + mockProcess.stdin.end = () => { + throw new Error("EPIPE") + } + + // Should not throw + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.disconnected).to.be.true + }) + + it("should handle stream destruction errors gracefully", async function () { + mockProcess.stdout.destroy = () => { + throw new Error("Stream error") + } + + // Should not throw + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.disconnected).to.be.true + }) + + it("should handle disconnect errors gracefully", async function () { + mockProcess.disconnect = () => { + throw new Error("Disconnect error") + } + + // Should not throw + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + }) + }) + + describe("edge cases", function () { + it("should handle null stderr stream", async function () { + ;(mockProcess as any).stderr = null + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.disconnected).to.be.true + }) + + it("should handle already completed task", async function () { + const completedTask = createMockTask(1, "completed", false) + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + completedTask, + 999, + true, + false, + mockIsRunning, + ) + + expect(completedTask.state !== "pending").to.be.true + expect(mockProcess.disconnected).to.be.true + }) + + it("should handle process with undefined PID", async function () { + ;(mockProcess as any).pid = undefined + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + undefined, + 999, + true, + false, + mockIsRunning, + ) + + expect(mockProcess.disconnected).to.be.true + }) + }) + + describe("timing and timeouts", function () { + it("should respect graceful task timeout", async function () { + const slowTask = createMockTask(1, "slow", true) + const startTime = Date.now() + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + slowTask, + 999, + true, // graceful - should wait up to 2000ms for task + false, + mockIsRunning, + ) + + const elapsed = Date.now() - startTime + // Should have waited some time but not too long + expect(elapsed).to.be.greaterThan(50) + expect(elapsed).to.be.lessThan(4000) + expect(slowTask.pending).to.be.false // Should be rejected + }) + + it("should respect non-graceful task timeout", async function () { + const slowTask = createMockTask(1, "slow", true) + const startTime = Date.now() + + await terminator.terminate( + mockProcess as any, + "TestProcess(12345)", + slowTask, + 999, + false, // not graceful - should wait only 250ms for task + false, + mockIsRunning, + ) + + const elapsed = Date.now() - startTime + // Should have waited less time + expect(elapsed).to.be.lessThan(1000) + expect(slowTask.pending).to.be.false // Should be rejected + }) + }) +}) diff --git a/src/ProcessTerminator.ts b/src/ProcessTerminator.ts new file mode 100644 index 0000000..547bedf --- /dev/null +++ b/src/ProcessTerminator.ts @@ -0,0 +1,185 @@ +import child_process from "node:child_process" +import { until } from "./Async" +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { Logger } from "./Logger" +import { kill } from "./Pids" +import { destroy } from "./Stream" +import { ensureSuffix } from "./String" +import { Task } from "./Task" +import { thenOrTimeout } from "./Timeout" + +/** + * Utility class for managing process termination lifecycle + */ +export class ProcessTerminator { + readonly #logger: () => Logger + + constructor(private readonly opts: InternalBatchProcessOptions) { + this.#logger = opts.logger + } + + /** + * Terminates a child process gracefully or forcefully + * + * @param proc The child process to terminate + * @param processName Name for logging purposes + * @param pid Process ID + * @param lastTask Current task being processed + * @param startupTaskId ID of the startup task + * @param gracefully Whether to wait for current task completion + * @param reason Reason for termination + * @param isExited Whether the process has already exited + * @param isRunning Function to check if process is still running + * @returns Promise that resolves when termination is complete + */ + async terminate( + proc: child_process.ChildProcess, + processName: string, + lastTask: Task | undefined, + startupTaskId: number, + gracefully: boolean, + isExited: boolean, + isRunning: () => boolean, + ): Promise { + // Wait for current task to complete if graceful termination requested + await this.#waitForTaskCompletion(lastTask, startupTaskId, gracefully) + + // Remove error listeners to prevent EPIPE errors during termination + this.#removeErrorListeners(proc) + + // Send exit command to process + this.#sendExitCommand(proc) + + // Destroy streams + this.#destroyStreams(proc) + + // Handle graceful shutdown with timeouts + await this.#handleGracefulShutdown(proc, gracefully, isExited, isRunning) + + // Force kill if still running + this.#forceKillIfRunning(proc, processName, isRunning) + + // Final cleanup + try { + proc.disconnect?.() + } catch { + // Ignore disconnect errors + } + // Note: Caller should emit childEnd event with proper BatchProcess instance + } + + async #waitForTaskCompletion( + lastTask: Task | undefined, + startupTaskId: number, + gracefully: boolean, + ): Promise { + // Don't wait for startup tasks or if no task is running + if (lastTask == null || lastTask.taskId === startupTaskId) { + return + } + + try { + // Wait for the process to complete and streams to flush + await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250) + } catch { + // Ignore errors during task completion wait + } + + // Reject task if still pending + if (lastTask.pending) { + lastTask.reject( + new Error( + `Process terminated before task completed (${JSON.stringify({ + gracefully, + lastTask, + })})`, + ), + ) + } + } + + #removeErrorListeners(proc: child_process.ChildProcess): void { + // Remove error listeners to prevent EPIPE errors during termination + // See https://github.com/nodejs/node/issues/26828 + for (const stream of [proc, proc.stdin, proc.stdout, proc.stderr]) { + stream?.removeAllListeners("error") + } + } + + #sendExitCommand(proc: child_process.ChildProcess): void { + if (proc.stdin?.writable !== true) { + return + } + + const exitCmd = + this.opts.exitCommand == null + ? null + : ensureSuffix(this.opts.exitCommand, "\n") + + try { + proc.stdin.end(exitCmd) + } catch { + // Ignore errors when sending exit command + } + } + + #destroyStreams(proc: child_process.ChildProcess): void { + // Destroy all streams to ensure cleanup + destroy(proc.stdin) + destroy(proc.stdout) + destroy(proc.stderr) + } + + async #handleGracefulShutdown( + proc: child_process.ChildProcess, + gracefully: boolean, + isExited: boolean, + isRunning: () => boolean, + ): Promise { + if ( + !this.opts.cleanupChildProcs || + !gracefully || + this.opts.endGracefulWaitTimeMillis <= 0 || + isExited + ) { + return + } + + // Wait for the exit command to take effect + await this.#awaitNotRunning( + this.opts.endGracefulWaitTimeMillis / 2, + isRunning, + ) + + // If still running, send kill signal + if (isRunning() && proc.pid != null) { + proc.kill() + } + + // Wait for the signal handler to work + await this.#awaitNotRunning( + this.opts.endGracefulWaitTimeMillis / 2, + isRunning, + ) + } + + #forceKillIfRunning( + proc: child_process.ChildProcess, + processName: string, + isRunning: () => boolean, + ): void { + if (this.opts.cleanupChildProcs && proc.pid != null && isRunning()) { + this.#logger().warn( + `${processName}.terminate(): force-killing still-running child.`, + ) + kill(proc.pid, true) + } + } + + async #awaitNotRunning( + timeout: number, + isRunning: () => boolean, + ): Promise { + await until(() => !isRunning(), timeout) + } +} diff --git a/src/ProcpsChecker.spec.ts b/src/ProcpsChecker.spec.ts new file mode 100644 index 0000000..5becc76 --- /dev/null +++ b/src/ProcpsChecker.spec.ts @@ -0,0 +1,33 @@ +import { expect } from "chai" +import { describe, it } from "mocha" +import { ProcpsMissingError, validateProcpsAvailable } from "./ProcpsChecker" + +describe("ProcpsChecker", () => { + describe("validateProcpsAvailable()", () => { + it("should not throw on systems with procps installed", () => { + // Since we're running tests, procps should be available + expect(() => validateProcpsAvailable()).to.not.throw() + }) + + it("should create appropriate error message for platform", () => { + const error = new ProcpsMissingError() + expect(error.name).to.equal("ProcpsMissingError") + expect(error.message).to.include("command not available") + + // Message should be specific to platform + if (process.platform === "win32") { + expect(error.message).to.include("tasklist") + } else { + expect(error.message).to.include("ps") + expect(error.message).to.include("procps") + } + }) + + it("should preserve original error", () => { + const originalError = new Error("Command failed") + const procpsError = new ProcpsMissingError(originalError) + + expect(procpsError.originalError).to.equal(originalError) + }) + }) +}) diff --git a/src/ProcpsChecker.ts b/src/ProcpsChecker.ts new file mode 100644 index 0000000..85a9fa1 --- /dev/null +++ b/src/ProcpsChecker.ts @@ -0,0 +1,52 @@ +import child_process from "node:child_process" +import { existsSync, readdirSync } from "node:fs" +import { isWin } from "./Platform" + +/** + * Error thrown when procps is missing on non-Windows systems + */ +export class ProcpsMissingError extends Error { + readonly originalError?: Error + + constructor(originalError?: Error) { + const message = isWin + ? "tasklist command not available" + : "ps command not available. Please install procps package (e.g., 'apt-get install procps' on Ubuntu/Debian)" + + super(message) + this.name = "ProcpsMissingError" + + if (originalError != null) { + this.originalError = originalError + } + } +} + +/** + * Check if the required process listing command is available + * @throws {ProcpsMissingError} if the command is not available + */ +export function validateProcpsAvailable(): void { + // on POSIX systems with a working /proc we can skip ps entirely + if (!isWin && existsSync("/proc")) { + const entries = readdirSync("/proc") + // if we see at least one numeric directory, assume /proc is usable + if (entries.some((d) => /^\d+$/.test(d))) { + return + } + // fall through to check `ps` if /proc is empty or unusable + } + + try { + const command = isWin ? "tasklist" : "ps" + const args = isWin ? ["/NH", "/FO", "CSV", "/FI", "PID eq 1"] : ["-p", "1"] + const timeout = isWin ? 15_000 : 5_000 // 15s for Windows, 5s elsewhere + + child_process.execFileSync(command, args, { + stdio: "pipe", + timeout, + }) + } catch (err) { + throw new ProcpsMissingError(err instanceof Error ? err : undefined) + } +} diff --git a/src/Rate.spec.ts b/src/Rate.spec.ts index 82f7b71..c2c6c1a 100644 --- a/src/Rate.spec.ts +++ b/src/Rate.spec.ts @@ -1,20 +1,22 @@ +import FakeTimers from "@sinonjs/fake-timers" import { minuteMs } from "./BatchClusterOptions" import { Rate } from "./Rate" import { expect, times } from "./_chai.spec" -const tk = require("timekeeper") - describe("Rate", () => { const now = Date.now() const r = new Rate() + let clock: FakeTimers.InstalledClock beforeEach(() => { - tk.freeze(now) - // clear() must be called _after_ freezing time + clock = FakeTimers.install({ now: now }) + // clear() must be called _after_ setting up fake timers r.clear() }) - after(() => tk.reset()) + afterEach(() => { + clock.uninstall() + }) function expectRate(rate: Rate, epm: number, tol = 0.1) { expect(rate.eventsPerMs).to.be.withinToleranceOf(epm, tol) @@ -27,7 +29,7 @@ describe("Rate", () => { }) it("maintains a rate of 0 after time with no events", () => { - tk.freeze(now + minuteMs) + clock.tick(minuteMs) expectRate(r, 0) }) @@ -37,38 +39,39 @@ describe("Rate", () => { () => { times(cnt, () => r.onEvent()) expectRate(r, 0) - tk.freeze(now + 100) + clock.tick(100) expectRate(r, 0) - tk.freeze(now + r.warmupMs + 1) + clock.tick(r.warmupMs - 100 + 1) expectRate(r, cnt / r.warmupMs) - tk.freeze(now + 2 * r.warmupMs) + clock.tick(r.warmupMs) expectRate(r, cnt / (2 * r.warmupMs)) - tk.freeze(now + 3 * r.warmupMs) + clock.tick(r.warmupMs) expectRate(r, cnt / (3 * r.warmupMs)) - tk.freeze(now + r.periodMs) + clock.tick(r.periodMs - 3 * r.warmupMs) expectRate(r, 0) - expect(r.msSinceLastEvent).to.eql(minuteMs) + expect(r.msSinceLastEvent).to.be.closeTo(r.periodMs, 5) }, ) } - for (const events of [5, 10, 100, 1000]) { + for (const events of [4, 32, 256, 1024]) { it( "calculates average rate for " + events + " events, and then decays", () => { const period = r.periodMs - times(events, (i) => { - tk.freeze(now + (period * i) / events) + times(events, () => { + clock.tick(r.periodMs / events) r.onEvent() }) + const tickMs = r.periodMs / 4 expectRate(r, events / period, 0.3) - tk.freeze(now + 1.25 * r.periodMs) + clock.tick(tickMs) expectRate(r, 0.75 * (events / period), 0.3) - tk.freeze(now + 1.5 * r.periodMs) + clock.tick(tickMs) expectRate(r, 0.5 * (events / period), 0.3) - tk.freeze(now + 1.75 * r.periodMs) - expectRate(r, 0.25 * (events / period), 0.3) - tk.freeze(now + 2 * r.periodMs) + clock.tick(tickMs) + expectRate(r, 0.25 * (events / period), 0.5) + clock.tick(tickMs) expectRate(r, 0) }, ) diff --git a/src/StreamHandler.spec.ts b/src/StreamHandler.spec.ts new file mode 100644 index 0000000..0d1230a --- /dev/null +++ b/src/StreamHandler.spec.ts @@ -0,0 +1,441 @@ +import child_process from "node:child_process" +import events from "node:events" +import { expect, processFactory } from "./_chai.spec" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { logger } from "./Logger" +import { + StreamContext, + StreamHandler, + StreamHandlerOptions, +} from "./StreamHandler" +import { Task } from "./Task" + +describe("StreamHandler", function () { + let streamHandler: StreamHandler + let emitter: BatchClusterEmitter + let mockContext: StreamContext + let onErrorCalls: { reason: string; error: Error }[] = [] + let endCalls: { gracefully: boolean; reason: string }[] = [] + + const options: StreamHandlerOptions = { + logger, + } + + beforeEach(function () { + emitter = new events.EventEmitter() as BatchClusterEmitter + streamHandler = new StreamHandler(options, emitter) + + onErrorCalls = [] + endCalls = [] + + // Create a mock context that simulates BatchProcess behavior + mockContext = { + name: "TestProcess(12345)", + isEnding: () => false, + getCurrentTask: () => undefined, + onError: (reason: string, error: Error) => { + onErrorCalls.push({ reason, error }) + }, + end: (gracefully: boolean, reason: string) => { + endCalls.push({ gracefully, reason }) + }, + } + }) + + describe("initial state", function () { + it("should initialize correctly", function () { + expect(streamHandler).to.not.be.undefined + + const stats = streamHandler.getStats() + expect(stats.handlerActive).to.be.true + expect(stats.emitterConnected).to.be.true + }) + }) + + describe("stream setup", function () { + let mockProcess: child_process.ChildProcess + + beforeEach(async function () { + // Create a real process for testing stream setup + mockProcess = await processFactory() + }) + + afterEach(function () { + if (mockProcess && !mockProcess.killed) { + mockProcess.kill() + } + }) + + it("should set up stream listeners on a child process", function () { + expect(() => { + streamHandler.setupStreamListeners(mockProcess, mockContext) + }).to.not.throw() + + // Verify streams exist + expect(mockProcess.stdin).to.not.be.null + expect(mockProcess.stdout).to.not.be.null + expect(mockProcess.stderr).to.not.be.null + }) + + it("should throw error if stdin is missing", function () { + const invalidProcess = { stdin: null } as child_process.ChildProcess + + expect(() => { + streamHandler.setupStreamListeners(invalidProcess, mockContext) + }).to.throw("Given proc had no stdin") + }) + + it("should throw error if stdout is missing", function () { + const invalidProcess = { + stdin: { + on: () => { + /* mock implementation */ + }, + }, // Mock stdin with on method + stdout: null, + } as any as child_process.ChildProcess + + expect(() => { + streamHandler.setupStreamListeners(invalidProcess, mockContext) + }).to.throw("Given proc had no stdout") + }) + }) + + describe("stdout processing", function () { + let mockTask: Task + let taskDataEvents: { data: any; task: any; context: any }[] = [] + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = [] + + beforeEach(function () { + taskDataEvents = [] + noTaskDataEvents = [] + + // Set up event listeners + emitter.on("taskData", (data, task, context) => { + taskDataEvents.push({ data, task, context }) + }) + + emitter.on("noTaskData", (stdout, stderr, context) => { + noTaskDataEvents.push({ stdout, stderr, context }) + }) + + // Create a mock task + mockTask = { + pending: true, + onStdout: () => { + /* mock implementation */ + }, + } as unknown as Task + }) + + it("should process stdout data with active task", function () { + mockContext.getCurrentTask = () => mockTask + const testData = "test output" + + streamHandler.processStdout(testData, mockContext) + + expect(taskDataEvents).to.have.length(1) + expect(taskDataEvents[0]?.data).to.eql(testData) + expect(taskDataEvents[0]?.task).to.eql(mockTask) + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should ignore stdout data when process is ending", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => true + const testData = "test output" + + streamHandler.processStdout(testData, mockContext) + + expect(taskDataEvents).to.have.length(0) + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should emit noTaskData and end process for stdout without task", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => false + const testData = "unexpected output" + + streamHandler.processStdout(testData, mockContext) + + expect(taskDataEvents).to.have.length(0) + expect(noTaskDataEvents).to.have.length(1) + expect(noTaskDataEvents[0]?.stdout).to.eql(testData) + expect(noTaskDataEvents[0]?.stderr).to.be.null + expect(endCalls).to.have.length(1) + expect(endCalls[0]?.gracefully).to.be.false + expect(endCalls[0]?.reason).to.eql("stdout.error") + }) + + it("should ignore blank stdout data", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => false + + streamHandler.processStdout("", mockContext) + streamHandler.processStdout(" ", mockContext) + streamHandler.processStdout("\n", mockContext) + + expect(taskDataEvents).to.have.length(0) + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should handle null stdout data", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => false + + streamHandler.processStdout(null as any, mockContext) + + expect(taskDataEvents).to.have.length(0) + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should not process stdout when task is not pending", function () { + const nonPendingTask = { + pending: false, + onStdout: () => { + /* mock implementation */ + }, + } as unknown as Task + + mockContext.getCurrentTask = () => nonPendingTask + mockContext.isEnding = () => false + const testData = "test output" + + streamHandler.processStdout(testData, mockContext) + + expect(taskDataEvents).to.have.length(0) + expect(noTaskDataEvents).to.have.length(1) + expect(endCalls).to.have.length(1) + expect(endCalls[0]?.reason).to.eql("stdout.error") + }) + }) + + describe("stderr processing", function () { + let mockTask: Task + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = [] + + beforeEach(function () { + noTaskDataEvents = [] + + // Set up event listeners + emitter.on("noTaskData", (stdout, stderr, context) => { + noTaskDataEvents.push({ stdout, stderr, context }) + }) + + // Create a mock task + mockTask = { + pending: true, + onStderr: () => { + /* mock implementation */ + }, + } as unknown as Task + }) + + it("should process stderr data with active task", function () { + mockContext.getCurrentTask = () => mockTask + const testData = "error output" + + streamHandler.processStderr(testData, mockContext) + + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should ignore stderr data when process is ending", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => true + const testData = "error output" + + streamHandler.processStderr(testData, mockContext) + + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should emit noTaskData and end process for stderr without task", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => false + const testData = "unexpected error" + + streamHandler.processStderr(testData, mockContext) + + expect(noTaskDataEvents).to.have.length(1) + expect(noTaskDataEvents[0]?.stdout).to.be.null + expect(noTaskDataEvents[0]?.stderr).to.eql(testData) + expect(endCalls).to.have.length(1) + expect(endCalls[0]?.gracefully).to.be.false + expect(endCalls[0]?.reason).to.eql("stderr") + }) + + it("should ignore blank stderr data", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => false + + streamHandler.processStderr("", mockContext) + streamHandler.processStderr(" ", mockContext) + streamHandler.processStderr("\n", mockContext) + + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should not process stderr when task is not pending", function () { + const nonPendingTask = { + pending: false, + onStderr: () => { + /* mock implementation */ + }, + } as unknown as Task + + mockContext.getCurrentTask = () => nonPendingTask + mockContext.isEnding = () => false + const testData = "error output" + + streamHandler.processStderr(testData, mockContext) + + expect(noTaskDataEvents).to.have.length(1) + expect(endCalls).to.have.length(1) + expect(endCalls[0]?.reason).to.eql("stderr") + }) + }) + + describe("utility methods", function () { + it("should correctly identify blank data", function () { + expect(streamHandler.isBlankData("")).to.be.true + expect(streamHandler.isBlankData(" ")).to.be.true + expect(streamHandler.isBlankData("\n")).to.be.true + expect(streamHandler.isBlankData("\t")).to.be.true + expect(streamHandler.isBlankData(null)).to.be.true + expect(streamHandler.isBlankData(undefined)).to.be.true + + expect(streamHandler.isBlankData("text")).to.be.false + expect(streamHandler.isBlankData(" text ")).to.be.false + expect(streamHandler.isBlankData(Buffer.from("data"))).to.be.false + }) + + it("should provide handler statistics", function () { + const stats = streamHandler.getStats() + + expect(stats).to.have.property("handlerActive") + expect(stats).to.have.property("emitterConnected") + expect(stats.handlerActive).to.be.true + expect(stats.emitterConnected).to.be.true + }) + }) + + describe("buffer handling", function () { + let mockTask: Task + let taskDataEvents: { data: any; task: any; context: any }[] = [] + + beforeEach(function () { + taskDataEvents = [] + + emitter.on("taskData", (data, task, context) => { + taskDataEvents.push({ data, task, context }) + }) + + mockTask = { + pending: true, + onStdout: () => { + /* mock implementation */ + }, + onStderr: () => { + /* mock implementation */ + }, + } as unknown as Task + }) + + it("should handle Buffer data in stdout", function () { + mockContext.getCurrentTask = () => mockTask + const bufferData = Buffer.from("test buffer data") + + streamHandler.processStdout(bufferData, mockContext) + + expect(taskDataEvents).to.have.length(1) + expect(taskDataEvents[0]?.data).to.eql(bufferData) + }) + + it("should handle Buffer data in stderr", function () { + mockContext.getCurrentTask = () => mockTask + const bufferData = Buffer.from("error buffer data") + + // Should not throw and should process normally + expect(() => { + streamHandler.processStderr(bufferData, mockContext) + }).to.not.throw() + }) + }) + + describe("integration scenarios", function () { + let mockTask: Task + let taskDataEvents: { data: any; task: any; context: any }[] = [] + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = [] + + beforeEach(function () { + taskDataEvents = [] + noTaskDataEvents = [] + + emitter.on("taskData", (data, task, context) => { + taskDataEvents.push({ data, task, context }) + }) + + emitter.on("noTaskData", (stdout, stderr, context) => { + noTaskDataEvents.push({ stdout, stderr, context }) + }) + + mockTask = { + pending: true, + onStdout: () => { + /* mock implementation */ + }, + onStderr: () => { + /* mock implementation */ + }, + } as unknown as Task + }) + + it("should handle mixed stdout and stderr with active task", function () { + mockContext.getCurrentTask = () => mockTask + + streamHandler.processStdout("stdout data", mockContext) + streamHandler.processStderr("stderr data", mockContext) + + expect(taskDataEvents).to.have.length(1) + expect(taskDataEvents[0]?.data).to.eql("stdout data") + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + + it("should handle task completion scenario", function () { + // Start with active task + mockContext.getCurrentTask = () => mockTask + streamHandler.processStdout("initial output", mockContext) + + expect(taskDataEvents).to.have.length(1) + + // Task completes, no current task + mockContext.getCurrentTask = () => undefined + streamHandler.processStdout("stray output", mockContext) + + expect(noTaskDataEvents).to.have.length(1) + expect(endCalls).to.have.length(1) + expect(endCalls[0]?.reason).to.eql("stdout.error") + }) + + it("should handle process ending scenario", function () { + mockContext.getCurrentTask = () => undefined + mockContext.isEnding = () => true + + streamHandler.processStdout("final output", mockContext) + streamHandler.processStderr("final error", mockContext) + + expect(taskDataEvents).to.have.length(0) + expect(noTaskDataEvents).to.have.length(0) + expect(endCalls).to.have.length(0) + }) + }) +}) diff --git a/src/StreamHandler.ts b/src/StreamHandler.ts new file mode 100644 index 0000000..40e9fdd --- /dev/null +++ b/src/StreamHandler.ts @@ -0,0 +1,133 @@ +import child_process from "node:child_process" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { Logger } from "./Logger" +import { map } from "./Object" +import { blank } from "./String" +import { Task } from "./Task" + +/** + * Configuration for stream handling behavior + */ +export interface StreamHandlerOptions { + readonly logger: () => Logger +} + +/** + * Interface for objects that can provide stream context + */ +export interface StreamContext { + readonly name: string + isEnding(): boolean + getCurrentTask(): Task | undefined + onError: (reason: string, error: Error) => void + end: (gracefully: boolean, reason: string) => void +} + +/** + * Handles stdout/stderr stream processing for child processes. + * Manages stream event listeners, data routing, and error handling. + */ +export class StreamHandler { + readonly #logger: () => Logger + + constructor( + options: StreamHandlerOptions, + private readonly emitter: BatchClusterEmitter, + ) { + this.#logger = options.logger + } + + /** + * Set up stream event listeners for a child process + */ + setupStreamListeners( + proc: child_process.ChildProcess, + context: StreamContext, + ): void { + const stdin = proc.stdin + if (stdin == null) throw new Error("Given proc had no stdin") + stdin.on("error", (err) => context.onError("stdin.error", err)) + + const stdout = proc.stdout + if (stdout == null) throw new Error("Given proc had no stdout") + stdout.on("error", (err) => context.onError("stdout.error", err)) + stdout.on("data", (data: string | Buffer) => this.#onStdout(data, context)) + + map(proc.stderr, (stderr) => { + stderr.on("error", (err) => context.onError("stderr.error", err)) + stderr.on("data", (data: string | Buffer) => + this.#onStderr(data, context), + ) + }) + } + + /** + * Handle stdout data from a child process + */ + #onStdout(data: string | Buffer, context: StreamContext): void { + if (data == null) return + + const task = context.getCurrentTask() + if (task != null && task.pending) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + this.emitter.emit("taskData", data, task, context as any) + task.onStdout(data) + } else if (context.isEnding()) { + // don't care if we're already being shut down. + } else if (!blank(data)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + this.emitter.emit("noTaskData", data, null, context as any) + context.end(false, "stdout.error") + } + } + + /** + * Handle stderr data from a child process + */ + #onStderr(data: string | Buffer, context: StreamContext): void { + if (blank(data)) return + + this.#logger().warn(context.name + ".onStderr(): " + String(data)) + + const task = context.getCurrentTask() + if (task != null && task.pending) { + task.onStderr(data) + } else if (!context.isEnding()) { + // If we're ending and there isn't a task, don't worry about it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + this.emitter.emit("noTaskData", null, data, context as any) + context.end(false, "stderr") + } + } + + /** + * Process stdout data directly (for testing or manual processing) + */ + processStdout(data: string | Buffer, context: StreamContext): void { + this.#onStdout(data, context) + } + + /** + * Process stderr data directly (for testing or manual processing) + */ + processStderr(data: string | Buffer, context: StreamContext): void { + this.#onStderr(data, context) + } + + /** + * Check if data is considered blank/empty + */ + isBlankData(data: string | Buffer | null | undefined): boolean { + return blank(data) + } + + /** + * Get stream handler statistics + */ + getStats() { + return { + handlerActive: true, + emitterConnected: this.emitter != null, + } + } +} diff --git a/src/String.ts b/src/String.ts index 0732d07..6c9b2d1 100644 --- a/src/String.ts +++ b/src/String.ts @@ -1,17 +1,21 @@ -import { isFunction } from "./Object" - -export function blank(s: string | Buffer | undefined): boolean { - return s == null || String(s).trim().length === 0 +export function blank(s: unknown): boolean { + return s == null || toS(s).trim().length === 0 } -export function notBlank(s: string | undefined): s is string { +export function notBlank(s: unknown): boolean { return !blank(s) } +export function toNotBlank(s: unknown): string | undefined { + const result = toS(s).trim() + return result.length === 0 ? undefined : result +} + export function ensureSuffix(s: string, suffix: string): string { return s.endsWith(suffix) ? s : s + suffix } -export function toS(s: any): string { - return s == null ? "" : isFunction(s.toString) ? s.toString() : String(s) +export function toS(s: unknown): string { + /* eslint-disable-next-line @typescript-eslint/no-base-to-string */ + return s == null ? "" : s.toString() } diff --git a/src/Task.ts b/src/Task.ts index 58a591c..ddd5bc3 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -15,7 +15,7 @@ let _taskId = 1 * instance has a promise that will be resolved or rejected based on the * result of the task. */ -export class Task { +export class Task { readonly taskId = _taskId++ #opts?: TaskOptions #startedAt?: number @@ -89,13 +89,13 @@ export class Task { if (passRE != null && passRE.exec(this.#stdout) != null) { // remove the pass token from stdout: this.#stdout = this.#stdout.replace(passRE, "") - this.#resolve(true) + void this.#resolve(true) } else { const failRE = this.#opts?.failRE if (failRE != null && failRE.exec(this.#stdout) != null) { // remove the fail token from stdout: this.#stdout = this.#stdout.replace(failRE, "") - this.#resolve(false) + void this.#resolve(false) } } } @@ -106,7 +106,7 @@ export class Task { if (failRE != null && failRE.exec(this.#stderr) != null) { // remove the fail token from stderr: this.#stderr = this.#stderr.replace(failRE, "") - this.#resolve(false) + void this.#resolve(false) } } @@ -146,14 +146,15 @@ export class Task { try { const parseResult = await this.parser(this.#stdout, this.#stderr, passed) if (this.#d.resolve(parseResult)) { + // success } else { this.#opts?.observer.emit( "internalError", new Error(this.toString() + " ._resolved() more than once"), ) } - } catch (error: any) { - this.reject(error) + } catch (error: unknown) { + this.reject(error instanceof Error ? error : new Error(String(error))) } } } diff --git a/src/TaskQueueManager.spec.ts b/src/TaskQueueManager.spec.ts new file mode 100644 index 0000000..2632760 --- /dev/null +++ b/src/TaskQueueManager.spec.ts @@ -0,0 +1,263 @@ +import events from "node:events" +import { expect, parser } from "./_chai.spec" +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { BatchProcess } from "./BatchProcess" +import { logger } from "./Logger" +import { Task } from "./Task" +import { TaskQueueManager } from "./TaskQueueManager" + +describe("TaskQueueManager", function () { + let queueManager: TaskQueueManager + let emitter: BatchClusterEmitter + let mockProcess: BatchProcess + + beforeEach(function () { + emitter = new events.EventEmitter() as BatchClusterEmitter + queueManager = new TaskQueueManager(logger, emitter) + + // Create a mock process that can execute tasks + mockProcess = { + ready: true, + idle: true, + pid: 12345, + execTask: () => true, // Always succeed + } as unknown as BatchProcess + }) + + describe("initial state", function () { + it("should start with empty queue", function () { + expect(queueManager.pendingTaskCount).to.eql(0) + expect(queueManager.isEmpty).to.be.true + expect(queueManager.pendingTasks).to.eql([]) + }) + + it("should return empty queue stats", function () { + const stats = queueManager.getQueueStats() + expect(stats.pendingTaskCount).to.eql(0) + expect(stats.isEmpty).to.be.true + }) + }) + + describe("task enqueuing", function () { + it("should enqueue tasks when not ended", function () { + const task = new Task("test command", parser) + const promise = queueManager.enqueueTask(task, false) + + expect(queueManager.pendingTaskCount).to.eql(1) + expect(queueManager.isEmpty).to.be.false + expect(queueManager.pendingTasks).to.have.length(1) + expect(queueManager.pendingTasks[0]).to.eql(task) + expect(promise).to.equal(task.promise) + }) + + it("should reject tasks when ended", function () { + const task = new Task("test command", parser) + const promise = queueManager.enqueueTask(task, true) + + expect(queueManager.pendingTaskCount).to.eql(0) + expect(queueManager.isEmpty).to.be.true + expect(promise).to.equal(task.promise) + expect(task.pending).to.be.false + }) + + it("should handle multiple tasks", function () { + const task1 = new Task("command 1", parser) + const task2 = new Task("command 2", parser) + const task3 = new Task("command 3", parser) + + queueManager.enqueueTask(task1, false) + queueManager.enqueueTask(task2, false) + queueManager.enqueueTask(task3, false) + + expect(queueManager.pendingTaskCount).to.eql(3) + expect(queueManager.pendingTasks).to.have.length(3) + }) + }) + + describe("task assignment", function () { + let task: Task + + beforeEach(function () { + task = new Task("test command", parser) + queueManager.enqueueTask(task, false) + }) + + it("should assign task to ready process", function () { + const result = queueManager.tryAssignNextTask(mockProcess) + + expect(result).to.be.true + expect(queueManager.pendingTaskCount).to.eql(0) + expect(queueManager.isEmpty).to.be.true + }) + + it("should not assign task when no ready process", function () { + const result = queueManager.tryAssignNextTask(undefined) + + expect(result).to.be.false + expect(queueManager.pendingTaskCount).to.eql(1) + expect(queueManager.isEmpty).to.be.false + }) + + it("should retry when process cannot execute task", function () { + const failingProcess = { + ...mockProcess, + execTask: () => false, // Always fail + } as unknown as BatchProcess + + const result = queueManager.tryAssignNextTask(failingProcess) + + expect(result).to.be.false + expect(queueManager.pendingTaskCount).to.eql(1) // Task should be re-queued + }) + + it("should stop retrying after max retries", function () { + const failingProcess = { + ...mockProcess, + execTask: () => false, + } as unknown as BatchProcess + + const result = queueManager.tryAssignNextTask(failingProcess, 0) + + expect(result).to.be.false + expect(queueManager.pendingTaskCount).to.eql(1) // Task remains when retries exhausted + }) + + it("should handle empty queue gracefully", function () { + // Clear the queue first + queueManager.clearAllTasks() + + const result = queueManager.tryAssignNextTask(mockProcess) + + expect(result).to.be.false + expect(queueManager.pendingTaskCount).to.eql(0) + }) + }) + + describe("queue processing", function () { + beforeEach(function () { + // Add multiple tasks + for (let i = 0; i < 5; i++) { + const task = new Task(`command ${i}`, parser) + queueManager.enqueueTask(task, false) + } + }) + + it("should process all tasks when process is always ready", function () { + const findReadyProcess = () => mockProcess + const assignedCount = queueManager.processQueue(findReadyProcess) + + expect(assignedCount).to.eql(5) + expect(queueManager.pendingTaskCount).to.eql(0) + expect(queueManager.isEmpty).to.be.true + }) + + it("should stop processing when no ready process available", function () { + const findReadyProcess = () => undefined + const assignedCount = queueManager.processQueue(findReadyProcess) + + expect(assignedCount).to.eql(0) + expect(queueManager.pendingTaskCount).to.eql(5) + expect(queueManager.isEmpty).to.be.false + }) + + it("should partially process queue when process becomes unavailable", function () { + let callCount = 0 + const findReadyProcess = () => { + callCount++ + return callCount <= 3 ? mockProcess : undefined + } + + const assignedCount = queueManager.processQueue(findReadyProcess) + + expect(assignedCount).to.eql(3) + expect(queueManager.pendingTaskCount).to.eql(2) + }) + + it("should handle process that fails to execute tasks", function () { + const failingProcess = { + ...mockProcess, + execTask: () => false, + } as unknown as BatchProcess + + const findReadyProcess = () => failingProcess + const assignedCount = queueManager.processQueue(findReadyProcess) + + expect(assignedCount).to.eql(0) + expect(queueManager.pendingTaskCount).to.be.greaterThan(0) // Tasks remain queued + }) + }) + + describe("queue management", function () { + beforeEach(function () { + // Add some tasks + for (let i = 0; i < 3; i++) { + const task = new Task(`command ${i}`, parser) + queueManager.enqueueTask(task, false) + } + }) + + it("should clear all tasks", function () { + expect(queueManager.pendingTaskCount).to.eql(3) + + queueManager.clearAllTasks() + + expect(queueManager.pendingTaskCount).to.eql(0) + expect(queueManager.isEmpty).to.be.true + expect(queueManager.pendingTasks).to.eql([]) + }) + + it("should provide accurate queue statistics", function () { + const stats = queueManager.getQueueStats() + + expect(stats.pendingTaskCount).to.eql(3) + expect(stats.isEmpty).to.be.false + }) + }) + + describe("error handling", function () { + it("should handle concurrent access gracefully", function () { + // Add a task + const task = new Task("test", parser) + queueManager.enqueueTask(task, false) + + // First process gets the task + const result1 = queueManager.tryAssignNextTask(mockProcess) + expect(result1).to.be.true + + // Second attempt on empty queue should return false + const result2 = queueManager.tryAssignNextTask(mockProcess) + expect(result2).to.be.false + expect(queueManager.pendingTaskCount).to.eql(0) + }) + }) + + describe("FIFO ordering", function () { + it("should process tasks in first-in-first-out order", function () { + const executedTasks: Task[] = [] + const trackingProcess = { + ...mockProcess, + execTask: (task: Task) => { + executedTasks.push(task) + return true + }, + } as unknown as BatchProcess + + // Enqueue tasks with identifiable commands + const task1 = new Task("first", parser) + const task2 = new Task("second", parser) + const task3 = new Task("third", parser) + + queueManager.enqueueTask(task1, false) + queueManager.enqueueTask(task2, false) + queueManager.enqueueTask(task3, false) + + // Process all tasks + queueManager.processQueue(() => trackingProcess) + + expect(executedTasks).to.have.length(3) + expect(executedTasks[0]?.command).to.eql("first") + expect(executedTasks[1]?.command).to.eql("second") + expect(executedTasks[2]?.command).to.eql("third") + }) + }) +}) diff --git a/src/TaskQueueManager.ts b/src/TaskQueueManager.ts new file mode 100644 index 0000000..3412e96 --- /dev/null +++ b/src/TaskQueueManager.ts @@ -0,0 +1,141 @@ +import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { BatchProcess } from "./BatchProcess" +import { Logger } from "./Logger" +import { Task } from "./Task" + +/** + * Manages task queuing, scheduling, and assignment to ready processes. + * Handles the task lifecycle from enqueue to assignment. + */ +export class TaskQueueManager { + readonly #tasks: Task[] = [] + readonly #logger: () => Logger + + constructor( + logger: () => Logger, + private readonly emitter?: BatchClusterEmitter, + ) { + this.#logger = logger + } + + /** + * Add a task to the queue for processing + */ + enqueueTask(task: Task, ended: boolean): Promise { + if (ended) { + task.reject( + new Error("BatchCluster has ended, cannot enqueue " + task.command), + ) + } else { + this.#tasks.push(task as Task) + } + return task.promise + } + + /** + * Simple enqueue method (alias for enqueueTask without ended check) + */ + enqueue(task: Task): void { + this.#tasks.push(task) + } + + /** + * Get the number of pending tasks in the queue + */ + get pendingTaskCount(): number { + return this.#tasks.length + } + + /** + * Get all pending tasks (mostly for testing) + */ + get pendingTasks(): readonly Task[] { + return this.#tasks + } + + /** + * Check if the queue is empty + */ + get isEmpty(): boolean { + return this.#tasks.length === 0 + } + + /** + * Attempt to assign the next task to a ready process. + * Returns true if a task was successfully assigned. + */ + tryAssignNextTask( + readyProcess: BatchProcess | undefined, + retries = 1, + ): boolean { + if (this.#tasks.length === 0 || retries < 0) { + return false + } + + // no procs are idle and healthy :( + if (readyProcess == null) { + return false + } + + const task = this.#tasks.shift() + if (task == null) { + this.emitter?.emit("internalError", new Error("unexpected null task")) + return false + } + + const submitted = readyProcess.execTask(task) + if (!submitted) { + // This isn't an internal error: the proc may have needed to run a health + // check. Let's reschedule the task and try again: + this.#tasks.push(task) + // We don't want to return false here (it'll stop the assignment loop) unless + // we actually can't submit the task: + return this.tryAssignNextTask(readyProcess, retries - 1) + } + + this.#logger().trace( + "TaskQueueManager.tryAssignNextTask(): submitted task", + { + child_pid: readyProcess.pid, + task, + }, + ) + + return submitted + } + + /** + * Process all pending tasks by assigning them to ready processes. + * Returns the number of tasks successfully assigned. + */ + processQueue(findReadyProcess: () => BatchProcess | undefined): number { + let assignedCount = 0 + + while (this.#tasks.length > 0) { + const readyProcess = findReadyProcess() + if (!this.tryAssignNextTask(readyProcess)) { + break + } + assignedCount++ + } + + return assignedCount + } + + /** + * Clear all pending tasks (used during shutdown) + */ + clearAllTasks(): void { + this.#tasks.length = 0 + } + + /** + * Get statistics about task assignment and queue state + */ + getQueueStats() { + return { + pendingTaskCount: this.#tasks.length, + isEmpty: this.isEmpty, + } + } +} diff --git a/src/Timeout.ts b/src/Timeout.ts index 318ba65..94a16c9 100644 --- a/src/Timeout.ts +++ b/src/Timeout.ts @@ -9,26 +9,30 @@ export async function thenOrTimeout( // something else in that case? return timeoutMs <= 1 ? p - : new Promise(async (resolve, reject) => { + : new Promise((resolve, reject) => { let pending = true - try { - const t = timers.setTimeout(() => { - if (pending) { - pending = false - resolve(Timeout) - } - }, timeoutMs) - const result = await p + const t = timers.setTimeout(() => { if (pending) { pending = false - clearTimeout(t) - resolve(result) + resolve(Timeout) } - } catch (err) { - if (pending) { - pending = false - reject(err) - } - } + }, timeoutMs) + + p.then( + (result) => { + if (pending) { + pending = false + clearTimeout(t) + resolve(result) + } + }, + (err: unknown) => { + if (pending) { + pending = false + clearTimeout(t) + reject(err instanceof Error ? err : new Error(String(err))) + } + }, + ) }) } diff --git a/src/WhyNotHealthy.ts b/src/WhyNotHealthy.ts new file mode 100644 index 0000000..4bdb103 --- /dev/null +++ b/src/WhyNotHealthy.ts @@ -0,0 +1,25 @@ +/** + * Reasons why a BatchProcess might not be healthy + */ +export type WhyNotHealthy = + | "broken" + | "closed" + | "ending" + | "ended" + | "idle" + | "old" + | "proc.close" + | "proc.disconnect" + | "proc.error" + | "proc.exit" + | "stderr.error" + | "stderr" + | "stdin.error" + | "stdout.error" + | "timeout" + | "tooMany" // < only sent by BatchCluster when maxProcs is reduced + | "startError" + | "unhealthy" + | "worn" + +export type WhyNotReady = WhyNotHealthy | "busy" diff --git a/src/_chai.spec.ts b/src/_chai.spec.ts index e761fce..e52fe78 100644 --- a/src/_chai.spec.ts +++ b/src/_chai.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ try { require("source-map-support").install() } catch { @@ -26,13 +27,11 @@ setLogger( Log.withTimestamps( Log.filterLevels( { - // tslint:disable: no-unbound-method trace: console.log, debug: console.log, info: console.log, warn: console.warn, error: console.error, - // tslint:enable: no-unbound-method }, (process.env.LOG as any) ?? "error", ), diff --git a/src/test-helpers.ts b/src/test-helpers.ts new file mode 100644 index 0000000..f256031 --- /dev/null +++ b/src/test-helpers.ts @@ -0,0 +1 @@ +export const ErrorPrefix = "ERROR: " diff --git a/src/test.spec.ts b/src/test.spec.ts index a90d21e..98b89e8 100644 --- a/src/test.spec.ts +++ b/src/test.spec.ts @@ -10,8 +10,6 @@ import { setRngseed, } from "./_chai.spec" -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - describe("test.js", () => { class Harness { readonly child: child_process.ChildProcess @@ -32,18 +30,18 @@ describe("test.js", () => { } async end(): Promise { this.child.stdin!.end(null) - await until(() => this.running().then((ea) => !ea), 1000) + await until(() => this.notRunning(), 1000) if (await this.running()) { console.error("Ack, I had to kill child pid " + this.child.pid) kill(this.child.pid) } return } - async running(): Promise { + running(): boolean { return pidExists(this.child.pid) } - async notRunning(): Promise { - return this.running().then((ea) => !ea) + notRunning(): boolean { + return !this.running() } async assertStdout(f: (output: string) => void) { // The OS may take a bit before the PID shows up in the process table: diff --git a/src/test.ts b/src/test.ts index fe985e9..afee866 100644 --- a/src/test.ts +++ b/src/test.ts @@ -40,7 +40,8 @@ function toF(s: string | undefined) { const failrate = toF(process.env.failrate) ?? 0 const rng = process.env.rngseed != null - ? require("seedrandom")(process.env.rngseed) + ? // eslint-disable-next-line @typescript-eslint/no-require-imports + require("seedrandom")(process.env.rngseed) : Math.random async function onLine(line: string): Promise { @@ -148,5 +149,6 @@ async function onLine(line: string): Promise { const m = new Mutex() process.stdin + // eslint-disable-next-line @typescript-eslint/no-require-imports .pipe(require("split2")()) .on("data", (ea: string) => m.serial(() => onLine(ea)))