From 6d8f8b608471329b765cb99b2e9cb0be6b27e26e Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 28 May 2025 10:19:53 -0700 Subject: [PATCH 01/60] feat(docs): switch to GHA --- .claude/settings.local.json | 5 +- .github/workflows/docs.yml | 66 + .gitignore | 1 + .typedoc.js | 8 +- docs/.nojekyll | 1 - docs/assets/highlight.css | 57 - docs/assets/icons.js | 15 - docs/assets/icons.svg | 1 - docs/assets/main.js | 59 - docs/assets/navigation.js | 1 - docs/assets/search.js | 1 - docs/assets/style.css | 1412 ---------------------- docs/classes/BatchCluster.html | 63 - docs/classes/BatchClusterOptions.html | 95 -- docs/classes/BatchProcess.html | 58 - docs/classes/Deferred.html | 21 - docs/classes/Rate.html | 20 - docs/classes/Task.html | 27 - docs/functions/SimpleParser.html | 9 - docs/functions/kill.html | 5 - docs/functions/logger-1.html | 1 - docs/functions/pidExists.html | 4 - docs/functions/pids.html | 3 - docs/functions/setLogger.html | 1 - docs/index.html | 69 -- docs/interfaces/BatchClusterEvents.html | 37 - docs/interfaces/BatchProcessOptions.html | 24 - docs/interfaces/ChildProcessFactory.html | 9 - docs/interfaces/Logger.html | 7 - docs/interfaces/Parser.html | 12 - docs/interfaces/TypedEventEmitter.html | 7 - docs/modules.html | 27 - docs/serve.json | 3 - docs/types/BatchClusterEmitter.html | 18 - docs/types/ChildExitReason.html | 1 - docs/types/WhyNotHealthy.html | 1 - docs/types/WhyNotReady.html | 1 - docs/variables/ConsoleLogger.html | 12 - docs/variables/Log.html | 1 - docs/variables/LogLevels.html | 1 - docs/variables/NoLogger.html | 2 - package.json | 8 +- 42 files changed, 75 insertions(+), 2099 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/.nojekyll delete mode 100644 docs/assets/highlight.css delete mode 100644 docs/assets/icons.js delete mode 100644 docs/assets/icons.svg delete mode 100644 docs/assets/main.js delete mode 100644 docs/assets/navigation.js delete mode 100644 docs/assets/search.js delete mode 100644 docs/assets/style.css delete mode 100644 docs/classes/BatchCluster.html delete mode 100644 docs/classes/BatchClusterOptions.html delete mode 100644 docs/classes/BatchProcess.html delete mode 100644 docs/classes/Deferred.html delete mode 100644 docs/classes/Rate.html delete mode 100644 docs/classes/Task.html delete mode 100644 docs/functions/SimpleParser.html delete mode 100644 docs/functions/kill.html delete mode 100644 docs/functions/logger-1.html delete mode 100644 docs/functions/pidExists.html delete mode 100644 docs/functions/pids.html delete mode 100644 docs/functions/setLogger.html delete mode 100644 docs/index.html delete mode 100644 docs/interfaces/BatchClusterEvents.html delete mode 100644 docs/interfaces/BatchProcessOptions.html delete mode 100644 docs/interfaces/ChildProcessFactory.html delete mode 100644 docs/interfaces/Logger.html delete mode 100644 docs/interfaces/Parser.html delete mode 100644 docs/interfaces/TypedEventEmitter.html delete mode 100644 docs/modules.html delete mode 100644 docs/serve.json delete mode 100644 docs/types/BatchClusterEmitter.html delete mode 100644 docs/types/ChildExitReason.html delete mode 100644 docs/types/WhyNotHealthy.html delete mode 100644 docs/types/WhyNotReady.html delete mode 100644 docs/variables/ConsoleLogger.html delete mode 100644 docs/variables/Log.html delete mode 100644 docs/variables/LogLevels.html delete mode 100644 docs/variables/NoLogger.html diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0d477ef..4b46733 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -17,7 +17,10 @@ "Bash(find:*)", "Bash(/usr/bin/rg -n \"onStdout|onStderr\" BatchProcess.ts)", "Bash(timeout 45s npm run test:compile)", - "Bash(timeout:*)" + "Bash(timeout:*)", + "Bash(gh run view:*)", + "Bash(mkdir:*)", + "Bash(npm run docs:build:*)" ], "deny": [] } diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..50a5a8c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,66 @@ +name: Docs + +on: + push: + branches: [main] + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Generate documentation + run: npm run docs:build + + - name: Prepare docs for deployment + run: | + cp .serve.json docs/serve.json + touch docs/.nojekyll + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "docs" + + # Deploy job + deploy: + # Add a dependency to the build job + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index f10c721..6d01c21 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.log coverage/ dist +docs/ node_modules npm-debug.log* yarn.lock \ No newline at end of file diff --git a/.typedoc.js b/.typedoc.js index ca78c6c..20e45b2 100644 --- a/.typedoc.js +++ b/.typedoc.js @@ -2,16 +2,10 @@ module.exports = { name: "batch-cluster", out: "./docs/", readme: "./README.md", - includes: "./src", gitRevision: "main", // < prevents docs from changing after every commit exclude: ["**/*test*", "**/*spec*"], excludePrivate: true, entryPoints: [ "./src/BatchCluster.ts", - // "./src/BatchClusterOptions.ts", - // "./src/BatchProcessOptions.ts", - // "./src/Logger.ts", - // "./src/Task.ts", - ], - + ] } diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e2ac661..0000000 --- a/docs/.nojekyll +++ /dev/null @@ -1 +0,0 @@ -TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/docs/assets/highlight.css b/docs/assets/highlight.css deleted file mode 100644 index e8daf4c..0000000 --- a/docs/assets/highlight.css +++ /dev/null @@ -1,57 +0,0 @@ -:root { - --light-hl-0: #795E26; - --dark-hl-0: #DCDCAA; - --light-hl-1: #000000; - --dark-hl-1: #D4D4D4; - --light-hl-2: #A31515; - --dark-hl-2: #CE9178; - --light-hl-3: #008000; - --dark-hl-3: #6A9955; - --light-hl-4: #0000FF; - --dark-hl-4: #569CD6; - --light-code-background: #FFFFFF; - --dark-code-background: #1E1E1E; -} - -@media (prefers-color-scheme: light) { :root { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --hl-2: var(--light-hl-2); - --hl-3: var(--light-hl-3); - --hl-4: var(--light-hl-4); - --code-background: var(--light-code-background); -} } - -@media (prefers-color-scheme: dark) { :root { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --hl-2: var(--dark-hl-2); - --hl-3: var(--dark-hl-3); - --hl-4: var(--dark-hl-4); - --code-background: var(--dark-code-background); -} } - -:root[data-theme='light'] { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --hl-2: var(--light-hl-2); - --hl-3: var(--light-hl-3); - --hl-4: var(--light-hl-4); - --code-background: var(--light-code-background); -} - -:root[data-theme='dark'] { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --hl-2: var(--dark-hl-2); - --hl-3: var(--dark-hl-3); - --hl-4: var(--dark-hl-4); - --code-background: var(--dark-code-background); -} - -.hl-0 { color: var(--hl-0); } -.hl-1 { color: var(--hl-1); } -.hl-2 { color: var(--hl-2); } -.hl-3 { color: var(--hl-3); } -.hl-4 { color: var(--hl-4); } -pre, code { background: var(--code-background); } diff --git a/docs/assets/icons.js b/docs/assets/icons.js deleted file mode 100644 index b79c9e8..0000000 --- a/docs/assets/icons.js +++ /dev/null @@ -1,15 +0,0 @@ -(function(svg) { - svg.innerHTML = ``; - svg.style.display = 'none'; - if (location.protocol === 'file:') { - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', updateUseElements); - else updateUseElements() - function updateUseElements() { - document.querySelectorAll('use').forEach(el => { - if (el.getAttribute('href').includes('#icon-')) { - el.setAttribute('href', el.getAttribute('href').replace(/.*#/, '#')); - } - }); - } - } -})(document.body.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))) \ No newline at end of file diff --git a/docs/assets/icons.svg b/docs/assets/icons.svg deleted file mode 100644 index 7dead61..0000000 --- a/docs/assets/icons.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/assets/main.js b/docs/assets/main.js deleted file mode 100644 index d6f1388..0000000 --- a/docs/assets/main.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -"use strict";(()=>{var Ce=Object.create;var ne=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Oe=Object.getOwnPropertyNames;var _e=Object.getPrototypeOf,Re=Object.prototype.hasOwnProperty;var Me=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Fe=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Oe(e))!Re.call(t,i)&&i!==n&&ne(t,i,{get:()=>e[i],enumerable:!(r=Pe(e,i))||r.enumerable});return t};var De=(t,e,n)=>(n=t!=null?Ce(_e(t)):{},Fe(e||!t||!t.__esModule?ne(n,"default",{value:t,enumerable:!0}):n,t));var ae=Me((se,oe)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,u],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. -`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ol?d+=2:a==l&&(n+=r[u+1]*i[d+1],u+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}s.str.length==1&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),y=s.str.charAt(1),p;y in s.node.edges?p=s.node.edges[y]:(p=new t.TokenSet,s.node.edges[y]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),l=0;l1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof se=="object"?oe.exports=n():e.lunr=n()}(this,function(){return t})})()});var re=[];function G(t,e){re.push({selector:e,constructor:t})}var U=class{constructor(){this.alwaysVisibleMember=null;this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){re.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(console.log("Show page"),document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){console.log("Scorlling");let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!e.checkVisibility()){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(n&&n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let r=document.createElement("p");r.classList.add("warning"),r.textContent="This member is normally hidden due to your filter settings.",n.prepend(r)}}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent="Copied!",e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent="Copy"},100)},1e3)})})}};var ie=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var de=De(ae());async function le(t,e){if(!window.searchData)return;let n=await fetch(window.searchData),r=new Blob([await n.arrayBuffer()]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();t.data=i,t.index=de.Index.load(i.index),e.classList.remove("loading"),e.classList.add("ready")}function he(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:t.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{le(e,t)}),le(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");let s=!1;i.addEventListener("mousedown",()=>s=!0),i.addEventListener("mouseup",()=>{s=!1,t.classList.remove("has-focus")}),r.addEventListener("focus",()=>t.classList.add("has-focus")),r.addEventListener("blur",()=>{s||(s=!1,t.classList.remove("has-focus"))}),Ae(t,i,r,e)}function Ae(t,e,n,r){n.addEventListener("input",ie(()=>{Ve(t,e,n,r)},200));let i=!1;n.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ne(e,n):s.key=="Escape"?n.blur():s.key=="ArrowUp"?ue(e,-1):s.key==="ArrowDown"?ue(e,1):i=!1}),n.addEventListener("keypress",s=>{i&&s.preventDefault()}),document.body.addEventListener("keydown",s=>{s.altKey||s.ctrlKey||s.metaKey||!n.matches(":focus")&&s.key==="/"&&(n.focus(),s.preventDefault())})}function Ve(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=ce(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` - ${ce(l.parent,i)}.${d}`);let y=document.createElement("li");y.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=u+d,y.append(p),e.appendChild(y)}}function ue(t,e){let n=t.querySelector(".current");if(!n)n=t.querySelector(e==1?"li:first-child":"li:last-child"),n&&n.classList.add("current");else{let r=n;if(e===1)do r=r.nextElementSibling??void 0;while(r instanceof HTMLElement&&r.offsetParent==null);else do r=r.previousElementSibling??void 0;while(r instanceof HTMLElement&&r.offsetParent==null);r&&(n.classList.remove("current"),r.classList.add("current"))}}function Ne(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),e.blur()}}function ce(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(K(t.substring(s,o)),`${K(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(K(t.substring(s))),i.join("")}var He={"&":"&","<":"<",">":">","'":"'",'"':"""};function K(t){return t.replace(/[&<>"'"]/g,e=>He[e])}var I=class{constructor(e){this.el=e.el,this.app=e.app}};var F="mousedown",fe="mousemove",H="mouseup",J={x:0,y:0},pe=!1,ee=!1,Be=!1,D=!1,me=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(me?"is-mobile":"not-mobile");me&&"ontouchstart"in document.documentElement&&(Be=!0,F="touchstart",fe="touchmove",H="touchend");document.addEventListener(F,t=>{ee=!0,D=!1;let e=F=="touchstart"?t.targetTouches[0]:t;J.y=e.pageY||0,J.x=e.pageX||0});document.addEventListener(fe,t=>{if(ee&&!D){let e=F=="touchstart"?t.targetTouches[0]:t,n=J.x-(e.pageX||0),r=J.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(H,()=>{ee=!1});document.addEventListener("click",t=>{pe&&(t.preventDefault(),t.stopImmediatePropagation(),pe=!1)});var X=class extends I{constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(H,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(F,n=>this.onDocumentPointerDown(n)),document.addEventListener(H,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var te;try{te=localStorage}catch{te={getItem(){return null},setItem(){}}}var Q=te;var ye=document.head.appendChild(document.createElement("style"));ye.dataset.for="filters";var Y=class extends I{constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),ye.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } -`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=Q.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){Q.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var Z=class extends I{constructor(e){super(e),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.dataset.key??this.summary.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`;let n=Q.getItem(this.key);this.el.open=n?n==="true":this.el.open,this.el.addEventListener("toggle",()=>this.update());let r=this.summary.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)}),this.update()}update(){this.icon.style.transform=`rotate(${this.el.open?0:-90}deg)`,Q.setItem(this.key,this.el.open.toString())}};function ge(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,ve(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),ve(t.value)})}function ve(t){document.documentElement.dataset.theme=t}var Le;function be(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",xe),xe())}async function xe(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let n=await(await fetch(window.navigationData)).arrayBuffer(),r=new Blob([n]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();Le=t.dataset.base+"/",t.innerHTML="";for(let s of i)we(s,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function we(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-index-accordion`:"tsd-index-accordion",s.dataset.key=i.join("$");let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.innerHTML='',Ee(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let u of t.children)we(u,l,i)}else Ee(t,r,t.class)}function Ee(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));r.href=Le+t.path,n&&(r.className=n),location.pathname===r.pathname&&r.classList.add("current"),t.kind&&(r.innerHTML=``),r.appendChild(document.createElement("span")).textContent=t.text}else e.appendChild(document.createElement("span")).textContent=t.text}G(X,"a[data-toggle]");G(Z,".tsd-index-accordion");G(Y,".tsd-filter-item input[type=checkbox]");var Se=document.getElementById("tsd-theme");Se&&ge(Se);var je=new U;Object.defineProperty(window,"app",{value:je});he();be();})(); -/*! Bundled license information: - -lunr/lunr.js: - (** - * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 - * Copyright (C) 2020 Oliver Nightingale - * @license MIT - *) - (*! - * lunr.utils - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Set - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.tokenizer - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Pipeline - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Vector - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.stemmer - * Copyright (C) 2020 Oliver Nightingale - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - *) - (*! - * lunr.stopWordFilter - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.trimmer - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.TokenSet - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Index - * Copyright (C) 2020 Oliver Nightingale - *) - (*! - * lunr.Builder - * Copyright (C) 2020 Oliver Nightingale - *) -*/ diff --git a/docs/assets/navigation.js b/docs/assets/navigation.js deleted file mode 100644 index 0fa6114..0000000 --- a/docs/assets/navigation.js +++ /dev/null @@ -1 +0,0 @@ -window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA42UUU/CMBDHv0ufUWQKKo8ixgeCBEl8MD7U7WANpV3ag7AYv7sZA+m69cZr//f7pb277fOHIeyRDdkTxzgdya1FMKzDMo4pG7JYcmvBdt30OsWNZB22Fiphw1708NtptLxlKLSytOxY1OqcGR2DDcmOKWV5hiUYA0ndcEooes4R6mRxSlELbtd1qji9tIfjHSh0Xi0Uglny2OtiWVaVRv1BoIm1wfjWah2lHaVCJsfyFx6jNnmjtqGO0k70auWuoWMqIwqecWMDcBlR8CLPIDm0c7wRiAFPraq19adB+VLMM3+Wjcabx/teP/I7P94LnAO3WvlGL26zfaT5VOMrcIlp7rsq4WWmOfAk4DlEra/TymoJ/h7suBH8WxbvcwuqttuouklN+ESvaGgCO5A2gJYhIZjq8M1PGYG/i00mwd/j5VbFhw+y6+ZVzeDO0ayFlE14cU5g0rv6GSyTqx4BZ6LYOuv+s878f0gLQiyFWUC/5Wf2P6wJvv4ANd1pyAEHAAA=" \ No newline at end of file diff --git a/docs/assets/search.js b/docs/assets/search.js deleted file mode 100644 index cf04735..0000000 --- a/docs/assets/search.js +++ /dev/null @@ -1 +0,0 @@ -window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file diff --git a/docs/assets/style.css b/docs/assets/style.css deleted file mode 100644 index 778b949..0000000 --- a/docs/assets/style.css +++ /dev/null @@ -1,1412 +0,0 @@ -:root { - /* Light */ - --light-color-background: #f2f4f8; - --light-color-background-secondary: #eff0f1; - --light-color-warning-text: #222; - --light-color-background-warning: #e6e600; - --light-color-icon-background: var(--light-color-background); - --light-color-accent: #c5c7c9; - --light-color-active-menu-item: var(--light-color-accent); - --light-color-text: #222; - --light-color-text-aside: #6e6e6e; - --light-color-link: #1f70c2; - - --light-color-ts-keyword: #056bd6; - --light-color-ts-project: #b111c9; - --light-color-ts-module: var(--light-color-ts-project); - --light-color-ts-namespace: var(--light-color-ts-project); - --light-color-ts-enum: #7e6f15; - --light-color-ts-enum-member: var(--light-color-ts-enum); - --light-color-ts-variable: #4760ec; - --light-color-ts-function: #572be7; - --light-color-ts-class: #1f70c2; - --light-color-ts-interface: #108024; - --light-color-ts-constructor: var(--light-color-ts-class); - --light-color-ts-property: var(--light-color-ts-variable); - --light-color-ts-method: var(--light-color-ts-function); - --light-color-ts-call-signature: var(--light-color-ts-method); - --light-color-ts-index-signature: var(--light-color-ts-property); - --light-color-ts-constructor-signature: var(--light-color-ts-constructor); - --light-color-ts-parameter: var(--light-color-ts-variable); - /* type literal not included as links will never be generated to it */ - --light-color-ts-type-parameter: #a55c0e; - --light-color-ts-accessor: var(--light-color-ts-property); - --light-color-ts-get-signature: var(--light-color-ts-accessor); - --light-color-ts-set-signature: var(--light-color-ts-accessor); - --light-color-ts-type-alias: #d51270; - /* reference not included as links will be colored with the kind that it points to */ - - --light-external-icon: url("data:image/svg+xml;utf8,"); - --light-color-scheme: light; - - /* Dark */ - --dark-color-background: #2b2e33; - --dark-color-background-secondary: #1e2024; - --dark-color-background-warning: #bebe00; - --dark-color-warning-text: #222; - --dark-color-icon-background: var(--dark-color-background-secondary); - --dark-color-accent: #9096a2; - --dark-color-active-menu-item: #5d5d6a; - --dark-color-text: #f5f5f5; - --dark-color-text-aside: #dddddd; - --dark-color-link: #00aff4; - - --dark-color-ts-keyword: #3399ff; - --dark-color-ts-project: #e358ff; - --dark-color-ts-module: var(--dark-color-ts-project); - --dark-color-ts-namespace: var(--dark-color-ts-project); - --dark-color-ts-enum: #f4d93e; - --dark-color-ts-enum-member: var(--dark-color-ts-enum); - --dark-color-ts-variable: #798dff; - --dark-color-ts-function: #a280ff; - --dark-color-ts-class: #8ac4ff; - --dark-color-ts-interface: #6cff87; - --dark-color-ts-constructor: var(--dark-color-ts-class); - --dark-color-ts-property: var(--dark-color-ts-variable); - --dark-color-ts-method: var(--dark-color-ts-function); - --dark-color-ts-call-signature: var(--dark-color-ts-method); - --dark-color-ts-index-signature: var(--dark-color-ts-property); - --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); - --dark-color-ts-parameter: var(--dark-color-ts-variable); - /* type literal not included as links will never be generated to it */ - --dark-color-ts-type-parameter: #e07d13; - --dark-color-ts-accessor: var(--dark-color-ts-property); - --dark-color-ts-get-signature: var(--dark-color-ts-accessor); - --dark-color-ts-set-signature: var(--dark-color-ts-accessor); - --dark-color-ts-type-alias: #ff6492; - /* reference not included as links will be colored with the kind that it points to */ - - --dark-external-icon: url("data:image/svg+xml;utf8,"); - --dark-color-scheme: dark; -} - -@media (prefers-color-scheme: light) { - :root { - --color-background: var(--light-color-background); - --color-background-secondary: var(--light-color-background-secondary); - --color-background-warning: var(--light-color-background-warning); - --color-warning-text: var(--light-color-warning-text); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-active-menu-item: var(--light-color-active-menu-item); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - - --color-ts-keyword: var(--light-color-ts-keyword); - --color-ts-module: var(--light-color-ts-module); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-enum-member: var(--light-color-ts-enum-member); - --color-ts-variable: var(--light-color-ts-variable); - --color-ts-function: var(--light-color-ts-function); - --color-ts-class: var(--light-color-ts-class); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-constructor: var(--light-color-ts-constructor); - --color-ts-property: var(--light-color-ts-property); - --color-ts-method: var(--light-color-ts-method); - --color-ts-call-signature: var(--light-color-ts-call-signature); - --color-ts-index-signature: var(--light-color-ts-index-signature); - --color-ts-constructor-signature: var( - --light-color-ts-constructor-signature - ); - --color-ts-parameter: var(--light-color-ts-parameter); - --color-ts-type-parameter: var(--light-color-ts-type-parameter); - --color-ts-accessor: var(--light-color-ts-accessor); - --color-ts-get-signature: var(--light-color-ts-get-signature); - --color-ts-set-signature: var(--light-color-ts-set-signature); - --color-ts-type-alias: var(--light-color-ts-type-alias); - - --external-icon: var(--light-external-icon); - --color-scheme: var(--light-color-scheme); - } -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--dark-color-background); - --color-background-secondary: var(--dark-color-background-secondary); - --color-background-warning: var(--dark-color-background-warning); - --color-warning-text: var(--dark-color-warning-text); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-active-menu-item: var(--dark-color-active-menu-item); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - - --color-ts-keyword: var(--dark-color-ts-keyword); - --color-ts-module: var(--dark-color-ts-module); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-enum-member: var(--dark-color-ts-enum-member); - --color-ts-variable: var(--dark-color-ts-variable); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-constructor: var(--dark-color-ts-constructor); - --color-ts-property: var(--dark-color-ts-property); - --color-ts-method: var(--dark-color-ts-method); - --color-ts-call-signature: var(--dark-color-ts-call-signature); - --color-ts-index-signature: var(--dark-color-ts-index-signature); - --color-ts-constructor-signature: var( - --dark-color-ts-constructor-signature - ); - --color-ts-parameter: var(--dark-color-ts-parameter); - --color-ts-type-parameter: var(--dark-color-ts-type-parameter); - --color-ts-accessor: var(--dark-color-ts-accessor); - --color-ts-get-signature: var(--dark-color-ts-get-signature); - --color-ts-set-signature: var(--dark-color-ts-set-signature); - --color-ts-type-alias: var(--dark-color-ts-type-alias); - - --external-icon: var(--dark-external-icon); - --color-scheme: var(--dark-color-scheme); - } -} - -html { - color-scheme: var(--color-scheme); -} - -body { - margin: 0; -} - -:root[data-theme="light"] { - --color-background: var(--light-color-background); - --color-background-secondary: var(--light-color-background-secondary); - --color-background-warning: var(--light-color-background-warning); - --color-warning-text: var(--light-color-warning-text); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-active-menu-item: var(--light-color-active-menu-item); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - - --color-ts-keyword: var(--light-color-ts-keyword); - --color-ts-module: var(--light-color-ts-module); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-enum-member: var(--light-color-ts-enum-member); - --color-ts-variable: var(--light-color-ts-variable); - --color-ts-function: var(--light-color-ts-function); - --color-ts-class: var(--light-color-ts-class); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-constructor: var(--light-color-ts-constructor); - --color-ts-property: var(--light-color-ts-property); - --color-ts-method: var(--light-color-ts-method); - --color-ts-call-signature: var(--light-color-ts-call-signature); - --color-ts-index-signature: var(--light-color-ts-index-signature); - --color-ts-constructor-signature: var( - --light-color-ts-constructor-signature - ); - --color-ts-parameter: var(--light-color-ts-parameter); - --color-ts-type-parameter: var(--light-color-ts-type-parameter); - --color-ts-accessor: var(--light-color-ts-accessor); - --color-ts-get-signature: var(--light-color-ts-get-signature); - --color-ts-set-signature: var(--light-color-ts-set-signature); - --color-ts-type-alias: var(--light-color-ts-type-alias); - - --external-icon: var(--light-external-icon); - --color-scheme: var(--light-color-scheme); -} - -:root[data-theme="dark"] { - --color-background: var(--dark-color-background); - --color-background-secondary: var(--dark-color-background-secondary); - --color-background-warning: var(--dark-color-background-warning); - --color-warning-text: var(--dark-color-warning-text); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-active-menu-item: var(--dark-color-active-menu-item); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - - --color-ts-keyword: var(--dark-color-ts-keyword); - --color-ts-module: var(--dark-color-ts-module); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-enum-member: var(--dark-color-ts-enum-member); - --color-ts-variable: var(--dark-color-ts-variable); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-constructor: var(--dark-color-ts-constructor); - --color-ts-property: var(--dark-color-ts-property); - --color-ts-method: var(--dark-color-ts-method); - --color-ts-call-signature: var(--dark-color-ts-call-signature); - --color-ts-index-signature: var(--dark-color-ts-index-signature); - --color-ts-constructor-signature: var( - --dark-color-ts-constructor-signature - ); - --color-ts-parameter: var(--dark-color-ts-parameter); - --color-ts-type-parameter: var(--dark-color-ts-type-parameter); - --color-ts-accessor: var(--dark-color-ts-accessor); - --color-ts-get-signature: var(--dark-color-ts-get-signature); - --color-ts-set-signature: var(--dark-color-ts-set-signature); - --color-ts-type-alias: var(--dark-color-ts-type-alias); - - --external-icon: var(--dark-external-icon); - --color-scheme: var(--dark-color-scheme); -} - -.always-visible, -.always-visible .tsd-signatures { - display: inherit !important; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.2; -} - -h1 > a:not(.link), -h2 > a:not(.link), -h3 > a:not(.link), -h4 > a:not(.link), -h5 > a:not(.link), -h6 > a:not(.link) { - text-decoration: none; - color: var(--color-text); -} - -h1 { - font-size: 1.875rem; - margin: 0.67rem 0; -} - -h2 { - font-size: 1.5rem; - margin: 0.83rem 0; -} - -h3 { - font-size: 1.25rem; - margin: 1rem 0; -} - -h4 { - font-size: 1.05rem; - margin: 1.33rem 0; -} - -h5 { - font-size: 1rem; - margin: 1.5rem 0; -} - -h6 { - font-size: 0.875rem; - margin: 2.33rem 0; -} - -.uppercase { - text-transform: uppercase; -} - -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -.container { - max-width: 1700px; - padding: 0 2rem; -} - -/* Footer */ -footer { - border-top: 1px solid var(--color-accent); - padding-top: 1rem; - padding-bottom: 1rem; - max-height: 3.5rem; -} -.tsd-generator { - margin: 0 1em; -} - -.container-main { - margin: 0 auto; - /* toolbar, footer, margin */ - min-height: calc(100vh - 41px - 56px - 4rem); -} - -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -@keyframes fade-out { - from { - opacity: 1; - visibility: visible; - } - to { - opacity: 0; - } -} -@keyframes fade-in-delayed { - 0% { - opacity: 0; - } - 33% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@keyframes fade-out-delayed { - 0% { - opacity: 1; - visibility: visible; - } - 66% { - opacity: 0; - } - 100% { - opacity: 0; - } -} -@keyframes pop-in-from-right { - from { - transform: translate(100%, 0); - } - to { - transform: translate(0, 0); - } -} -@keyframes pop-out-to-right { - from { - transform: translate(0, 0); - visibility: visible; - } - to { - transform: translate(100%, 0); - } -} -body { - background: var(--color-background); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - font-size: 16px; - color: var(--color-text); -} - -a { - color: var(--color-link); - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -a.external[target="_blank"] { - background-image: var(--external-icon); - background-position: top 3px right; - background-repeat: no-repeat; - padding-right: 13px; -} - -code, -pre { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - padding: 0.2em; - margin: 0; - font-size: 0.875rem; - border-radius: 0.8em; -} - -pre { - position: relative; - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; - padding: 10px; - border: 1px solid var(--color-accent); -} -pre code { - padding: 0; - font-size: 100%; -} -pre > button { - position: absolute; - top: 10px; - right: 10px; - opacity: 0; - transition: opacity 0.1s; - box-sizing: border-box; -} -pre:hover > button, -pre > button.visible { - opacity: 1; -} - -blockquote { - margin: 1em 0; - padding-left: 1em; - border-left: 4px solid gray; -} - -.tsd-typography { - line-height: 1.333em; -} -.tsd-typography ul { - list-style: square; - padding: 0 0 0 20px; - margin: 0; -} -.tsd-typography .tsd-index-panel h3, -.tsd-index-panel .tsd-typography h3, -.tsd-typography h4, -.tsd-typography h5, -.tsd-typography h6 { - font-size: 1em; -} -.tsd-typography h5, -.tsd-typography h6 { - font-weight: normal; -} -.tsd-typography p, -.tsd-typography ul, -.tsd-typography ol { - margin: 1em 0; -} -.tsd-typography table { - border-collapse: collapse; - border: none; -} -.tsd-typography td, -.tsd-typography th { - padding: 6px 13px; - border: 1px solid var(--color-accent); -} -.tsd-typography thead, -.tsd-typography tr:nth-child(even) { - background-color: var(--color-background-secondary); -} - -.tsd-breadcrumb { - margin: 0; - padding: 0; - color: var(--color-text-aside); -} -.tsd-breadcrumb a { - color: var(--color-text-aside); - text-decoration: none; -} -.tsd-breadcrumb a:hover { - text-decoration: underline; -} -.tsd-breadcrumb li { - display: inline; -} -.tsd-breadcrumb li:after { - content: " / "; -} - -.tsd-comment-tags { - display: flex; - flex-direction: column; -} -dl.tsd-comment-tag-group { - display: flex; - align-items: center; - overflow: hidden; - margin: 0.5em 0; -} -dl.tsd-comment-tag-group dt { - display: flex; - margin-right: 0.5em; - font-size: 0.875em; - font-weight: normal; -} -dl.tsd-comment-tag-group dd { - margin: 0; -} -code.tsd-tag { - padding: 0.25em 0.4em; - border: 0.1em solid var(--color-accent); - margin-right: 0.25em; - font-size: 70%; -} -h1 code.tsd-tag:first-of-type { - margin-left: 0.25em; -} - -dl.tsd-comment-tag-group dd:before, -dl.tsd-comment-tag-group dd:after { - content: " "; -} -dl.tsd-comment-tag-group dd pre, -dl.tsd-comment-tag-group dd:after { - clear: both; -} -dl.tsd-comment-tag-group p { - margin: 0; -} - -.tsd-panel.tsd-comment .lead { - font-size: 1.1em; - line-height: 1.333em; - margin-bottom: 2em; -} -.tsd-panel.tsd-comment .lead:last-child { - margin-bottom: 0; -} - -.tsd-filter-visibility h4 { - font-size: 1rem; - padding-top: 0.75rem; - padding-bottom: 0.5rem; - margin: 0; -} -.tsd-filter-item:not(:last-child) { - margin-bottom: 0.5rem; -} -.tsd-filter-input { - display: flex; - width: fit-content; - width: -moz-fit-content; - align-items: center; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - cursor: pointer; -} -.tsd-filter-input input[type="checkbox"] { - cursor: pointer; - position: absolute; - width: 1.5em; - height: 1.5em; - opacity: 0; -} -.tsd-filter-input input[type="checkbox"]:disabled { - pointer-events: none; -} -.tsd-filter-input svg { - cursor: pointer; - width: 1.5em; - height: 1.5em; - margin-right: 0.5em; - border-radius: 0.33em; - /* Leaving this at full opacity breaks event listeners on Firefox. - Don't remove unless you know what you're doing. */ - opacity: 0.99; -} -.tsd-filter-input input[type="checkbox"]:focus + svg { - transform: scale(0.95); -} -.tsd-filter-input input[type="checkbox"]:focus:not(:focus-visible) + svg { - transform: scale(1); -} -.tsd-checkbox-background { - fill: var(--color-accent); -} -input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { - stroke: var(--color-text); -} -.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { - fill: var(--color-background); - stroke: var(--color-accent); - stroke-width: 0.25rem; -} -.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { - stroke: var(--color-accent); -} - -.tsd-theme-toggle { - padding-top: 0.75rem; -} -.tsd-theme-toggle > h4 { - display: inline; - vertical-align: middle; - margin-right: 0.75rem; -} - -.tsd-hierarchy { - list-style: square; - margin: 0; -} -.tsd-hierarchy .target { - font-weight: bold; -} - -.tsd-full-hierarchy:not(:last-child) { - margin-bottom: 1em; - padding-bottom: 1em; - border-bottom: 1px solid var(--color-accent); -} -.tsd-full-hierarchy, -.tsd-full-hierarchy ul { - list-style: none; - margin: 0; - padding: 0; -} -.tsd-full-hierarchy ul { - padding-left: 1.5rem; -} -.tsd-full-hierarchy a { - padding: 0.25rem 0 !important; - font-size: 1rem; - display: inline-flex; - align-items: center; - color: var(--color-text); -} - -.tsd-panel-group.tsd-index-group { - margin-bottom: 0; -} -.tsd-index-panel .tsd-index-list { - list-style: none; - line-height: 1.333em; - margin: 0; - padding: 0.25rem 0 0 0; - overflow: hidden; - display: grid; - grid-template-columns: repeat(3, 1fr); - column-gap: 1rem; - grid-template-rows: auto; -} -@media (max-width: 1024px) { - .tsd-index-panel .tsd-index-list { - grid-template-columns: repeat(2, 1fr); - } -} -@media (max-width: 768px) { - .tsd-index-panel .tsd-index-list { - grid-template-columns: repeat(1, 1fr); - } -} -.tsd-index-panel .tsd-index-list li { - -webkit-page-break-inside: avoid; - -moz-page-break-inside: avoid; - -ms-page-break-inside: avoid; - -o-page-break-inside: avoid; - page-break-inside: avoid; -} - -.tsd-flag { - display: inline-block; - padding: 0.25em 0.4em; - border-radius: 4px; - color: var(--color-comment-tag-text); - background-color: var(--color-comment-tag); - text-indent: 0; - font-size: 75%; - line-height: 1; - font-weight: normal; -} - -.tsd-anchor { - position: relative; - top: -100px; -} - -.tsd-member { - position: relative; -} -.tsd-member .tsd-anchor + h3 { - display: flex; - align-items: center; - margin-top: 0; - margin-bottom: 0; - border-bottom: none; -} - -.tsd-navigation.settings { - margin: 1rem 0; -} -.tsd-navigation > a, -.tsd-navigation .tsd-accordion-summary { - width: calc(100% - 0.25rem); - display: flex; - align-items: center; -} -.tsd-navigation a, -.tsd-navigation summary > span, -.tsd-page-navigation a { - display: flex; - width: calc(100% - 0.25rem); - align-items: center; - padding: 0.25rem; - color: var(--color-text); - text-decoration: none; - box-sizing: border-box; -} -.tsd-navigation a.current, -.tsd-page-navigation a.current { - background: var(--color-active-menu-item); -} -.tsd-navigation a:hover, -.tsd-page-navigation a:hover { - text-decoration: underline; -} -.tsd-navigation ul, -.tsd-page-navigation ul { - margin-top: 0; - margin-bottom: 0; - padding: 0; - list-style: none; -} -.tsd-navigation li, -.tsd-page-navigation li { - padding: 0; - max-width: 100%; -} -.tsd-nested-navigation { - margin-left: 3rem; -} -.tsd-nested-navigation > li > details { - margin-left: -1.5rem; -} -.tsd-small-nested-navigation { - margin-left: 1.5rem; -} -.tsd-small-nested-navigation > li > details { - margin-left: -1.5rem; -} - -.tsd-page-navigation ul { - padding-left: 1.75rem; -} - -#tsd-sidebar-links a { - margin-top: 0; - margin-bottom: 0.5rem; - line-height: 1.25rem; -} -#tsd-sidebar-links a:last-of-type { - margin-bottom: 0; -} - -a.tsd-index-link { - padding: 0.25rem 0 !important; - font-size: 1rem; - line-height: 1.25rem; - display: inline-flex; - align-items: center; - color: var(--color-text); -} -.tsd-accordion-summary { - list-style-type: none; /* hide marker on non-safari */ - outline: none; /* broken on safari, so just hide it */ -} -.tsd-accordion-summary::-webkit-details-marker { - display: none; /* hide marker on safari */ -} -.tsd-accordion-summary, -.tsd-accordion-summary a { - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - - cursor: pointer; -} -.tsd-accordion-summary a { - width: calc(100% - 1.5rem); -} -.tsd-accordion-summary > * { - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; -} -.tsd-index-accordion .tsd-accordion-summary > svg { - margin-left: 0.25rem; -} -.tsd-index-content > :not(:first-child) { - margin-top: 0.75rem; -} -.tsd-index-heading { - margin-top: 1.5rem; - margin-bottom: 0.75rem; -} - -.tsd-kind-icon { - margin-right: 0.5rem; - width: 1.25rem; - height: 1.25rem; - min-width: 1.25rem; - min-height: 1.25rem; -} -.tsd-kind-icon path { - transform-origin: center; - transform: scale(1.1); -} -.tsd-signature > .tsd-kind-icon { - margin-right: 0.8rem; -} - -.tsd-panel { - margin-bottom: 2.5rem; -} -.tsd-panel.tsd-member { - margin-bottom: 4rem; -} -.tsd-panel:empty { - display: none; -} -.tsd-panel > h1, -.tsd-panel > h2, -.tsd-panel > h3 { - margin: 1.5rem -1.5rem 0.75rem -1.5rem; - padding: 0 1.5rem 0.75rem 1.5rem; -} -.tsd-panel > h1.tsd-before-signature, -.tsd-panel > h2.tsd-before-signature, -.tsd-panel > h3.tsd-before-signature { - margin-bottom: 0; - border-bottom: none; -} - -.tsd-panel-group { - margin: 4rem 0; -} -.tsd-panel-group.tsd-index-group { - margin: 2rem 0; -} -.tsd-panel-group.tsd-index-group details { - margin: 2rem 0; -} - -#tsd-search { - transition: background-color 0.2s; -} -#tsd-search .title { - position: relative; - z-index: 2; -} -#tsd-search .field { - position: absolute; - left: 0; - top: 0; - right: 2.5rem; - height: 100%; -} -#tsd-search .field input { - box-sizing: border-box; - position: relative; - top: -50px; - z-index: 1; - width: 100%; - padding: 0 10px; - opacity: 0; - outline: 0; - border: 0; - background: transparent; - color: var(--color-text); -} -#tsd-search .field label { - position: absolute; - overflow: hidden; - right: -40px; -} -#tsd-search .field input, -#tsd-search .title, -#tsd-toolbar-links a { - transition: opacity 0.2s; -} -#tsd-search .results { - position: absolute; - visibility: hidden; - top: 40px; - width: 100%; - margin: 0; - padding: 0; - list-style: none; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); -} -#tsd-search .results li { - background-color: var(--color-background); - line-height: initial; - padding: 4px; -} -#tsd-search .results li:nth-child(even) { - background-color: var(--color-background-secondary); -} -#tsd-search .results li.state { - display: none; -} -#tsd-search .results li.current:not(.no-results), -#tsd-search .results li:hover:not(.no-results) { - background-color: var(--color-accent); -} -#tsd-search .results a { - display: flex; - align-items: center; - padding: 0.25rem; - box-sizing: border-box; -} -#tsd-search .results a:before { - top: 10px; -} -#tsd-search .results span.parent { - color: var(--color-text-aside); - font-weight: normal; -} -#tsd-search.has-focus { - background-color: var(--color-accent); -} -#tsd-search.has-focus .field input { - top: 0; - opacity: 1; -} -#tsd-search.has-focus .title, -#tsd-search.has-focus #tsd-toolbar-links a { - z-index: 0; - opacity: 0; -} -#tsd-search.has-focus .results { - visibility: visible; -} -#tsd-search.loading .results li.state.loading { - display: block; -} -#tsd-search.failure .results li.state.failure { - display: block; -} - -#tsd-toolbar-links { - position: absolute; - top: 0; - right: 2rem; - height: 100%; - display: flex; - align-items: center; - justify-content: flex-end; -} -#tsd-toolbar-links a { - margin-left: 1.5rem; -} -#tsd-toolbar-links a:hover { - text-decoration: underline; -} - -.tsd-signature { - margin: 0 0 1rem 0; - padding: 1rem 0.5rem; - border: 1px solid var(--color-accent); - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: 14px; - overflow-x: auto; -} - -.tsd-signature-keyword { - color: var(--color-ts-keyword); - font-weight: normal; -} - -.tsd-signature-symbol { - color: var(--color-text-aside); - font-weight: normal; -} - -.tsd-signature-type { - font-style: italic; - font-weight: normal; -} - -.tsd-signatures { - padding: 0; - margin: 0 0 1em 0; - list-style-type: none; -} -.tsd-signatures .tsd-signature { - margin: 0; - border-color: var(--color-accent); - border-width: 1px 0; - transition: background-color 0.1s; -} -.tsd-description .tsd-signatures .tsd-signature { - border-width: 1px; -} - -ul.tsd-parameter-list, -ul.tsd-type-parameter-list { - list-style: square; - margin: 0; - padding-left: 20px; -} -ul.tsd-parameter-list > li.tsd-parameter-signature, -ul.tsd-type-parameter-list > li.tsd-parameter-signature { - list-style: none; - margin-left: -20px; -} -ul.tsd-parameter-list h5, -ul.tsd-type-parameter-list h5 { - font-size: 16px; - margin: 1em 0 0.5em 0; -} -.tsd-sources { - margin-top: 1rem; - font-size: 0.875em; -} -.tsd-sources a { - color: var(--color-text-aside); - text-decoration: underline; -} -.tsd-sources ul { - list-style: none; - padding: 0; -} - -.tsd-page-toolbar { - position: sticky; - z-index: 1; - top: 0; - left: 0; - width: 100%; - color: var(--color-text); - background: var(--color-background-secondary); - border-bottom: 1px var(--color-accent) solid; - transition: transform 0.3s ease-in-out; -} -.tsd-page-toolbar a { - color: var(--color-text); - text-decoration: none; -} -.tsd-page-toolbar a.title { - font-weight: bold; -} -.tsd-page-toolbar a.title:hover { - text-decoration: underline; -} -.tsd-page-toolbar .tsd-toolbar-contents { - display: flex; - justify-content: space-between; - height: 2.5rem; - margin: 0 auto; -} -.tsd-page-toolbar .table-cell { - position: relative; - white-space: nowrap; - line-height: 40px; -} -.tsd-page-toolbar .table-cell:first-child { - width: 100%; -} -.tsd-page-toolbar .tsd-toolbar-icon { - box-sizing: border-box; - line-height: 0; - padding: 12px 0; -} - -.tsd-widget { - display: inline-block; - overflow: hidden; - opacity: 0.8; - height: 40px; - transition: - opacity 0.1s, - background-color 0.2s; - vertical-align: bottom; - cursor: pointer; -} -.tsd-widget:hover { - opacity: 0.9; -} -.tsd-widget.active { - opacity: 1; - background-color: var(--color-accent); -} -.tsd-widget.no-caption { - width: 40px; -} -.tsd-widget.no-caption:before { - margin: 0; -} - -.tsd-widget.options, -.tsd-widget.menu { - display: none; -} -input[type="checkbox"] + .tsd-widget:before { - background-position: -120px 0; -} -input[type="checkbox"]:checked + .tsd-widget:before { - background-position: -160px 0; -} - -img { - max-width: 100%; -} - -.tsd-anchor-icon { - display: inline-flex; - align-items: center; - margin-left: 0.5rem; - vertical-align: middle; - color: var(--color-text); -} - -.tsd-anchor-icon svg { - width: 1em; - height: 1em; - visibility: hidden; -} - -.tsd-anchor-link:hover > .tsd-anchor-icon svg { - visibility: visible; -} - -.deprecated { - text-decoration: line-through !important; -} - -.warning { - padding: 1rem; - color: var(--color-warning-text); - background: var(--color-background-warning); -} - -.tsd-kind-project { - color: var(--color-ts-project); -} -.tsd-kind-module { - color: var(--color-ts-module); -} -.tsd-kind-namespace { - color: var(--color-ts-namespace); -} -.tsd-kind-enum { - color: var(--color-ts-enum); -} -.tsd-kind-enum-member { - color: var(--color-ts-enum-member); -} -.tsd-kind-variable { - color: var(--color-ts-variable); -} -.tsd-kind-function { - color: var(--color-ts-function); -} -.tsd-kind-class { - color: var(--color-ts-class); -} -.tsd-kind-interface { - color: var(--color-ts-interface); -} -.tsd-kind-constructor { - color: var(--color-ts-constructor); -} -.tsd-kind-property { - color: var(--color-ts-property); -} -.tsd-kind-method { - color: var(--color-ts-method); -} -.tsd-kind-call-signature { - color: var(--color-ts-call-signature); -} -.tsd-kind-index-signature { - color: var(--color-ts-index-signature); -} -.tsd-kind-constructor-signature { - color: var(--color-ts-constructor-signature); -} -.tsd-kind-parameter { - color: var(--color-ts-parameter); -} -.tsd-kind-type-literal { - color: var(--color-ts-type-literal); -} -.tsd-kind-type-parameter { - color: var(--color-ts-type-parameter); -} -.tsd-kind-accessor { - color: var(--color-ts-accessor); -} -.tsd-kind-get-signature { - color: var(--color-ts-get-signature); -} -.tsd-kind-set-signature { - color: var(--color-ts-set-signature); -} -.tsd-kind-type-alias { - color: var(--color-ts-type-alias); -} - -/* if we have a kind icon, don't color the text by kind */ -.tsd-kind-icon ~ span { - color: var(--color-text); -} - -* { - scrollbar-width: thin; - scrollbar-color: var(--color-accent) var(--color-icon-background); -} - -*::-webkit-scrollbar { - width: 0.75rem; -} - -*::-webkit-scrollbar-track { - background: var(--color-icon-background); -} - -*::-webkit-scrollbar-thumb { - background-color: var(--color-accent); - border-radius: 999rem; - border: 0.25rem solid var(--color-icon-background); -} - -/* mobile */ -@media (max-width: 769px) { - .tsd-widget.options, - .tsd-widget.menu { - display: inline-block; - } - - .container-main { - display: flex; - } - html .col-content { - float: none; - max-width: 100%; - width: 100%; - } - html .col-sidebar { - position: fixed !important; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - z-index: 1024; - top: 0 !important; - bottom: 0 !important; - left: auto !important; - right: 0 !important; - padding: 1.5rem 1.5rem 0 0; - width: 75vw; - visibility: hidden; - background-color: var(--color-background); - transform: translate(100%, 0); - } - html .col-sidebar > *:last-child { - padding-bottom: 20px; - } - html .overlay { - content: ""; - display: block; - position: fixed; - z-index: 1023; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.75); - visibility: hidden; - } - - .to-has-menu .overlay { - animation: fade-in 0.4s; - } - - .to-has-menu .col-sidebar { - animation: pop-in-from-right 0.4s; - } - - .from-has-menu .overlay { - animation: fade-out 0.4s; - } - - .from-has-menu .col-sidebar { - animation: pop-out-to-right 0.4s; - } - - .has-menu body { - overflow: hidden; - } - .has-menu .overlay { - visibility: visible; - } - .has-menu .col-sidebar { - visibility: visible; - transform: translate(0, 0); - display: flex; - flex-direction: column; - gap: 1.5rem; - max-height: 100vh; - padding: 1rem 2rem; - } - .has-menu .tsd-navigation { - max-height: 100%; - } -} - -/* one sidebar */ -@media (min-width: 770px) { - .container-main { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); - grid-template-areas: "sidebar content"; - margin: 2rem auto; - } - - .col-sidebar { - grid-area: sidebar; - } - .col-content { - grid-area: content; - padding: 0 1rem; - } -} -@media (min-width: 770px) and (max-width: 1399px) { - .col-sidebar { - max-height: calc(100vh - 2rem - 42px); - overflow: auto; - position: sticky; - top: 42px; - padding-top: 1rem; - } - .site-menu { - margin-top: 1rem; - } -} - -/* two sidebars */ -@media (min-width: 1200px) { - .container-main { - grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax(0, 20rem); - grid-template-areas: "sidebar content toc"; - } - - .col-sidebar { - display: contents; - } - - .page-menu { - grid-area: toc; - padding-left: 1rem; - } - .site-menu { - grid-area: sidebar; - } - - .site-menu { - margin-top: 1rem 0; - } - - .page-menu, - .site-menu { - max-height: calc(100vh - 2rem - 42px); - overflow: auto; - position: sticky; - top: 42px; - } -} diff --git a/docs/classes/BatchCluster.html b/docs/classes/BatchCluster.html deleted file mode 100644 index 12ca6cf..0000000 --- a/docs/classes/BatchCluster.html +++ /dev/null @@ -1,63 +0,0 @@ -Codestin Search App

Class BatchCluster

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

-

Given the large number of configuration options, the constructor -receives a single options hash. The most important of these are the -ChildProcessFactory, which specifies the factory that creates -ChildProcess instances, and BatchProcessOptions, which specifies how -child tasks can be verified and shut down.

-

Constructors

Properties

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

Type declaration

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

      Parameters

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

      Returns this

See

BatchClusterEvents

-

Since

v9.0.0

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

Type declaration

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

      Parameters

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

      Returns this

See

BatchClusterEvents

-
options: AllOpts

Accessors

  • get busyProcCount(): number
  • Returns number

    the current number of child processes currently servicing tasks

    -
  • get childEndCounts(): {
    ย ย ย ย broken: number;
    ย ย ย ย closed: number;
    ย ย ย ย ended: number;
    ย ย ย ย ending: number;
    ย ย ย ย idle: number;
    ย ย ย ย old: number;
    ย ย ย ย proc.close: number;
    ย ย ย ย proc.disconnect: number;
    ย ย ย ย proc.error: number;
    ย ย ย ย proc.exit: number;
    ย ย ย ย startError: number;
    ย ย ย ย stderr: number;
    ย ย ย ย stderr.error: number;
    ย ย ย ย stdin.error: number;
    ย ย ย ย stdout.error: number;
    ย ย ย ย timeout: number;
    ย ย ย ย tooMany: number;
    ย ย ย ย unhealthy: number;
    ย ย ย ย worn: number;
    }
  • Returns {
    ย ย ย ย broken: number;
    ย ย ย ย closed: number;
    ย ย ย ย ended: number;
    ย ย ย ย ending: number;
    ย ย ย ย idle: number;
    ย ย ย ย old: number;
    ย ย ย ย proc.close: number;
    ย ย ย ย proc.disconnect: number;
    ย ย ย ย proc.error: number;
    ย ย ย ย proc.exit: number;
    ย ย ย ย startError: number;
    ย ย ย ย stderr: number;
    ย ย ย ย stderr.error: number;
    ย ย ย ย stdin.error: number;
    ย ย ย ย stdout.error: number;
    ย ย ย ย timeout: number;
    ย ย ย ย tooMany: number;
    ย ย ย ย unhealthy: number;
    ย ย ย ย worn: number;
    }

    • broken: number
    • closed: number
    • ended: number
    • ending: number
    • idle: number
    • old: number
    • proc.close: number
    • proc.disconnect: number
    • proc.error: number
    • proc.exit: number
    • startError: number
    • stderr: number
    • stderr.error: number
    • stdin.error: number
    • stdout.error: number
    • timeout: number
    • tooMany: number
    • unhealthy: number
    • worn: number
  • get internalErrorCount(): number
  • For integration tests:

    -

    Returns number

  • get isIdle(): boolean
  • Returns boolean

    true if all previously-enqueued tasks have settled

    -
  • get meanTasksPerProc(): number
  • Returns number

    the mean number of tasks completed by child processes

    -
  • get pendingTaskCount(): number
  • Returns number

    the number of pending tasks

    -
  • get procCount(): number
  • Returns number

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

    -
  • get spawnedProcCount(): number
  • Returns number

    the total number of child processes created by this instance

    -

Methods

  • Shut down any currently-running child processes. New child processes will -be started automatically to handle new tasks.

    -

    Parameters

    • gracefully: boolean = true

    Returns Promise<void>

  • Shut down this instance, and all child processes.

    -

    Parameters

    • gracefully: boolean = true

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

      -

    Returns Deferred<void>

  • Submits task for processing by a BatchProcess instance

    -

    Type Parameters

    • T

    Parameters

    Returns Promise<T>

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

    -
  • Verify that each BatchProcess PID is actually alive.

    -

    Returns number[]

    the spawned PIDs that are still in the process table.

    -
  • Reset the maximum number of active child processes to maxProcs. Note that -this is handled gracefully: child processes are only reduced as tasks are -completed.

    -

    Parameters

    • maxProcs: number

    Returns void

  • For diagnostics. Contents may change.

    -

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

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

    -

    Only public for tests.

    -

    Returns Promise<void[]>

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

Class BatchClusterOptions

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

-

Constructors

Properties

cleanupChildProcs: boolean = true

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

-

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

-

Defaults to true.

-
endGracefulWaitTimeMillis: number = 500

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

-

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

-
healthCheckIntervalMillis: number = 0

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

-

Set this to 0 to disable this feature.

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

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

-

Type declaration

maxFailedTasksPerProcess: number = 2

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

-

Set this to 0 to disable this feature.

-
maxIdleMsPerProcess: number = 0

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

-

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

-
maxProcAgeMillis: number = ...

Child processes will be recycled when they reach this age.

-

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

-

Defaults to 5 minutes. Set to 0 to disable.

-
maxProcs: number = 1

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

-

Defaults to 1.

-
maxReasonableProcessFailuresPerMinute: number = 10

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

-

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

-

Defaults to 10. Set to 0 to disable.

-
maxTasksPerProcess: number = 500

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

-

Must be >= 0. Defaults to 500

-
minDelayBetweenSpawnMillis: number = ...

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

-

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

-
onIdleIntervalMillis: number = ...

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

-

Must be > 0. Defaults to 10 seconds.

-
pidCheckIntervalMillis: number = ...

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

-

Set this to 0 to disable this feature.

-
spawnTimeoutMillis: number = ...

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

-

Defaults to 15 seconds. Set to 0 to disable.

-
streamFlushMillis: number = ...

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

-

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

-

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

-

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

-

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

-
taskTimeoutMillis: number = ...

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

-

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

-

Defaults to 10 seconds. Set to 0 to disable.

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

Class BatchProcess

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

-

Constructors

  • Parameters

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

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

      -
        • (): void
        • Returns void

    Returns BatchProcess

Properties

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

Accessors

  • get ended(): boolean
  • Returns boolean

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

    -
  • get ending(): boolean
  • Returns boolean

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

    -
  • get exited(): boolean
  • Returns boolean

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

    -
  • get healthy(): boolean
  • Returns boolean

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

    -
  • get idle(): boolean
  • Returns boolean

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

    -
  • get ready(): boolean
  • Returns boolean

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

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

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

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

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

    -

Methods

  • End this child process.

    -

    Parameters

    • gracefully: boolean = true

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

      -
    • reason: WhyNotHealthy

      who called end() (used for logging)

      -

    Returns Promise<void>

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

    -
  • Returns boolean

    true if the child process is in the process table

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

Class Deferred<T>

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

-

Type Parameters

  • T

Implements

  • PromiseLike<T>

Constructors

Properties

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

Accessors

  • get fulfilled(): boolean
  • Returns boolean

    true iff resolve has been invoked

    -
  • get pending(): boolean
  • Returns boolean

    true iff neither resolve nor rejected have been invoked

    -
  • get rejected(): boolean
  • Returns boolean

    true iff rejected has been invoked

    -
  • get settled(): boolean
  • Returns boolean

    true iff resolve or rejected have been invoked

    -

Methods

  • Parameters

    • Optional reason: string | Error

    Returns boolean

  • Parameters

    • value: T

    Returns boolean

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

Constructors

  • Parameters

    • periodMs: number = minuteMs

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

      -
    • warmupMs: number = secondMs

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

      -

    Returns Rate

Properties

periodMs: number = minuteMs

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

-
warmupMs: number = secondMs

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

-

Accessors

  • get eventCount(): number
  • Returns number

  • get eventsPerMinute(): number
  • Returns number

  • get eventsPerMs(): number
  • Returns number

  • get eventsPerSecond(): number
  • Returns number

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

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

Methods

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

Class Task<T>

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

-

Type Parameters

  • T = any

Constructors

Properties

Accessors

Methods

Constructors

  • Type Parameters

    • T = any

    Parameters

    • command: string

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

      -
    • parser: Parser<T>

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

      -

    Returns Task<T>

Properties

command: string

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

-
parser: Parser<T>

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

-
taskId: number = ...

Accessors

  • get pending(): boolean
  • Returns boolean

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

    the resolution or rejection of this task.

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

  • get state(): string
  • Returns string

Methods

  • Parameters

    • opts: TaskOptions

    Returns void

  • Parameters

    • buf: string | Buffer

    Returns void

  • Parameters

    • buf: string | Buffer

    Returns void

  • Parameters

    • error: Error

    Returns boolean

    true if the wrapped promise was rejected

    -
  • Returns string

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

Function SimpleParser

  • Invoked once per task.

    -

    Parameters

    • stdout: string

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

      -
    • stderr: undefined | string

      if defined, includes all text emitted to stderr.

      -
    • passed: boolean

      true iff the PASS pattern was found in stdout.

      -

    Returns string | Promise<string>

    Throws

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

    -

    See

    BatchProcessOptions

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

Function kill

  • Send a signal to the given process id.

    -

    Parameters

    • pid: undefined | number

      the process id. Required.

      -
    • force: boolean = false

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

      -

    Returns boolean

    Export

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

Function logger

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

Function pidExists

  • Parameters

    • pid: undefined | number

      process id. Required.

      -

    Returns boolean

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

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

Function pids

  • Only used by tests

    -

    Returns Promise<number[]>

    all the Process IDs in the process table.

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

Function setLogger

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

batch-cluster

batch-cluster

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

-

npm version -Build status -GitHub issues -CodeQL -Known Vulnerabilities

-

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

-

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

-

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

-

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

-

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

-

Installation

Depending on your yarn/npm preference:

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

Changelog

See CHANGELOG.md.

-

Usage

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

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

    -

    Note the constructor -options -takes a union type of

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

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

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

    -
  8. -
-

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

-

Caution

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

-

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

-

See issue #13 for details.

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

Interface BatchClusterEvents

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

-

See BatchClusterEmitter for more details.

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

Properties

beforeEnd: (() => void)

Emitted when this instance is in the process of ending.

-

Type declaration

    • (): void
    • Returns void

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

Emitted when a child process has ended

-

Type declaration

childStart: ((childProcess) => void)

Emitted when a child process has started

-

Type declaration

    • (childProcess): void
    • Parameters

      Returns void

end: (() => void)

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

-

Type declaration

    • (): void
    • Returns void

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

Emitted when a child process has an error during shutdown

-

Type declaration

    • (error, proc?): void
    • Parameters

      Returns void

fatalError: ((error) => void)

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

-

Type declaration

    • (error): void
    • Parameters

      • error: Error

      Returns void

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

Emitted when a process fails health checks

-

Type declaration

    • (error, proc): void
    • Parameters

      Returns void

internalError: ((error) => void)

Emitted when an internal consistency check fails

-

Type declaration

    • (error): void
    • Parameters

      • error: Error

      Returns void

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

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

-

Type declaration

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

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

      Returns void

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

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

-

Type declaration

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

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

-

Type declaration

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

      Returns void

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

Emitted when a task has an error

-

Type declaration

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

      Returns void

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

Emitted when a task has been resolved

-

Type declaration

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

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

-

Type declaration

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

      Returns void

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

Interface BatchProcessOptions

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

-

All fields must be set.

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

Properties

exitCommand?: string

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

-
fail: string | RegExp

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

-
healthCheckCommand?: string

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

-

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

-
pass: string | RegExp

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

-
versionCommand: string

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

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

Interface ChildProcessFactory

These are required parameters for a given BatchCluster.

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

Properties

Properties

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

Expected to be a simple call to execFile. Platform-specific code is the -responsibility of this thunk. Error handlers will be registered as -appropriate.

-

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

-

Type declaration

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

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

Interface Logger

Simple interface for logging.

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

Properties

Properties

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

Interface Parser<T>

Parser implementations convert stdout and stderr from the underlying child -process to a more useable format. This can be a no-op passthrough if no -parsing is necessary.

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

Type Parameters

  • T
  • Invoked once per task.

    -

    Parameters

    • stdout: string

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

      -
    • stderr: undefined | string

      if defined, includes all text emitted to stderr.

      -
    • passed: boolean

      true iff the PASS pattern was found in stdout.

      -

    Returns T | Promise<T>

    Throws

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

    -

    See

    BatchProcessOptions

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

Interface TypedEventEmitter<T>

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

Type Parameters

  • T

Methods

  • Type Parameters

    • E extends string | number | symbol

    Parameters

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

    Returns boolean

  • Type Parameters

    • E extends string | number | symbol

    Parameters

    • event: E

    Returns Function[]

  • Type Parameters

    • E extends string | number | symbol

    Parameters

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

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

          Returns void

    Returns this

  • Type Parameters

    • E extends string | number | symbol

    Parameters

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

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

          Returns void

    Returns this

  • Type Parameters

    • E extends string | number | symbol

    Parameters

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

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

          Returns void

    Returns this

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

Type alias BatchClusterEmitter

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

-

This approach has some benefits:

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

but has one drawback:

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

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

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

Type alias ChildExitReason

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

Type alias WhyNotHealthy

WhyNotHealthy: "broken" | "closed" | "ending" | "ended" | "idle" | "old" | "proc.close" | "proc.disconnect" | "proc.error" | "proc.exit" | "stderr.error" | "stderr" | "stdin.error" | "stdout.error" | "timeout" | "tooMany" | "startError" | "unhealthy" | "worn"
\ No newline at end of file diff --git a/docs/types/WhyNotReady.html b/docs/types/WhyNotReady.html deleted file mode 100644 index 1098b47..0000000 --- a/docs/types/WhyNotReady.html +++ /dev/null @@ -1 +0,0 @@ -Codestin Search App

Type alias WhyNotReady

WhyNotReady: WhyNotHealthy | "busy"
\ No newline at end of file diff --git a/docs/variables/ConsoleLogger.html b/docs/variables/ConsoleLogger.html deleted file mode 100644 index 347efea..0000000 --- a/docs/variables/ConsoleLogger.html +++ /dev/null @@ -1,12 +0,0 @@ -Codestin Search App

Variable ConsoleLoggerConst

ConsoleLogger: Logger = ...

Default Logger implementation.

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

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

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

Variable LogConst

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

Type declaration

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

        Returns any

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

        Returns any

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

Variable LogLevelsConst

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

Variable NoLoggerConst

NoLogger: Logger = ...

Logger that disables all logging.

-
\ No newline at end of file diff --git a/package.json b/package.json index 1dc5ed3..0ca87d7 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,9 @@ "watch": "rimraf dist & tsc --watch", "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": "serve docs", - "docs": "bash -c 'for i in {1..4} ; do npm run docs:$i ; done'" + "docs:build": "typedoc --options .typedoc.js", + "docs:serve": "cp .serve.json docs/serve.json && touch docs/.nojekyll && serve docs", + "docs": "npm run docs:build && npm run docs:serve" }, "release-it": { "src": { From 3330144d934bd208f1091189c8becada05601d4d Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 28 May 2025 10:31:47 -0700 Subject: [PATCH 02/60] chore(docs): remove warnings by exposing types that are part of the public API --- src/Args.ts | 2 ++ src/BatchCluster.ts | 34 +++++++++++++++++++++++++--------- src/BatchClusterEmitter.ts | 3 +-- src/Logger.ts | 14 +++++++------- src/Pids.ts | 1 - src/Task.ts | 2 +- 6 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/Args.ts diff --git a/src/Args.ts b/src/Args.ts new file mode 100644 index 0000000..8effb6e --- /dev/null +++ b/src/Args.ts @@ -0,0 +1,2 @@ + +export type Args = T extends (...args: infer A) => void ? A : never; diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index 50705fc..c68a8e8 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -1,25 +1,29 @@ import events from "node:events" import process from "node:process" import timers from "node:timers" +import type { Args } from "./Args" import { BatchClusterEmitter, BatchClusterEvents, ChildEndReason, TypedEventEmitter, } from "./BatchClusterEmitter" -import { BatchClusterOptions } from "./BatchClusterOptions" +import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator" +import type { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" import type { BatchClusterStats } from "./BatchClusterStats" -import { BatchProcessOptions } from "./BatchProcessOptions" +import type { BatchProcessOptions } from "./BatchProcessOptions" import type { ChildProcessFactory } from "./ChildProcessFactory" -import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" +import type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" import { Deferred } from "./Deferred" -import { Logger } from "./Logger" +import { HealthCheckStrategy } from "./HealthCheckStrategy" +import type { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { Logger, LoggerFunction } from "./Logger" import { verifyOptions } from "./OptionsVerifier" import { Parser } from "./Parser" -import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator" +import { HealthCheckable, ProcessHealthMonitor } from "./ProcessHealthMonitor" import { ProcessPoolManager } from "./ProcessPoolManager" import { validateProcpsAvailable } from "./ProcpsChecker" -import { Task } from "./Task" +import { Task, TaskOptions } from "./Task" import { TaskQueueManager } from "./TaskQueueManager" import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" @@ -33,18 +37,29 @@ export { ProcpsMissingError } from "./ProcpsChecker" export { Rate } from "./Rate" export { Task } from "./Task" export type { + Args, BatchClusterEmitter, BatchClusterEvents, BatchClusterStats, BatchProcessOptions, - ChildEndReason as ChildExitReason, + ChildEndReason, ChildProcessFactory, + CombinedBatchProcessOptions, + HealthCheckable, + HealthCheckStrategy, + InternalBatchProcessOptions, + LoggerFunction, Parser, + ProcessHealthMonitor, + TaskOptions, TypedEventEmitter, WhyNotHealthy, WhyNotReady, + WithObserver, } +export const a: HealthCheckable = {} as any + /** * BatchCluster instances manage 0 or more homogeneous child processes, and * provide the main interface for enqueuing `Task`s via `enqueueTask`. @@ -82,13 +97,14 @@ export class BatchCluster { 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, + maxReasonableProcessFailuresPerMinute: + this.options.maxReasonableProcessFailuresPerMinute, logger: this.#logger, }, () => this.#onIdleLater(), diff --git a/src/BatchClusterEmitter.ts b/src/BatchClusterEmitter.ts index b19668f..53fd4ce 100644 --- a/src/BatchClusterEmitter.ts +++ b/src/BatchClusterEmitter.ts @@ -1,9 +1,8 @@ +import { Args } from "./Args" import { BatchProcess } from "./BatchProcess" import { Task } from "./Task" import { WhyNotHealthy } from "./WhyNotHealthy" -type Args = T extends (...args: infer A) => void ? A : never - export type ChildEndReason = WhyNotHealthy | "tooMany" // Type-safe EventEmitter! Note that this interface is not comprehensive: diff --git a/src/Logger.ts b/src/Logger.ts index 2c4bd54..3ca4713 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -2,17 +2,17 @@ import util from "node:util" import { map } from "./Object" import { notBlank } from "./String" -type LogFunc = (message: string, ...optionalParams: unknown[]) => void +export type LoggerFunction = (message: string, ...optionalParams: unknown[]) => void /** * Simple interface for logging. */ export interface Logger { - trace: LogFunc - debug: LogFunc - info: LogFunc - warn: LogFunc - error: LogFunc + trace: LoggerFunction + debug: LoggerFunction + info: LoggerFunction + warn: LoggerFunction + error: LoggerFunction } export const LogLevels: (keyof Logger)[] = [ @@ -30,7 +30,7 @@ const noop = () => undefined /** * Default `Logger` implementation. * - * - `debug` and `info` go to {@link util.debuglog}("batch-cluster")`. + * - `debug` and `info` go to `util.debuglog("batch-cluster")`. * * - `warn` and `error` go to `console.warn` and `console.error`. * diff --git a/src/Pids.ts b/src/Pids.ts index 5cff0ec..7a670d2 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -29,7 +29,6 @@ export function pidExists(pid: number | undefined): boolean { /** * Send a signal to the given process id. * - * @export * @param pid the process id. Required. * @param force if true, and the current user has * permissions to send the signal, the pid will be forced to shut down. Defaults to false. diff --git a/src/Task.ts b/src/Task.ts index ddd5bc3..388eed3 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -3,7 +3,7 @@ import { Deferred } from "./Deferred" import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" import { Parser } from "./Parser" -type TaskOptions = Pick< +export type TaskOptions = Pick< InternalBatchProcessOptions, "streamFlushMillis" | "observer" | "passRE" | "failRE" | "logger" > From b3f69cd61d4e1848b203af9b8acd5a57f2a2bcc4 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 28 May 2025 10:32:24 -0700 Subject: [PATCH 03/60] chore: update @types/node and typescript-eslint to latest versions --- package-lock.json | 172 ++++++++++++++++++++++++++++------------------ package.json | 4 +- 2 files changed, 107 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 242cc94..fff3bd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^22.15.21", + "@types/node": "^22.15.23", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -38,7 +38,7 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.32.1" + "typescript-eslint": "^8.33.0" }, "engines": { "node": ">=20" @@ -566,9 +566,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "22.15.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.23.tgz", + "integrity": "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw==", "dev": true, "license": "MIT", "dependencies": { @@ -590,17 +590,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -614,7 +614,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -630,16 +630,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -654,15 +654,34 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "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==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -672,15 +691,32 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -697,9 +733,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -711,14 +747,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -777,16 +815,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -801,13 +839,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3647,19 +3685,6 @@ "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", @@ -4152,6 +4177,19 @@ "dev": true, "license": "ISC" }, + "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/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", @@ -5374,15 +5412,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 0ca87d7..1b6b349 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^22.15.21", + "@types/node": "^22.15.23", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -76,6 +76,6 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.32.1" + "typescript-eslint": "^8.33.0" } } From 26884af8c296fcd26b6893f220868aa94fd5fbcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:54:28 +0000 Subject: [PATCH 04/60] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 83b4e50..cf9c7d1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 87a95b7c8126dc93bcf5f17a886e93d900e922e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:25:21 +0000 Subject: [PATCH 05/60] Bump actions/setup-node from 4.2.0 to 4.4.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.2.0 to 4.4.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a...49933ea5288caeca8642d1e84afbd3f7d6820020) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/node.js.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index b28aef5..2f71da9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -14,7 +14,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: "20" - run: npm ci @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: ${{ matrix.node-version }} - run: npm ci From 59372113f72460327bb97ea472967e5d03b3e616 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 28 May 2025 10:34:46 -0700 Subject: [PATCH 06/60] chore: remove unused constant declaration for HealthCheckable --- src/BatchCluster.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index c68a8e8..9b3e81d 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -58,8 +58,6 @@ export type { WithObserver, } -export const a: HealthCheckable = {} as any - /** * BatchCluster instances manage 0 or more homogeneous child processes, and * provide the main interface for enqueuing `Task`s via `enqueueTask`. From a1b9805f56a5b4d50ddbd5e6dc8c40cf8130dad5 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Sat, 31 May 2025 10:44:21 -0700 Subject: [PATCH 07/60] chore: update dependencies for eslint and typescript --- .claude/settings.local.json | 9 +++++++-- package-lock.json | 29 +++++++++++++++++++++-------- package.json | 4 ++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4b46733..7326010 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,8 +20,13 @@ "Bash(timeout:*)", "Bash(gh run view:*)", "Bash(mkdir:*)", - "Bash(npm run docs:build:*)" + "Bash(npm run docs:build:*)", + "Bash(npm test:*)", + "Bash(/usr/bin/rg -n \"TODO|FIXME|XXX|HACK\" src/)", + "Bash(npx npm-check-updates)", + "Bash(gh repo view:*)" ], "deny": [] - } + }, + "enableAllProjectMcpServers": false } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fff3bd0..423bb4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "14.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.27.0", + "@eslint/js": "^9.28.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.23", + "@types/node": "^22.15.29", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -175,9 +175,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { @@ -566,9 +566,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.23.tgz", - "integrity": "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw==", + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2294,6 +2294,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/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/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", diff --git a/package.json b/package.json index 1b6b349..f8376a2 100644 --- a/package.json +++ b/package.json @@ -47,14 +47,14 @@ "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.27.0", + "@eslint/js": "^9.28.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.23", + "@types/node": "^22.15.29", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", From ff233f581e53bc3e5f8bf2489bfaceedfcf3aa61 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 13 Jun 2025 22:20:11 -0700 Subject: [PATCH 08/60] chore(art): logo --- README.md | 2 + doc/logo.png | Bin 0 -> 58821 bytes doc/logo.svg | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 doc/logo.png create mode 100644 doc/logo.svg diff --git a/README.md b/README.md index c1fc419..458d4d2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # batch-cluster +![PhotoStructure batch-cluster logo](https://raw.githubusercontent.com/photostructure/batch-cluster.js/main/doc/logo.svg) + **Efficient, concurrent work via batch-mode command-line tools from within Node.js.** [![npm version](https://img.shields.io/npm/v/batch-cluster.svg)](https://www.npmjs.com/package/batch-cluster) diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1988e8987f5a6959eafc06f4b4255f56bcfe586f GIT binary patch literal 58821 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 literal 0 HcmV?d00001 diff --git a/doc/logo.svg b/doc/logo.svg new file mode 100644 index 0000000..c1a0d1d --- /dev/null +++ b/doc/logo.svg @@ -0,0 +1,226 @@ + + + + From 1e1e4b267197fee16e1d43fe2210e3a8f54119a4 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 13 Jun 2025 22:26:13 -0700 Subject: [PATCH 09/60] chore: remove redundant header from README and add typedoc configuration --- README.md | 2 -- typedoc.json | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 typedoc.json diff --git a/README.md b/README.md index 458d4d2..733bfd3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# batch-cluster - ![PhotoStructure batch-cluster logo](https://raw.githubusercontent.com/photostructure/batch-cluster.js/main/doc/logo.svg) **Efficient, concurrent work via batch-mode command-line tools from within Node.js.** diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..2a87724 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,26 @@ +{ + "entryPoints": ["src/BatchCluster.ts"], + "out": "docs", + "includeVersion": false, + "excludePrivate": true, + "excludeInternal": true, + "readme": "README.md", + "githubPages": true, + "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/test/**"], + "tsconfig": "tsconfig.json", + "navigationLinks": { + "GitHub": "https://github.com/photostructure/batch-cluster.js", + "NPM": "https://www.npmjs.com/package/batch-cluster" + }, + "highlightLanguages": [ + "typescript", + "javascript", + "c++", + "cpp", + "c", + "json", + "bash", + "shell", + "powershell" + ] +} From b21840834d31b0e5119dcb0edccab95c0739a8ec Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 13 Jun 2025 22:27:04 -0700 Subject: [PATCH 10/60] chore: update devDependencies for eslint, mocha, typescript-eslint, and @types/node --- package-lock.json | 175 +++++++++++++++++++++++----------------------- package.json | 8 +-- 2 files changed, 93 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index 423bb4f..8ed3e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "14.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.28.0", + "@eslint/js": "^9.29.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.29", + "@types/node": "^24.0.1", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -26,7 +26,7 @@ "eslint": "^9.27.0", "eslint-plugin-import": "^2.31.0", "globals": "^16.2.0", - "mocha": "^11.5.0", + "mocha": "^11.6.0", "npm-check-updates": "^18.0.1", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", @@ -38,7 +38,7 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.33.0" + "typescript-eslint": "^8.34.0" }, "engines": { "node": ">=20" @@ -175,9 +175,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", "dev": true, "license": "MIT", "engines": { @@ -566,13 +566,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", - "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "version": "24.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", + "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -590,17 +590,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", - "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.33.0", - "@typescript-eslint/type-utils": "8.33.0", - "@typescript-eslint/utils": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -614,15 +614,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.33.0", + "@typescript-eslint/parser": "^8.34.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==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -630,16 +630,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", - "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.33.0", - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/typescript-estree": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "engines": { @@ -655,14 +655,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", - "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.33.0", - "@typescript-eslint/types": "^8.33.0", + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "engines": { @@ -671,17 +671,20 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", - "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0" + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -692,9 +695,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", - "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", "dev": true, "license": "MIT", "engines": { @@ -709,14 +712,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", - "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.33.0", - "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -733,9 +736,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", - "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", "dev": true, "license": "MIT", "engines": { @@ -747,16 +750,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", - "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.33.0", - "@typescript-eslint/tsconfig-utils": "8.33.0", - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0", + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -776,9 +779,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -815,16 +818,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", - "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.33.0", - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/typescript-estree": "8.33.0" + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -839,13 +842,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", - "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3775,9 +3778,9 @@ } }, "node_modules/mocha": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.5.0.tgz", - "integrity": "sha512-VKDjhy6LMTKm0WgNEdlY77YVsD49LZnPSXJAaPNL9NRYQADxvORsyG1DIQY6v53BKTnlNbEE2MbVCDbnxr4K3w==", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.6.0.tgz", + "integrity": "sha512-i0JVb+OUBqw63X/1pC3jCyJsqYisgxySBbsQa8TKvefpA1oEnw7JXxXnftfMHRsw7bEEVGRtVlHcDYXBa7FzVw==", "dev": true, "license": "MIT", "dependencies": { @@ -3797,7 +3800,7 @@ "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", + "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" @@ -5425,15 +5428,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", - "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz", + "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.33.0", - "@typescript-eslint/parser": "8.33.0", - "@typescript-eslint/utils": "8.33.0" + "@typescript-eslint/eslint-plugin": "8.34.0", + "@typescript-eslint/parser": "8.34.0", + "@typescript-eslint/utils": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5474,9 +5477,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, "license": "MIT" }, @@ -5650,9 +5653,9 @@ } }, "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", + "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", "dev": true, "license": "Apache-2.0" }, diff --git a/package.json b/package.json index f8376a2..17968b3 100644 --- a/package.json +++ b/package.json @@ -47,14 +47,14 @@ "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.28.0", + "@eslint/js": "^9.29.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.29", + "@types/node": "^24.0.1", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -64,7 +64,7 @@ "eslint": "^9.27.0", "eslint-plugin-import": "^2.31.0", "globals": "^16.2.0", - "mocha": "^11.5.0", + "mocha": "^11.6.0", "npm-check-updates": "^18.0.1", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", @@ -76,6 +76,6 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.33.0" + "typescript-eslint": "^8.34.0" } } From 929527201f6b6c6b426defbe1736f1adc7db2968 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Sat, 14 Jun 2025 09:18:39 -0700 Subject: [PATCH 11/60] chore(test): improve test stability on windows in BatchCluster.spec.ts --- .claude/settings.local.json | 9 ++++++++- src/BatchCluster.spec.ts | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7326010..0c90046 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,14 @@ "Bash(npm test:*)", "Bash(/usr/bin/rg -n \"TODO|FIXME|XXX|HACK\" src/)", "Bash(npx npm-check-updates)", - "Bash(gh repo view:*)" + "Bash(gh repo view:*)", + "Bash(gh issue list:*)", + "Bash(gh pr list:*)", + "Bash(gh run list:*)", + "Bash(for i in {1..5})", + "Bash(do echo \"Run $i:\")", + "Bash(break)", + "Bash(done)" ], "deny": [] }, diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index 0f09333..7f51d0b 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -440,7 +440,9 @@ describe("BatchCluster", function () { await bc.vacuumProcs() // Expect no prior pids to remain, as long as there were before-pids: - if (pids.length > 0) + // NOTE: On Windows, PIDs can be reused quickly, so we only check this + // on non-Windows platforms to avoid flakiness + if (pids.length > 0 && !isWin) expect(bc.pids()).to.not.include.members(pids) expect(bc.meanTasksPerProc).to.be.within( From 62a022fb5d4d3af0d42919fdc7c6b08cfd885cde Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 18 Jun 2025 18:29:09 -0700 Subject: [PATCH 12/60] chore(docs): merge configs --- .typedoc.js | 11 ----------- typedoc.json | 18 +++++++++--------- 2 files changed, 9 insertions(+), 20 deletions(-) delete mode 100644 .typedoc.js diff --git a/.typedoc.js b/.typedoc.js deleted file mode 100644 index 20e45b2..0000000 --- a/.typedoc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - name: "batch-cluster", - out: "./docs/", - readme: "./README.md", - gitRevision: "main", // < prevents docs from changing after every commit - exclude: ["**/*test*", "**/*spec*"], - excludePrivate: true, - entryPoints: [ - "./src/BatchCluster.ts", - ] -} diff --git a/typedoc.json b/typedoc.json index 2a87724..be76959 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,26 +1,26 @@ { "entryPoints": ["src/BatchCluster.ts"], - "out": "docs", + "out": "build/docs", + "name": "batch-cluster", "includeVersion": false, "excludePrivate": true, "excludeInternal": true, "readme": "README.md", "githubPages": true, - "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/test/**"], - "tsconfig": "tsconfig.json", + "exclude": ["**/*test*", "**/*spec*"], + "projectDocuments": ["CHANGELOG.md", "SECURITY.md", "LICENSE"], "navigationLinks": { "GitHub": "https://github.com/photostructure/batch-cluster.js", "NPM": "https://www.npmjs.com/package/batch-cluster" }, "highlightLanguages": [ - "typescript", + "bash", + "docker", + "dockerfile", "javascript", - "c++", - "cpp", - "c", "json", - "bash", "shell", - "powershell" + "typescript", + "yaml" ] } From 8f0ca36c59a89e6df8adc2ceef1c05a371a02a9b Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 18 Jun 2025 18:29:52 -0700 Subject: [PATCH 13/60] chore(pkg): set up automatic updates via precommit --- package-lock.json | 559 ++++++++++++++++++++++++++++++++++++++++------ package.json | 18 +- 2 files changed, 499 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ed3e2e..398eeb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.1", + "@types/node": "^24.0.3", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -26,8 +26,9 @@ "eslint": "^9.27.0", "eslint-plugin-import": "^2.31.0", "globals": "^16.2.0", - "mocha": "^11.6.0", + "mocha": "^11.7.0", "npm-check-updates": "^18.0.1", + "npm-run-all": "4.1.5", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", "rimraf": "^5.0.10", @@ -38,7 +39,7 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.34.0" + "typescript-eslint": "^8.34.1" }, "engines": { "node": ">=20" @@ -566,9 +567,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", - "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { @@ -590,17 +591,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", - "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", + "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/type-utils": "8.34.0", - "@typescript-eslint/utils": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/type-utils": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -614,7 +615,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.0", + "@typescript-eslint/parser": "^8.34.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -630,16 +631,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", - "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", + "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4" }, "engines": { @@ -655,14 +656,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", - "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", + "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.0", - "@typescript-eslint/types": "^8.34.0", + "@typescript-eslint/tsconfig-utils": "^8.34.1", + "@typescript-eslint/types": "^8.34.1", "debug": "^4.3.4" }, "engines": { @@ -677,14 +678,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", - "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0" + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -695,9 +696,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", - "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", + "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", "dev": true, "license": "MIT", "engines": { @@ -712,14 +713,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", - "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", + "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/utils": "8.34.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -736,9 +737,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", - "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", + "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", "dev": true, "license": "MIT", "engines": { @@ -750,16 +751,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", - "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.0", - "@typescript-eslint/tsconfig-utils": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -818,16 +819,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", - "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0" + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -842,14 +843,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", - "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1943,6 +1944,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.10", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", @@ -2285,9 +2296,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2807,6 +2818,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2918,6 +2936,13 @@ "he": "bin/he" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3005,6 +3030,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -3503,6 +3535,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3564,6 +3603,22 @@ "uc.micro": "^2.0.0" } }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3670,6 +3725,15 @@ "dev": true, "license": "MIT" }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3778,9 +3842,9 @@ } }, "node_modules/mocha": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.6.0.tgz", - "integrity": "sha512-i0JVb+OUBqw63X/1pC3jCyJsqYisgxySBbsQa8TKvefpA1oEnw7JXxXnftfMHRsw7bEEVGRtVlHcDYXBa7FzVw==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.0.tgz", + "integrity": "sha512-bXfLy/mI8n4QICg+pWj1G8VduX5vC0SHRwFpiR5/Fxc8S2G906pSfkyMmHVsdJNQJQNh3LE67koad9GzEvkV6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3879,6 +3943,36 @@ "node": ">= 0.6" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/npm-check-updates": { "version": "18.0.1", "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz", @@ -3894,6 +3988,183 @@ "npm": ">=8.12.1" } }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -4118,6 +4389,20 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4176,6 +4461,19 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -4206,6 +4504,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/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", @@ -4346,6 +4667,21 @@ "node": ">=0.10.0" } }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -4807,6 +5143,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -4917,6 +5266,42 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -4991,6 +5376,25 @@ "node": ">=8" } }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -5428,15 +5832,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz", - "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", + "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.34.0", - "@typescript-eslint/parser": "8.34.0", - "@typescript-eslint/utils": "8.34.0" + "@typescript-eslint/eslint-plugin": "8.34.1", + "@typescript-eslint/parser": "8.34.1", + "@typescript-eslint/utils": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5511,6 +5915,17 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 17968b3..ec22d67 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,20 @@ "scripts": { "ci": "npm ci", "clean": "rimraf dist", - "fmt": "prettier --write src/*.ts", + "fmt": "prettier --write .", "lint": "eslint src", "compile": "tsc", "watch": "rimraf dist & tsc --watch", - "pretest": "npm run clean && npm run lint && npm run compile", + "pretest": "run-s clean lint compile", "test": "mocha dist/**/*.spec.js", + "docs": "run-s docs:*", "docs:build": "typedoc --options .typedoc.js", "docs:serve": "cp .serve.json docs/serve.json && touch docs/.nojekyll && serve docs", - "docs": "npm run docs:build && npm run docs:serve" + "update": "run-p update:*", + "update:deps": "ncu -u --install always", + "install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest", + "update:actions": "pinact run -u", + "precommit": "npm i && run-s update test" }, "release-it": { "src": { @@ -54,7 +59,7 @@ "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.1", + "@types/node": "^24.0.3", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -64,8 +69,9 @@ "eslint": "^9.27.0", "eslint-plugin-import": "^2.31.0", "globals": "^16.2.0", - "mocha": "^11.6.0", + "mocha": "^11.7.0", "npm-check-updates": "^18.0.1", + "npm-run-all": "4.1.5", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", "rimraf": "^5.0.10", @@ -76,6 +82,6 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.34.0" + "typescript-eslint": "^8.34.1" } } From ca422b7943f29e2ae34ec85e9d5a50b80203fbe1 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 18 Jun 2025 18:30:04 -0700 Subject: [PATCH 14/60] pin github actions to SHAs --- .github/workflows/codeql-analysis.yml | 8 ++-- .github/workflows/docs.yml | 20 +++----- .github/workflows/node.js.yml | 67 +++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cf9c7d1..f92e953 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@11bd71901bbe5b1630ceea73d27597364c9af683 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 50a5a8c..4a0ebd4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,13 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "20.x" - cache: "npm" + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 - name: Install dependencies run: npm ci @@ -30,18 +27,13 @@ jobs: - name: Generate documentation run: npm run docs:build - - name: Prepare docs for deployment - run: | - cp .serve.json docs/serve.json - touch docs/.nojekyll - - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: - path: "docs" + path: "build/docs" # Deploy job deploy: @@ -63,4 +55,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 2f71da9..d9d749a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -8,13 +8,14 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: jobs: lint: runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: "20" - run: npm ci @@ -32,10 +33,68 @@ jobs: node-version: [20.x, 22.x, 23.x, 24.x] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test + + publish: + runs-on: ubuntu-24.04 + needs: [lint, build] + if: ${{ github.event_name == 'workflow_dispatch' }} + permissions: + contents: write + packages: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Fetch all history for proper versioning + fetch-depth: 0 + + - name: Use Node.js 20 + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Setup SSH Bot + uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v0.3.0 + with: + ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} + git-user-name: ${{ secrets.GIT_USER_NAME }} + git-user-email: ${{ secrets.GIT_USER_EMAIL }} + + - name: Install dependencies + run: npm ci + + - name: Build and test + run: npm test + + - name: Create release + run: | + # Bump version and create signed commit and tag + npm version patch -m "release: %s" + + # Push the version commit and tag + git push --follow-tags + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub release + run: | + # Get the version from package.json + VERSION=$(node -p "require('./package.json').version") + + # Create GitHub release + gh release create "v${VERSION}" \ + --title "Release v${VERSION}" \ + --generate-notes + env: + GH_TOKEN: ${{ github.token }} + From 4b261b8b798e958afd43ed610dc1d14b7afc8537 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 18 Jun 2025 20:43:40 -0700 Subject: [PATCH 15/60] chore(docs): fix script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ec22d67..7ca194e 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,13 @@ "pretest": "run-s clean lint compile", "test": "mocha dist/**/*.spec.js", "docs": "run-s docs:*", - "docs:build": "typedoc --options .typedoc.js", + "docs:build": "typedoc", "docs:serve": "cp .serve.json docs/serve.json && touch docs/.nojekyll && serve docs", "update": "run-p update:*", "update:deps": "ncu -u --install always", "install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest", "update:actions": "pinact run -u", - "precommit": "npm i && run-s update test" + "precommit": "npm i && run-s update docs:build test" }, "release-it": { "src": { From 3bc72ba99f4fa4f1c9105ff45fabc62809b4356e Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Tue, 24 Jun 2025 06:22:29 -0700 Subject: [PATCH 16/60] chore(docs): remove caution section regarding cleanupChildProcs (we don't need procps anymore) --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 733bfd3..68d9732 100644 --- a/README.md +++ b/README.md @@ -79,12 +79,4 @@ See for an example child process. Note that the script is _designed_ to be flaky on order to test BatchCluster's retry and error handling code. -## Caution - -The default `BatchClusterOptions.cleanupChildProcs` value of `true` means that BatchCluster will try to use `ps` to ensure Node's view of process state are correct, and that errant -processes are cleaned up. - -If you run this in a docker image based off Alpine or Debian Slim, **this won't work properly unless you install the `procps` package.** - -[See issue #13 for details.](https://github.com/photostructure/batch-cluster.js/issues/13) From 2da0f5d5876916dc6e24047ee30ea2acd47ffe55 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 26 Jun 2025 20:02:29 -0700 Subject: [PATCH 17/60] chore(pkg): ncu -u --- package-lock.json | 228 ++++++++++++++++++++++++++-------------------- package.json | 10 +- 2 files changed, 135 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index 398eeb1..5ff1fd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.3", + "@types/node": "^24.0.4", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -24,12 +24,12 @@ "chai-subset": "^1.6.0", "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "globals": "^16.2.0", - "mocha": "^11.7.0", + "mocha": "^11.7.1", "npm-check-updates": "^18.0.1", "npm-run-all": "4.1.5", - "prettier": "^3.5.3", + "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.1.0", "rimraf": "^5.0.10", "seedrandom": "^3.0.5", @@ -39,7 +39,7 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.34.1" + "typescript-eslint": "^8.35.0" }, "engines": { "node": ">=20" @@ -567,9 +567,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", "dev": true, "license": "MIT", "dependencies": { @@ -591,17 +591,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", - "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/type-utils": "8.34.1", - "@typescript-eslint/utils": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -615,7 +615,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.1", + "@typescript-eslint/parser": "^8.35.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -631,16 +631,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", - "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4" }, "engines": { @@ -656,14 +656,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", - "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.1", - "@typescript-eslint/types": "^8.34.1", + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", "debug": "^4.3.4" }, "engines": { @@ -678,14 +678,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", - "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1" + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -696,9 +696,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", - "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "dev": true, "license": "MIT", "engines": { @@ -713,14 +713,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", - "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -737,9 +737,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", - "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "dev": true, "license": "MIT", "engines": { @@ -751,16 +751,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", - "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.1", - "@typescript-eslint/tsconfig-utils": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -819,16 +819,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", - "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1" + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -843,13 +843,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", - "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/types": "8.35.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1071,18 +1071,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1955,9 +1957,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1988,7 +1990,9 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", @@ -2003,6 +2007,7 @@ "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -2207,9 +2212,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -2235,30 +2240,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -3251,6 +3256,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3842,9 +3860,9 @@ } }, "node_modules/mocha": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.0.tgz", - "integrity": "sha512-bXfLy/mI8n4QICg+pWj1G8VduX5vC0SHRwFpiR5/Fxc8S2G906pSfkyMmHVsdJNQJQNh3LE67koad9GzEvkV6g==", + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", "dev": true, "license": "MIT", "dependencies": { @@ -4548,9 +4566,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -5312,6 +5330,20 @@ "node": ">= 10.x" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5832,15 +5864,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", - "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.34.1", - "@typescript-eslint/parser": "8.34.1", - "@typescript-eslint/utils": "8.34.1" + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 7ca194e..53e60fb 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.3", + "@types/node": "^24.0.4", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -67,12 +67,12 @@ "chai-subset": "^1.6.0", "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "globals": "^16.2.0", - "mocha": "^11.7.0", + "mocha": "^11.7.1", "npm-check-updates": "^18.0.1", "npm-run-all": "4.1.5", - "prettier": "^3.5.3", + "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.1.0", "rimraf": "^5.0.10", "seedrandom": "^3.0.5", @@ -82,6 +82,6 @@ "ts-node": "^10.9.2", "typedoc": "^0.28.5", "typescript": "~5.8.3", - "typescript-eslint": "^8.34.1" + "typescript-eslint": "^8.35.0" } } From 56f9835447d0f1699a9c5ae10cf3ad842b967e1b Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 26 Jun 2025 20:03:07 -0700 Subject: [PATCH 18/60] chore(fmt) --- .claude/settings.local.json | 2 +- .github/dependabot.yml | 3 +- .github/workflows/codeql-analysis.yml | 66 +++++++++++----------- .github/workflows/node.js.yml | 19 +++---- .prettierrc | 4 +- .serve.json | 2 +- .vscode/launch.json | 14 ++--- .vscode/settings.json | 21 +++---- .vscode/tasks.json | 12 ++-- CHANGELOG.md | 2 - CLAUDE.md | 6 +- README.md | 5 +- SECURITY.md | 2 +- eslint.config.mjs | 14 ++--- src/Args.ts | 3 +- src/Logger.ts | 5 +- tsconfig.json | 68 ++++++++++++----------- types/chai-withintoleranceof/index.d.ts | 9 ++- types/chai-withintoleranceof/package.json | 2 +- 19 files changed, 124 insertions(+), 135 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0c90046..75cb4a9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,4 +36,4 @@ "deny": [] }, "enableAllProjectMcpServers": false -} \ No newline at end of file +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 445e8f3..f570414 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,7 @@ version: 2 updates: - - # Maintain dependencies for GitHub Actions + # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f92e953..e8a5f8e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,12 +7,12 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: [main] schedule: - - cron: '0 16 * * 3' + - cron: "0 16 * * 3" jobs: analyze: @@ -24,43 +24,43 @@ jobs: matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - language: ['javascript'] + language: ["javascript"] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 - # โ„น๏ธ Command-line programs to run using the OS shell. - # ๐Ÿ“š https://git.io/JvXDl + # โ„น๏ธ Command-line programs to run using the OS shell. + # ๐Ÿ“š https://git.io/JvXDl - # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index d9d749a..9a3be53 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -53,48 +53,47 @@ jobs: with: # Fetch all history for proper versioning fetch-depth: 0 - + - name: Use Node.js 20 uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: "20" registry-url: "https://registry.npmjs.org" - + - name: Setup SSH Bot uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v0.3.0 with: ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} git-user-name: ${{ secrets.GIT_USER_NAME }} git-user-email: ${{ secrets.GIT_USER_EMAIL }} - + - name: Install dependencies run: npm ci - + - name: Build and test run: npm test - + - name: Create release run: | # Bump version and create signed commit and tag npm version patch -m "release: %s" - + # Push the version commit and tag git push --follow-tags - + - name: Publish to npm run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - + - name: Create GitHub release run: | # Get the version from package.json VERSION=$(node -p "require('./package.json').version") - + # Create GitHub release gh release create "v${VERSION}" \ --title "Release v${VERSION}" \ --generate-notes env: GH_TOKEN: ${{ github.token }} - diff --git a/.prettierrc b/.prettierrc index 1135760..6235122 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,7 +7,5 @@ } } ], - "plugins": [ - "prettier-plugin-organize-imports" - ] + "plugins": ["prettier-plugin-organize-imports"] } diff --git a/.serve.json b/.serve.json index 1a05945..b308df8 100644 --- a/.serve.json +++ b/.serve.json @@ -1,3 +1,3 @@ { "cleanUrls": false -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 683625c..d6915ce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,22 +17,16 @@ "name": "Mocha Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { "type": "pwa-node", "request": "launch", "name": "Launch Program", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "program": "${workspaceFolder}/dist/BatchCluster.js", - "outFiles": [ - "${workspaceFolder}/**/*.js" - ] + "outFiles": ["${workspaceFolder}/**/*.js"] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 45e960a..bc9cf30 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,11 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.ignoreWords": [ - "Equalish", - "cygwin", - "debouncer", - "debouncing", - "rngseed" - ], - "cSpell.words": [ - "sinonjs", - "zombification" - ] + "typescript.tsdk": "node_modules/typescript/lib", + "cSpell.ignoreWords": [ + "Equalish", + "cygwin", + "debouncer", + "debouncing", + "rngseed" + ], + "cSpell.words": ["sinonjs", "zombification"] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 67fbbc1..115bedf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,25 +1,21 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 + // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "type": "typescript", "tsconfig": "tsconfig.json", - "problemMatcher": [ - "$tsc" - ], + "problemMatcher": ["$tsc"], "group": "build" }, { "type": "npm", "script": "watch", - "problemMatcher": [ - "$tsc" - ], + "problemMatcher": ["$tsc"], "label": "npm: watch", "detail": "rimraf dist & tsc --watch", "group": "build" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a533adb..50469c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,6 @@ See [Semver](http://semver.org/). - ๐Ÿ’” Several methods, including BatchCluster#pids() were changed from async to sync (as they were needlessly async). - ๐Ÿ“ฆ A number of timeout options can now be validly 0 to disable timeouts: - - `spawnTimeoutMillis` - `taskTimeoutMillis` @@ -257,7 +256,6 @@ See [Semver](http://semver.org/). ## v7.0.0 - ๐Ÿ’” Several fields were renamed to make things more consistent: - - `BatchCluster.pendingTasks` was renamed to `BatchCluster.pendingTaskCount`. - A new `BatchCluster.pendingTasks` method now matches `BatchCluster.currentTasks`, which both return `Task[]`. - `BatchCluster.busyProcs` was renamed to `busyProcCount`. diff --git a/CLAUDE.md b/CLAUDE.md index 4f53832..eeec5b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,12 +5,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Common Development Commands ### Build & Development + - `npm install` - Install dependencies - `npm run compile` - Compile TypeScript to JavaScript (outputs to dist/) - `npm run watch` - Watch mode for TypeScript compilation - `npm run clean` - Clean build artifacts ### Testing & Quality + - `npm test` - Run all tests (includes linting and compilation) - `npm run lint` - Run ESLint on TypeScript source files - `npm run fmt` - Format code with Prettier @@ -18,6 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `npx mocha --require ts-node/register src/Object.spec.ts` - Run individual tests like this ### Documentation + - `npm run docs` - Generate and serve TypeDoc documentation ## Architecture Overview @@ -51,6 +54,7 @@ This library manages clusters of child processes to efficiently handle batch ope ### Testing Approach The test suite uses a custom test script (`src/test.ts`) that simulates a batch-mode command-line tool with configurable failure rates. Tests can control: + - `failrate` - Probability of task failure - `rngseed` - Seed for deterministic randomness - `ignoreExit` - Whether to ignore termination signals @@ -66,4 +70,4 @@ The test suite uses a custom test script (`src/test.ts`) that simulates a batch- - **Null checks**: Always use explicit `x == null` or `x != null` checks. Do not use falsy/truthy checks for nullish values. - Good: `if (value != null)`, `if (value == null)` - - Bad: `if (value)`, `if (!value)` \ No newline at end of file + - Bad: `if (value)`, `if (!value)` diff --git a/README.md b/README.md index 68d9732..ef38df5 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ BatchCluster will ensure a given process is only given one task at a time. Note the [constructor options](https://photostructure.github.io/batch-cluster.js/classes/BatchCluster.html#constructor) takes a union type of - - [ChildProcessFactory](https://photostructure.github.io/batch-cluster.js/interfaces/ChildProcessFactory.html) and - [BatchProcessOptions](https://photostructure.github.io/batch-cluster.js/interfaces/BatchProcessOptions.html), @@ -59,7 +58,7 @@ BatchCluster will ensure a given process is only given one task at a time. - [BatchClusterOptions](https://photostructure.github.io/batch-cluster.js/classes/BatchClusterOptions.html), which has defaults that may or may not be relevant to your application. -1. The [default logger](https://photostructure.github.io/batch-cluster.js/interfaces/Logger.html) +1. The [default logger](https://photostructure.github.io/batch-cluster.js/interfaces/Logger.html) writes warning and error messages to `console.warn` and `console.error`. You can change this to your logger by using [setLogger](https://photostructure.github.io/batch-cluster.js/modules.html#setLogger) or by providing a logger to the `BatchCluster` constructor. @@ -78,5 +77,3 @@ See [src/test.ts](https://github.com/photostructure/batch-cluster.js/blob/main/src/test.ts) for an example child process. Note that the script is _designed_ to be flaky on order to test BatchCluster's retry and error handling code. - - diff --git a/SECURITY.md b/SECURITY.md index adfcc68..8d41a47 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Only the latest version of this library is supported. ## Reporting a Vulnerability -If you find a vulnerability, *or even think you have*, please send an email to +If you find a vulnerability, _or even think you have_, please send an email to the author. Each signed git release by `mceachen` contains a monitored email address. diff --git a/eslint.config.mjs b/eslint.config.mjs index 3ed8913..61dc251 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,8 +1,8 @@ // eslint.config.mjs import eslint from "@eslint/js"; +import importPlugin from "eslint-plugin-import"; import globals from "globals"; import tseslint from "typescript-eslint"; -import importPlugin from "eslint-plugin-import"; export default tseslint.config( { @@ -31,13 +31,13 @@ export default tseslint.config( }, rules: { // Project-specific preferences that differ from defaults - "eqeqeq": ["error", "always", { null: "ignore" }], // Allow == null for defensive coding + eqeqeq: ["error", "always", { null: "ignore" }], // Allow == null for defensive coding "@typescript-eslint/no-unnecessary-condition": "off", // We want defensive null checks "@typescript-eslint/prefer-optional-chain": "off", // Prefer explicit null checks for clarity - + // Import rules "import/no-cycle": "error", // TypeScript can't catch circular imports - + // Stricter than defaults "no-console": "error", }, @@ -50,7 +50,7 @@ export default tseslint.config( rules: { // Relax rules that are problematic for test files "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-non-null-assertion": "off", @@ -66,9 +66,9 @@ export default tseslint.config( "@typescript-eslint/restrict-plus-operands": "off", "no-console": "off", "@typescript-eslint/no-var-requires": "off", - + // Re-enable one valuable rule that's safe for tests "import/no-cycle": "error", // Circular imports are bad even in tests }, }, -); \ No newline at end of file +); diff --git a/src/Args.ts b/src/Args.ts index 8effb6e..4f89b2c 100644 --- a/src/Args.ts +++ b/src/Args.ts @@ -1,2 +1 @@ - -export type Args = T extends (...args: infer A) => void ? A : never; +export type Args = T extends (...args: infer A) => void ? A : never diff --git a/src/Logger.ts b/src/Logger.ts index 3ca4713..98bf2ef 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -2,7 +2,10 @@ import util from "node:util" import { map } from "./Object" import { notBlank } from "./String" -export type LoggerFunction = (message: string, ...optionalParams: unknown[]) => void +export type LoggerFunction = ( + message: string, + ...optionalParams: unknown[] +) => void /** * Simple interface for logging. diff --git a/tsconfig.json b/tsconfig.json index d3eac13..d347c60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,10 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["ES2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "target": "ES2019" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "ES2019" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -24,9 +26,9 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -42,13 +44,13 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - "removeComments": false, /* Disable emitting comments. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "removeComments": false /* Disable emitting comments. */, // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ @@ -66,35 +68,35 @@ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ /* Interop Constraints */ - "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - "noPropertyAccessFromIndexSignature": false, /* Enforces using indexed accessors for keys declared using an indexed type */ - "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ - "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */, + "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, + "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, + "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, + "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, + "noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */, + "useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */, + "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, + "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, + "noUnusedParameters": true /* Raise an error when a function parameter isn't read */, + "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */, + "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, + "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, + "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, + "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, + "noPropertyAccessFromIndexSignature": false /* Enforces using indexed accessors for keys declared using an indexed type */, + "allowUnusedLabels": false /* Disable error reporting for unused labels. */, + "allowUnreachableCode": false /* Disable error reporting for unreachable code. */, /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } diff --git a/types/chai-withintoleranceof/index.d.ts b/types/chai-withintoleranceof/index.d.ts index 77a914c..73882d6 100644 --- a/types/chai-withintoleranceof/index.d.ts +++ b/types/chai-withintoleranceof/index.d.ts @@ -10,8 +10,11 @@ interface WithinTolerance { } declare namespace Chai { - interface Assertion extends LanguageChains, NumericComparison, TypeComparison { - withinToleranceOf: WithinTolerance; - withinTolOf: WithinTolerance; + interface Assertion + extends LanguageChains, + NumericComparison, + TypeComparison { + withinToleranceOf: WithinTolerance + withinTolOf: WithinTolerance } } diff --git a/types/chai-withintoleranceof/package.json b/types/chai-withintoleranceof/package.json index 810deb2..cd0a1b9 100644 --- a/types/chai-withintoleranceof/package.json +++ b/types/chai-withintoleranceof/package.json @@ -2,4 +2,4 @@ "name": "@types/chai-withintoleranceof", "version": "0.0.1", "types": "./index.d.ts" -} \ No newline at end of file +} From f91b0c2556aa2f96a956816b1589a3be81767d36 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 26 Jun 2025 20:10:29 -0700 Subject: [PATCH 19/60] chore: update git-ssh-signing-action to v1.0.0 --- .github/workflows/node.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 9a3be53..cfbd562 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -61,7 +61,7 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Setup SSH Bot - uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v0.3.0 + uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v1.0.0 with: ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} git-user-name: ${{ secrets.GIT_USER_NAME }} From 4da68037e12fbaa2f3312d510adb17988f2ca96c Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 27 Jun 2025 09:50:30 -0700 Subject: [PATCH 20/60] chore(gha): pinact --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/docs.yml | 2 +- .github/workflows/node.js.yml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e8a5f8e..8f18e8f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/init@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/autobuild@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/analyze@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4a0ebd4..dc60f45 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Node.js - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - name: Install dependencies run: npm ci diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index cfbd562..277890e 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -15,7 +15,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "20" - run: npm ci @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Use Node.js 20 - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "20" registry-url: "https://registry.npmjs.org" From c4d76efc4f1a049654de247afabbddb5adb3c1ce Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 27 Jun 2025 09:50:50 -0700 Subject: [PATCH 21/60] chore(pkg): ncu --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ff1fd4..8e7fa87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", - "typedoc": "^0.28.5", + "typedoc": "^0.28.6", "typescript": "~5.8.3", "typescript-eslint": "^8.35.0" }, @@ -5800,9 +5800,9 @@ } }, "node_modules/typedoc": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz", - "integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==", + "version": "0.28.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.6.tgz", + "integrity": "sha512-2VvfK6z3yupcu75qZB00LGICg4qa0lw4yPBrFcnZgqIMwpLjLWopTqNeJ4SSS/s92myvWBECY5zcOSMqpvW3CA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 53e60fb..b33bd15 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", - "typedoc": "^0.28.5", + "typedoc": "^0.28.6", "typescript": "~5.8.3", "typescript-eslint": "^8.35.0" } From 6a9be5b0e52ed4b56241ca0d845af3e861750b5b Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 27 Jun 2025 09:50:58 -0700 Subject: [PATCH 22/60] chore(dep): shush --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f570414..315c7ff 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,9 +10,17 @@ updates: directory: "/" schedule: interval: "weekly" + open-pull-requests-limit: 0 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] # Maintain dependencies for npm - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" + open-pull-requests-limit: 0 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] From 920e4629b947b46761f6c392291ba629d56e382f Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 10:47:30 -0700 Subject: [PATCH 23/60] chore: update GitHub Actions to use latest versions of checkout and setup-node --- .github/workflows/codeql-analysis.yml | 8 ++++---- .github/workflows/docs.yml | 6 +++--- .github/workflows/node.js.yml | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8f18e8f..f1224cf 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1 + uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1 + uses: github/codeql-action/autobuild@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1 + uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index dc60f45..1a73b80 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 - name: Install dependencies run: npm ci @@ -31,7 +31,7 @@ jobs: uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 with: path: "build/docs" diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 277890e..0c4c751 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -14,8 +14,8 @@ jobs: lint: runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: "20" - run: npm ci @@ -33,9 +33,9 @@ jobs: node-version: [20.x, 22.x, 23.x, 24.x] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -49,13 +49,13 @@ jobs: contents: write packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # Fetch all history for proper versioning fetch-depth: 0 - name: Use Node.js 20 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: "20" registry-url: "https://registry.npmjs.org" From 9fde021f01adc223ffedd820872b1ad552fa1227 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 10:47:49 -0700 Subject: [PATCH 24/60] chore: update devDependencies to latest versions --- package-lock.json | 254 +++++++++++++++++++++++----------------------- package.json | 16 +-- 2 files changed, 136 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e7fa87..fda3f5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "14.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.29.0", + "@eslint/js": "^9.33.0", "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7", "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.4", + "@types/node": "^24.3.0", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -25,21 +25,21 @@ "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", "eslint-plugin-import": "^2.32.0", - "globals": "^16.2.0", + "globals": "^16.3.0", "mocha": "^11.7.1", - "npm-check-updates": "^18.0.1", + "npm-check-updates": "^18.0.2", "npm-run-all": "4.1.5", "prettier": "^3.6.2", - "prettier-plugin-organize-imports": "^4.1.0", + "prettier-plugin-organize-imports": "^4.2.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.6", - "typescript": "~5.8.3", - "typescript-eslint": "^8.35.0" + "typedoc": "^0.28.10", + "typescript": "~5.9.2", + "typescript-eslint": "^8.40.0" }, "engines": { "node": ">=20" @@ -176,9 +176,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", - "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -213,16 +213,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.2.tgz", - "integrity": "sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.11.0.tgz", + "integrity": "sha512-ooCDMAOKv71O7MszbXjSQGcI6K5T6NKlemQZOBHLq7Sv/oXCRfYbZ7UgbzFdl20lSXju6Juds4I3y30R6rHA4Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.4.2", - "@shikijs/langs": "^3.4.2", - "@shikijs/themes": "^3.4.2", - "@shikijs/types": "^3.4.2", + "@shikijs/engine-oniguruma": "^3.11.0", + "@shikijs/langs": "^3.11.0", + "@shikijs/themes": "^3.11.0", + "@shikijs/types": "^3.11.0", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -395,40 +395,40 @@ "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==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.11.0.tgz", + "integrity": "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.4.2", + "@shikijs/types": "3.11.0", "@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==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.11.0.tgz", + "integrity": "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.4.2" + "@shikijs/types": "3.11.0" } }, "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==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.11.0.tgz", + "integrity": "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.4.2" + "@shikijs/types": "3.11.0" } }, "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==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.11.0.tgz", + "integrity": "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==", "dev": true, "license": "MIT", "dependencies": { @@ -567,13 +567,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", - "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -591,17 +591,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", - "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/type-utils": "8.35.0", - "@typescript-eslint/utils": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -615,9 +615,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.0", + "@typescript-eslint/parser": "^8.40.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -631,16 +631,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", - "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/typescript-estree": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4" }, "engines": { @@ -652,18 +652,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", - "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.0", - "@typescript-eslint/types": "^8.35.0", + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "engines": { @@ -674,18 +674,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", - "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0" + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -696,9 +696,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", - "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", "dev": true, "license": "MIT", "engines": { @@ -709,18 +709,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", - "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.0", - "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -733,13 +734,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", - "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", "dev": true, "license": "MIT", "engines": { @@ -751,16 +752,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", - "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.0", - "@typescript-eslint/tsconfig-utils": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -776,7 +777,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -819,16 +820,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", - "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/typescript-estree": "8.35.0" + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -839,17 +840,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", - "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2781,9 +2782,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -3992,9 +3993,9 @@ } }, "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==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.2.tgz", + "integrity": "sha512-9uVFZUCg5oDOcbzdsrJ4BEvq2gikd23tXuF5mqpl4mxVl051lzB00Xmd7ZVjVWY3XNUF3BQKWlN/qmyD8/bwrA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4582,15 +4583,15 @@ } }, "node_modules/prettier-plugin-organize-imports": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz", - "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.2.0.tgz", + "integrity": "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==", "dev": true, "license": "MIT", "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", - "vue-tsc": "^2.1.0" + "vue-tsc": "^2.1.0 || 3" }, "peerDependenciesMeta": { "vue-tsc": { @@ -5800,17 +5801,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.6", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.6.tgz", - "integrity": "sha512-2VvfK6z3yupcu75qZB00LGICg4qa0lw4yPBrFcnZgqIMwpLjLWopTqNeJ4SSS/s92myvWBECY5zcOSMqpvW3CA==", + "version": "0.28.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.10.tgz", + "integrity": "sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.2.2", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.7.1" + "yaml": "^2.8.0" }, "bin": { "typedoc": "bin/typedoc" @@ -5820,7 +5821,7 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -5850,9 +5851,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5864,15 +5865,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", - "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", + "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.35.0", - "@typescript-eslint/parser": "8.35.0", - "@typescript-eslint/utils": "8.35.0" + "@typescript-eslint/eslint-plugin": "8.40.0", + "@typescript-eslint/parser": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5883,7 +5885,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/uc.micro": { @@ -5913,9 +5915,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index b33bd15..f3c70fb 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,14 @@ "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.29.0", + "@eslint/js": "^9.33.0", "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7", "@types/chai-string": "^1.4.5", "@types/chai-subset": "^1.3.6", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.4", + "@types/node": "^24.3.0", "@types/sinonjs__fake-timers": "^8.1.5", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", @@ -68,20 +68,20 @@ "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", "eslint-plugin-import": "^2.32.0", - "globals": "^16.2.0", + "globals": "^16.3.0", "mocha": "^11.7.1", - "npm-check-updates": "^18.0.1", + "npm-check-updates": "^18.0.2", "npm-run-all": "4.1.5", "prettier": "^3.6.2", - "prettier-plugin-organize-imports": "^4.1.0", + "prettier-plugin-organize-imports": "^4.2.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.6", - "typescript": "~5.8.3", - "typescript-eslint": "^8.35.0" + "typedoc": "^0.28.10", + "typescript": "~5.9.2", + "typescript-eslint": "^8.40.0" } } From 856d641e858bb61b96a44aa847b05866ebb9d488 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 11:11:09 -0700 Subject: [PATCH 25/60] fix(procps): remove unused ProcpsChecker and related tests; refactor BatchCluster to eliminate procps dependency. Fixes #58 --- CHANGELOG.md | 4 +++ src/BatchCluster.procps.spec.ts | 21 ------------- src/BatchCluster.spec.ts | 6 ++-- src/BatchCluster.ts | 3 -- src/Pids.ts | 48 ------------------------------ src/ProcpsChecker.spec.ts | 33 --------------------- src/ProcpsChecker.ts | 52 --------------------------------- src/_chai.spec.ts | 15 +++++----- 8 files changed, 15 insertions(+), 167 deletions(-) delete mode 100644 src/BatchCluster.procps.spec.ts delete mode 100644 src/ProcpsChecker.spec.ts delete mode 100644 src/ProcpsChecker.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 50469c3..93be554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ See [Semver](http://semver.org/). - ๐Ÿ“ฆ Minor packaging changes +## v15.0.0 + +- ๐Ÿ’” Deleted the standalone `pids()` function and associated code (including the ProcpsChecker). This function was exported but only used internally by tests. This fixes the [issue #58](https://github.com/photostructure/batch-cluster.js/issues/58) (by deleting the unused code! _the best kind of bugfix_). Thanks for the report, [Zaczero](https://github.com/Zaczero)! + ## v14.0.0 - ๐Ÿ’” Dropped official support for Node v14, v16, and v18. Minimum Node.js version is now v20. diff --git a/src/BatchCluster.procps.spec.ts b/src/BatchCluster.procps.spec.ts deleted file mode 100644 index 281b251..0000000 --- a/src/BatchCluster.procps.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from "chai" -import { describe, it } from "mocha" -import { BatchCluster, ProcpsMissingError } from "./BatchCluster" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { processFactory } from "./_chai.spec" - -describe("BatchCluster procps validation", () => { - it("should validate procps availability during construction", () => { - // This test verifies that BatchCluster calls validateProcpsAvailable() - // On systems where procps is available (like our test environment), - // construction should succeed - expect(() => { - new BatchCluster({ ...DefaultTestOptions, processFactory }) - }).to.not.throw() - }) - - it("should export ProcpsMissingError for user handling", () => { - expect(ProcpsMissingError).to.be.a("function") - expect(new ProcpsMissingError().name).to.equal("ProcpsMissingError") - }) -}) diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index 7f51d0b..ff0ba6b 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -1,13 +1,13 @@ import FakeTimers from "@sinonjs/fake-timers" import process from "node:process" import { + childProcs, currentTestPids, expect, flatten, parser, parserErrors, processFactory, - procs, setFailratePct, setIgnoreExit, setNewline, @@ -281,7 +281,7 @@ describe("BatchCluster", function () { setNewline(newline as any) setIgnoreExit(ignoreExit) bc = listen(new BatchCluster({ ...opts, processFactory })) - procs.length = 0 + childProcs.length = 0 }) afterEach(async () => { @@ -424,7 +424,7 @@ describe("BatchCluster", function () { // Expect a reasonable number of new pids. Worst case, we // errored after every start, so there may be more then iters // pids spawned. - expect(procs.length).to.eql(bc.spawnedProcCount) + expect(childProcs.length).to.eql(bc.spawnedProcCount) expect(bc.spawnedProcCount).to.be.within( results.length / opts.maxTasksPerProcess, diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index 9b3e81d..3223908 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -84,9 +84,6 @@ 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 diff --git a/src/Pids.ts b/src/Pids.ts index 7a670d2..9f9903a 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -1,9 +1,3 @@ -import child_process from "node:child_process" -import { existsSync } from "node:fs" -import { readdir } from "node:fs/promises" -import { asError } from "./Error" -import { isWin } from "./Platform" - /** * @param {number} pid process id. Required. * @returns boolean true if the given process id is in the local process @@ -43,45 +37,3 @@ export function kill(pid: number | undefined, force = false): boolean { // failed to get priority--assume the pid is gone. } } - -/** - * Only used by tests - * - * @returns {Promise} all the Process IDs in the process table. - */ -export async function pids(): Promise { - // Linuxโ€style: read /proc - if (!isWin && existsSync("/proc")) { - const names = await readdir("/proc") - return names.filter((d) => /^\d+$/.test(d)).map((d) => parseInt(d, 10)) - } - - // fallback: ps or tasklist - const cmd = isWin ? "tasklist" : "ps" - const args = isWin ? ["/NH", "/FO", "CSV"] : ["-e", "-o", "pid="] - - return new Promise((resolve, reject) => { - child_process.execFile(cmd, args, (err, stdout, stderr) => { - if (err) return reject(asError(err)) - if (stderr.trim()) return reject(new Error(stderr)) - - const pids = stdout - .trim() - .split(/[\r\n]+/) - .map((line) => { - if (isWin) { - // "Image","PID",โ€ฆ - // split on "," and strip outer quotes: - const cols = line.split('","') - const pidStr = cols[1]?.replace(/"/g, "") - return Number(pidStr) - } - // ps -o pid= gives you just the number - return Number(line.trim()) - }) - .filter((n) => Number.isFinite(n) && n > 0) - - resolve(pids) - }) - }) -} diff --git a/src/ProcpsChecker.spec.ts b/src/ProcpsChecker.spec.ts deleted file mode 100644 index 5becc76..0000000 --- a/src/ProcpsChecker.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from "chai" -import { describe, it } from "mocha" -import { ProcpsMissingError, validateProcpsAvailable } from "./ProcpsChecker" - -describe("ProcpsChecker", () => { - describe("validateProcpsAvailable()", () => { - it("should not throw on systems with procps installed", () => { - // Since we're running tests, procps should be available - expect(() => validateProcpsAvailable()).to.not.throw() - }) - - it("should create appropriate error message for platform", () => { - const error = new ProcpsMissingError() - expect(error.name).to.equal("ProcpsMissingError") - expect(error.message).to.include("command not available") - - // Message should be specific to platform - if (process.platform === "win32") { - expect(error.message).to.include("tasklist") - } else { - expect(error.message).to.include("ps") - expect(error.message).to.include("procps") - } - }) - - it("should preserve original error", () => { - const originalError = new Error("Command failed") - const procpsError = new ProcpsMissingError(originalError) - - expect(procpsError.originalError).to.equal(originalError) - }) - }) -}) diff --git a/src/ProcpsChecker.ts b/src/ProcpsChecker.ts deleted file mode 100644 index 85a9fa1..0000000 --- a/src/ProcpsChecker.ts +++ /dev/null @@ -1,52 +0,0 @@ -import child_process from "node:child_process" -import { existsSync, readdirSync } from "node:fs" -import { isWin } from "./Platform" - -/** - * Error thrown when procps is missing on non-Windows systems - */ -export class ProcpsMissingError extends Error { - readonly originalError?: Error - - constructor(originalError?: Error) { - const message = isWin - ? "tasklist command not available" - : "ps command not available. Please install procps package (e.g., 'apt-get install procps' on Ubuntu/Debian)" - - super(message) - this.name = "ProcpsMissingError" - - if (originalError != null) { - this.originalError = originalError - } - } -} - -/** - * Check if the required process listing command is available - * @throws {ProcpsMissingError} if the command is not available - */ -export function validateProcpsAvailable(): void { - // on POSIX systems with a working /proc we can skip ps entirely - if (!isWin && existsSync("/proc")) { - const entries = readdirSync("/proc") - // if we see at least one numeric directory, assume /proc is usable - if (entries.some((d) => /^\d+$/.test(d))) { - return - } - // fall through to check `ps` if /proc is empty or unusable - } - - try { - const command = isWin ? "tasklist" : "ps" - const args = isWin ? ["/NH", "/FO", "CSV", "/FI", "PID eq 1"] : ["-p", "1"] - const timeout = isWin ? 15_000 : 5_000 // 15s for Windows, 5s elsewhere - - child_process.execFileSync(command, args, { - stdio: "pipe", - timeout, - }) - } catch (err) { - throw new ProcpsMissingError(err instanceof Error ? err : undefined) - } -} diff --git a/src/_chai.spec.ts b/src/_chai.spec.ts index e52fe78..724d883 100644 --- a/src/_chai.spec.ts +++ b/src/_chai.spec.ts @@ -11,7 +11,7 @@ import path from "node:path" import process from "node:process" import { Log, logger, setLogger } from "./Logger" import { Parser } from "./Parser" -import { pids } from "./Pids" +import { pidExists } from "./Pids" import { notBlank } from "./String" use(require("chai-as-promised")) @@ -102,15 +102,16 @@ declare namespace Chai { } } -export const procs: child_process.ChildProcess[] = [] +export const childProcs: child_process.ChildProcess[] = [] export function testPids(): number[] { - return procs.map((proc) => proc.pid).filter((ea) => ea != null) as number[] + return childProcs + .map((proc) => proc.pid) + .filter((ea) => ea != null) as number[] } -export async function currentTestPids(): Promise { - const alivePids = new Set(await pids()) - return testPids().filter((ea) => alivePids.has(ea)) +export function currentTestPids(): number[] { + return testPids().filter(pidExists) } export function sortNumeric(arr: number[]): number[] { @@ -198,6 +199,6 @@ export const processFactory = () => { }, }, ) - procs.push(proc) + childProcs.push(proc) return proc } From 94cd48a17ca974bbfe8b48ed6e2b38eec15278ba Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 11:11:16 -0700 Subject: [PATCH 26/60] fix(docs): update serve command to use the correct build path for documentation --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3c70fb..951b521 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "test": "mocha dist/**/*.spec.js", "docs": "run-s docs:*", "docs:build": "typedoc", - "docs:serve": "cp .serve.json docs/serve.json && touch docs/.nojekyll && serve docs", + "docs:serve": "cp .serve.json build/docs/serve.json && touch build/docs/.nojekyll && serve build/docs", "update": "run-p update:*", "update:deps": "ncu -u --install always", "install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest", From fff8058af481570a32de4caa1f21d4ef60646e37 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 11:11:32 -0700 Subject: [PATCH 27/60] chore(fmt) --- .github/dependabot.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 315c7ff..c0cf0be 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,8 @@ updates: open-pull-requests-limit: 0 ignore: - dependency-name: "*" - update-types: ["version-update:semver-minor", "version-update:semver-patch"] + update-types: + ["version-update:semver-minor", "version-update:semver-patch"] # Maintain dependencies for npm - package-ecosystem: "npm" @@ -23,4 +24,5 @@ updates: open-pull-requests-limit: 0 ignore: - dependency-name: "*" - update-types: ["version-update:semver-minor", "version-update:semver-patch"] + update-types: + ["version-update:semver-minor", "version-update:semver-patch"] From 8e541506ddb9901eceb4d1375c5e9a4af11f92d6 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 11:12:29 -0700 Subject: [PATCH 28/60] chore(fmt): use inline export type {} from ... instead of import/export type --- src/BatchCluster.ts | 57 ++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index 3223908..c352622 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -1,62 +1,51 @@ import events from "node:events" import process from "node:process" import timers from "node:timers" -import type { Args } from "./Args" -import { - BatchClusterEmitter, - BatchClusterEvents, - ChildEndReason, - TypedEventEmitter, -} from "./BatchClusterEmitter" +import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter" import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator" -import type { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" +import type { BatchClusterOptions } from "./BatchClusterOptions" import type { BatchClusterStats } from "./BatchClusterStats" import type { BatchProcessOptions } from "./BatchProcessOptions" import type { ChildProcessFactory } from "./ChildProcessFactory" import type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" import { Deferred } from "./Deferred" -import { HealthCheckStrategy } from "./HealthCheckStrategy" -import type { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { Logger, LoggerFunction } from "./Logger" +import { Logger } from "./Logger" import { verifyOptions } from "./OptionsVerifier" -import { Parser } from "./Parser" -import { HealthCheckable, ProcessHealthMonitor } from "./ProcessHealthMonitor" import { ProcessPoolManager } from "./ProcessPoolManager" -import { validateProcpsAvailable } from "./ProcpsChecker" -import { Task, TaskOptions } from "./Task" +import { Task } from "./Task" import { TaskQueueManager } from "./TaskQueueManager" -import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" export { BatchClusterOptions } from "./BatchClusterOptions" export { BatchProcess } from "./BatchProcess" export { Deferred } from "./Deferred" export * from "./Logger" export { SimpleParser } from "./Parser" -export { kill, pidExists, pids } from "./Pids" -export { ProcpsMissingError } from "./ProcpsChecker" +export { kill, pidExists } from "./Pids" export { Rate } from "./Rate" export { Task } from "./Task" +// Type exports organized by source module +export type { Args } from "./Args" export type { - Args, BatchClusterEmitter, BatchClusterEvents, - BatchClusterStats, - BatchProcessOptions, ChildEndReason, - ChildProcessFactory, - CombinedBatchProcessOptions, + TypedEventEmitter, +} from "./BatchClusterEmitter" +export type { WithObserver } from "./BatchClusterOptions" +export type { BatchClusterStats } from "./BatchClusterStats" +export type { BatchProcessOptions } from "./BatchProcessOptions" +export type { ChildProcessFactory } from "./ChildProcessFactory" +export type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" +export type { HealthCheckStrategy } from "./HealthCheckStrategy" +export type { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +export type { LoggerFunction } from "./Logger" +export type { Parser } from "./Parser" +export type { HealthCheckable, - HealthCheckStrategy, - InternalBatchProcessOptions, - LoggerFunction, - Parser, ProcessHealthMonitor, - TaskOptions, - TypedEventEmitter, - WhyNotHealthy, - WhyNotReady, - WithObserver, -} +} from "./ProcessHealthMonitor" +export type { TaskOptions } from "./Task" +export type { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" /** * BatchCluster instances manage 0 or more homogeneous child processes, and @@ -240,7 +229,7 @@ export class BatchCluster { /** * @return the current pending Tasks (mostly for testing) */ - get pendingTasks() { + get pendingTasks(): readonly Task[] { return this.#taskQueue.pendingTasks } From 53bd6f2af79355788790657b00b14168760d191a Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 11:14:19 -0700 Subject: [PATCH 29/60] chore(fmt): accept prettier defaults (enables semicolons) --- .prettierrc | 8 - .vscode/settings.json | 2 +- CHANGELOG.md | 2 + src/Args.ts | 2 +- src/Array.spec.ts | 68 +-- src/Array.ts | 20 +- src/Async.ts | 20 +- src/BatchCluster.spec.ts | 746 ++++++++++++----------- src/BatchCluster.ts | 212 +++---- src/BatchClusterEmitter.ts | 52 +- src/BatchClusterEventCoordinator.spec.ts | 378 ++++++------ src/BatchClusterEventCoordinator.ts | 88 +-- src/BatchClusterOptions.spec.ts | 60 +- src/BatchClusterOptions.ts | 44 +- src/BatchClusterStats.ts | 26 +- src/BatchProcess.ts | 256 ++++---- src/BatchProcessOptions.ts | 10 +- src/ChildProcessFactory.ts | 4 +- src/CombinedBatchProcessOptions.ts | 8 +- src/DefaultTestOptions.spec.ts | 6 +- src/Deferred.spec.ts | 98 +-- src/Deferred.ts | 62 +- src/Error.ts | 8 +- src/HealthCheckStrategy.ts | 46 +- src/InternalBatchProcessOptions.ts | 8 +- src/Logger.ts | 68 +-- src/Mean.ts | 26 +- src/Mutex.ts | 34 +- src/Object.spec.ts | 24 +- src/Object.ts | 16 +- src/OptionsVerifier.ts | 54 +- src/Parser.ts | 12 +- src/Pids.ts | 16 +- src/Platform.ts | 10 +- src/ProcessHealthMonitor.spec.ts | 381 ++++++------ src/ProcessHealthMonitor.ts | 122 ++-- src/ProcessPoolManager.spec.ts | 275 ++++----- src/ProcessPoolManager.ts | 158 ++--- src/ProcessTerminator.spec.ts | 464 +++++++------- src/ProcessTerminator.ts | 70 +-- src/Rate.spec.ts | 100 +-- src/Rate.ts | 58 +- src/Stream.ts | 6 +- src/StreamHandler.spec.ts | 480 +++++++-------- src/StreamHandler.ts | 80 +-- src/String.ts | 12 +- src/Task.ts | 86 +-- src/TaskQueueManager.spec.ts | 300 ++++----- src/TaskQueueManager.ts | 62 +- src/Timeout.ts | 28 +- src/WhyNotHealthy.ts | 4 +- src/_chai.spec.ts | 124 ++-- src/test-helpers.ts | 2 +- src/test.spec.ts | 206 ++++--- src/test.ts | 114 ++-- types/chai-withintoleranceof/index.d.ts | 6 +- 56 files changed, 2821 insertions(+), 2811 deletions(-) diff --git a/.prettierrc b/.prettierrc index 6235122..55c1943 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,11 +1,3 @@ { - "overrides": [ - { - "files": "*.ts", - "options": { - "semi": false - } - } - ], "plugins": ["prettier-plugin-organize-imports"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index bc9cf30..81106d7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,5 @@ "debouncing", "rngseed" ], - "cSpell.words": ["sinonjs", "zombification"] + "cSpell.words": ["Pids", "Procs", "sinonjs", "zombification"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 93be554..c0b1087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ See [Semver](http://semver.org/). - ๐Ÿ’” Deleted the standalone `pids()` function and associated code (including the ProcpsChecker). This function was exported but only used internally by tests. This fixes the [issue #58](https://github.com/photostructure/batch-cluster.js/issues/58) (by deleting the unused code! _the best kind of bugfix_). Thanks for the report, [Zaczero](https://github.com/Zaczero)! +- ๐Ÿ“ฆ Simplified `prettier` config to accept all defaults -- this added semicolons to every file. + ## v14.0.0 - ๐Ÿ’” Dropped official support for Node v14, v16, and v18. Minimum Node.js version is now v20. diff --git a/src/Args.ts b/src/Args.ts index 4f89b2c..ad0e18d 100644 --- a/src/Args.ts +++ b/src/Args.ts @@ -1 +1 @@ -export type Args = T extends (...args: infer A) => void ? A : never +export type Args = T extends (...args: infer A) => void ? A : never; diff --git a/src/Array.spec.ts b/src/Array.spec.ts index ecfe2ef..f18ed59 100644 --- a/src/Array.spec.ts +++ b/src/Array.spec.ts @@ -1,43 +1,43 @@ -import { filterInPlace } from "./Array" -import { expect, times } from "./_chai.spec" +import { filterInPlace } from "./Array"; +import { expect, times } from "./_chai.spec"; describe("Array", () => { describe("filterInPlace()", () => { it("no-ops if filter returns true", () => { - const arr = times(10, (i) => i) - const exp = times(10, (i) => i) - expect(filterInPlace(arr, () => true)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = times(10, (i) => i); + expect(filterInPlace(arr, () => true)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("clears array if filter returns false", () => { - const arr = times(10, (i) => i) - const exp: number[] = [] - expect(filterInPlace(arr, () => false)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp: number[] = []; + expect(filterInPlace(arr, () => false)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for < 5 filter", () => { - const arr = times(10, (i) => i) - const exp = [0, 1, 2, 3, 4] - expect(filterInPlace(arr, (i) => i < 5)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = [0, 1, 2, 3, 4]; + expect(filterInPlace(arr, (i) => i < 5)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for > 5 filter", () => { - const arr = times(10, (i) => i) - const exp = [5, 6, 7, 8, 9] - expect(filterInPlace(arr, (i) => i >= 5)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = [5, 6, 7, 8, 9]; + expect(filterInPlace(arr, (i) => i >= 5)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for even filter", () => { - const arr = times(10, (i) => i) - const exp = [0, 2, 4, 6, 8] - expect(filterInPlace(arr, (i) => i % 2 === 0)).to.eql(exp) - expect(arr).to.eql(exp) - }) + const arr = times(10, (i) => i); + const exp = [0, 2, 4, 6, 8]; + expect(filterInPlace(arr, (i) => i % 2 === 0)).to.eql(exp); + expect(arr).to.eql(exp); + }); it("removes entries for odd filter", () => { - const arr = times(10, (i) => i) - const exp = [1, 3, 5, 7, 9] - expect(filterInPlace(arr, (i) => i % 2 === 1)).to.eql(exp) - expect(arr).to.eql(exp) - }) - }) -}) + const arr = times(10, (i) => i); + const exp = [1, 3, 5, 7, 9]; + expect(filterInPlace(arr, (i) => i % 2 === 1)).to.eql(exp); + expect(arr).to.eql(exp); + }); + }); +}); diff --git a/src/Array.ts b/src/Array.ts index 99d1bf0..f012d88 100644 --- a/src/Array.ts +++ b/src/Array.ts @@ -3,27 +3,27 @@ * predicate `filter`. */ export function filterInPlace(arr: T[], filter: (t: T) => boolean): T[] { - const len = arr.length - let j = 0 + const len = arr.length; + let j = 0; // PERF: for-loop to avoid the additional closure from a forEach for (let i = 0; i < len; i++) { - const ea = arr[i]! + const ea = arr[i]!; if (filter(ea)) { - if (i !== j) arr[j] = ea - j++ + if (i !== j) arr[j] = ea; + j++; } } - arr.length = j - return arr + arr.length = j; + return arr; } export function count( arr: T[], predicate: (t: T, idx: number) => boolean, ): number { - let acc = 0 + let acc = 0; for (let idx = 0; idx < arr.length; idx++) { - if (predicate(arr[idx]!, idx)) acc++ + if (predicate(arr[idx]!, idx)) acc++; } - return acc + return acc; } diff --git a/src/Async.ts b/src/Async.ts index 44e63b0..367c5d4 100644 --- a/src/Async.ts +++ b/src/Async.ts @@ -1,10 +1,10 @@ -import timers from "node:timers" +import timers from "node:timers"; export function delay(millis: number, unref = false): Promise { return new Promise((resolve) => { - const t = timers.setTimeout(() => resolve(), millis) - if (unref) t.unref() - }) + const t = timers.setTimeout(() => resolve(), millis); + if (unref) t.unref(); + }); } /** @@ -16,15 +16,15 @@ export async function until( timeoutMs: number, delayMs = 50, ): Promise { - const timeoutAt = Date.now() + timeoutMs - let count = 0 + const timeoutAt = Date.now() + timeoutMs; + let count = 0; while (Date.now() < timeoutAt) { if (await f(count)) { - return true + return true; } else { - count++ - await delay(delayMs) + count++; + await delay(delayMs); } } - return false + return false; } diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index ff0ba6b..98c9f00 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -1,5 +1,5 @@ -import FakeTimers from "@sinonjs/fake-timers" -import process from "node:process" +import FakeTimers from "@sinonjs/fake-timers"; +import process from "node:process"; import { childProcs, currentTestPids, @@ -14,23 +14,23 @@ import { testPids, times, unhandledRejections, -} from "./_chai.spec" -import { filterInPlace } from "./Array" -import { delay, until } from "./Async" -import { BatchCluster } from "./BatchCluster" -import { secondMs } from "./BatchClusterOptions" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { map, omit } from "./Object" -import { isWin } from "./Platform" -import { toS } from "./String" -import { Task } from "./Task" -import { thenOrTimeout } from "./Timeout" - -const isCI = process.env.CI === "1" +} from "./_chai.spec"; +import { filterInPlace } from "./Array"; +import { delay, until } from "./Async"; +import { BatchCluster } from "./BatchCluster"; +import { secondMs } from "./BatchClusterOptions"; +import { DefaultTestOptions } from "./DefaultTestOptions.spec"; +import { map, omit } from "./Object"; +import { isWin } from "./Platform"; +import { toS } from "./String"; +import { Task } from "./Task"; +import { thenOrTimeout } from "./Timeout"; + +const isCI = process.env.CI === "1"; function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { - const common = a.filter((ea) => b.includes(ea)) - const minLength = Math.min(a.length, b.length) + const common = a.filter((ea) => b.includes(ea)); + const minLength = Math.min(a.length, b.length); if (common.length < minLength - maxAcceptableDiffs) { expect(a).to.eql( b, @@ -41,14 +41,14 @@ function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { minLength, common_length: common.length, }), - ) + ); } } describe("BatchCluster", function () { - const ErrorPrefix = "ERROR: " + const ErrorPrefix = "ERROR: "; - const ShutdownTimeoutMs = 12 * secondMs + const ShutdownTimeoutMs = 12 * secondMs; function runTasks( bc: BatchCluster, @@ -59,50 +59,50 @@ describe("BatchCluster", function () { bc .enqueueTask(new Task("upcase abc " + (i + start), parser)) .catch((err) => ErrorPrefix + err), - ) + ); } class Events { - readonly taskData: { cmd: string | undefined; data: string }[] = [] - readonly events: { event: string }[] = [] - readonly startedPids: number[] = [] - readonly exitedPids: number[] = [] - readonly startErrors: Error[] = [] - readonly endErrors: Error[] = [] - readonly fatalErrors: Error[] = [] - readonly taskErrors: Error[] = [] - readonly noTaskData: any[] = [] - readonly healthCheckErrors: Error[] = [] - readonly unhealthyPids: number[] = [] - readonly runtimeMs: number[] = [] + readonly taskData: { cmd: string | undefined; data: string }[] = []; + readonly events: { event: string }[] = []; + readonly startedPids: number[] = []; + readonly exitedPids: number[] = []; + readonly startErrors: Error[] = []; + readonly endErrors: Error[] = []; + readonly fatalErrors: Error[] = []; + readonly taskErrors: Error[] = []; + readonly noTaskData: any[] = []; + readonly healthCheckErrors: Error[] = []; + readonly unhealthyPids: number[] = []; + readonly runtimeMs: number[] = []; } - let events = new Events() - const internalErrors: Error[] = [] + let events = new Events(); + const internalErrors: Error[] = []; function assertExpectedResults(results: string[]) { const dataResults = flatten( events.taskData.map((ea) => ea.data.split(/[\n\r]+/)), - ) + ); results.forEach((result, index) => { if (!result.startsWith(ErrorPrefix)) { - expect(result).to.eql("ABC " + index) - expect(dataResults.toString()).to.include(result) + expect(result).to.eql("ABC " + index); + expect(dataResults.toString()).to.include(result); } - }) + }); } beforeEach(function () { - events = new Events() - }) + events = new Events(); + }); process.on("SIGPIPE", (error) => { - internalErrors.push(new Error("process.on(SIGPIPE): " + String(error))) - }) + internalErrors.push(new Error("process.on(SIGPIPE): " + String(error))); + }); function postAssertions() { - expect(internalErrors).to.eql([], "internal errors") + expect(internalErrors).to.eql([], "internal errors"); events.runtimeMs.forEach((ea) => expect(ea).to.be.within( @@ -110,32 +110,32 @@ describe("BatchCluster", function () { 5000, JSON.stringify({ runtimeMs: events.runtimeMs }), ), - ) + ); } - const expectedEndEvents = [{ event: "beforeEnd" }, { event: "end" }] + const expectedEndEvents = [{ event: "beforeEnd" }, { event: "end" }]; async function shutdown(bc: BatchCluster) { - if (bc == null) return // we skipped the spec - const endPromise = bc.end(true) + if (bc == null) return; // we skipped the spec + const endPromise = bc.end(true); // "ended" should be true immediately, but it may still be waiting for child // processes to exit: - expect(bc.ended).to.eql(true) + expect(bc.ended).to.eql(true); async function checkShutdown() { // const isIdle = bc.isIdle // If bc has been told to shut down, it won't ever finish any pending commands. // const pendingCommands = bc.pendingTasks.map((ea) => ea.command) - const runningCommands = bc.currentTasks.map((ea) => ea.command) - const busyProcCount = bc.busyProcCount - const pids = bc.pids() - const livingPids = await currentTestPids() + const runningCommands = bc.currentTasks.map((ea) => ea.command); + const busyProcCount = bc.busyProcCount; + const pids = bc.pids(); + const livingPids = await currentTestPids(); const done = runningCommands.length === 0 && busyProcCount === 0 && pids.length === 0 && - livingPids.length === 0 + livingPids.length === 0; if (!done) console.log("shutdown(): waiting for end", { @@ -143,24 +143,24 @@ describe("BatchCluster", function () { busyProcCount, pids, livingPids, - }) - return done + }); + return done; } // Mac CI can be extremely slow to shut down: const endOrTimeout = await thenOrTimeout( endPromise.promise.then(() => true), ShutdownTimeoutMs, - ) + ); const shutdownOrTimeout = await thenOrTimeout( until(checkShutdown, ShutdownTimeoutMs, 1000), ShutdownTimeoutMs, - ) - expect(endOrTimeout).to.eql(true, ".end() failed") - expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed") + ); + expect(endOrTimeout).to.eql(true, ".end() failed"); + expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed"); // Calling bc.end() again should be a no-op and return the same Deferred: - expect(bc.end(true).settled).to.eql(true) + expect(bc.end(true).settled).to.eql(true); expect(bc.internalErrorCount).to.eql( 0, JSON.stringify({ @@ -168,85 +168,85 @@ describe("BatchCluster", function () { internalErrors, noTaskData: events.noTaskData, }), - ) - expect(internalErrors).to.eql([], "no expected internal errors") + ); + expect(internalErrors).to.eql([], "no expected internal errors"); expect(events.noTaskData).to.eql( [], "no expected noTaskData events, but got " + JSON.stringify(events.noTaskData), - ) - return + ); + return; } function listen(bc: BatchCluster) { // This is a typings verification, too: bc.on("childStart", (cp) => map(cp.pid, (ea) => events.startedPids.push(ea)), - ) - bc.on("childEnd", (cp) => map(cp.pid, (ea) => events.exitedPids.push(ea))) - bc.on("startError", (err) => events.startErrors.push(err)) - bc.on("endError", (err) => events.endErrors.push(err)) - bc.on("fatalError", (err) => events.fatalErrors.push(err)) + ); + bc.on("childEnd", (cp) => map(cp.pid, (ea) => events.exitedPids.push(ea))); + bc.on("startError", (err) => events.startErrors.push(err)); + bc.on("endError", (err) => events.endErrors.push(err)); + bc.on("fatalError", (err) => events.fatalErrors.push(err)); bc.on("noTaskData", (stdout, stderr, proc) => { events.noTaskData.push({ stdout: toS(stdout), stderr: toS(stderr), proc_pid: proc?.pid, streamFlushMillis: bc.options.streamFlushMillis, - }) - }) + }); + }); bc.on("internalError", (err) => { - console.error("BatchCluster.spec: internal error: " + err) - internalErrors.push(err) - }) + console.error("BatchCluster.spec: internal error: " + err); + internalErrors.push(err); + }); bc.on("taskData", (data, task: Task | undefined) => events.taskData.push({ cmd: task?.command, data: toS(data), }), - ) + ); bc.on("taskResolved", (task: Task) => { - const runtimeMs = task.runtimeMs - expect(runtimeMs).to.not.eql(undefined) + const runtimeMs = task.runtimeMs; + expect(runtimeMs).to.not.eql(undefined); - events.runtimeMs.push(runtimeMs!) - }) + events.runtimeMs.push(runtimeMs!); + }); bc.on("healthCheckError", (err, proc) => { - events.healthCheckErrors.push(err) - events.unhealthyPids.push(proc.pid) - }) - bc.on("taskError", (err) => events.taskErrors.push(err)) + events.healthCheckErrors.push(err); + events.unhealthyPids.push(proc.pid); + }); + bc.on("taskError", (err) => events.taskErrors.push(err)); for (const event of ["beforeEnd", "end"] as ("beforeEnd" | "end")[]) { - bc.on(event, () => events.events.push({ event })) + bc.on(event, () => events.events.push({ event })); } - return bc + return bc; } - const newlines = ["lf"] + const newlines = ["lf"]; if (isWin) { // Don't need to test crlf except on windows: - newlines.push("crlf") + newlines.push("crlf"); } it("supports .off()", () => { - const emitTimes: number[] = [] - const bc = new BatchCluster({ ...DefaultTestOptions, processFactory }) - const listener = () => emitTimes.push(Date.now()) + const emitTimes: number[] = []; + const bc = new BatchCluster({ ...DefaultTestOptions, processFactory }); + const listener = () => emitTimes.push(Date.now()); // pick a random event that doesn't require arguments: - const evt = "beforeEnd" as const - bc.on(evt, listener) - bc.emitter.emit(evt) - expect(emitTimes.length).to.eql(1) - emitTimes.length = 0 - bc.off(evt, listener) - bc.emitter.emit(evt) - expect(emitTimes).to.eql([]) - postAssertions() - }) + const evt = "beforeEnd" as const; + bc.on(evt, listener); + bc.emitter.emit(evt); + expect(emitTimes.length).to.eql(1); + emitTimes.length = 0; + bc.off(evt, listener); + bc.emitter.emit(evt); + expect(emitTimes).to.eql([]); + postAssertions(); + }); for (const newline of newlines) { for (const maxProcs of [1, 4]) { @@ -262,44 +262,44 @@ describe("BatchCluster", function () { minDelayBetweenSpawnMillis, }), function () { - let bc: BatchCluster + let bc: BatchCluster; const opts: any = { ...DefaultTestOptions, maxProcs, minDelayBetweenSpawnMillis, - } + }; if (healthcheck) { - opts.healthCheckIntervalMillis = 250 - opts.healthCheckCommand = "flaky 0.5" // fail half the time (ensure we get a proc end due to "unhealthy") + opts.healthCheckIntervalMillis = 250; + opts.healthCheckCommand = "flaky 0.5"; // fail half the time (ensure we get a proc end due to "unhealthy") } // failrate needs to be high enough to trigger but low enough to allow // retries to succeed. beforeEach(function () { - setNewline(newline as any) - setIgnoreExit(ignoreExit) - bc = listen(new BatchCluster({ ...opts, processFactory })) - childProcs.length = 0 - }) + setNewline(newline as any); + setIgnoreExit(ignoreExit); + bc = listen(new BatchCluster({ ...opts, processFactory })); + childProcs.length = 0; + }); afterEach(async () => { - await shutdown(bc) - expect(bc.internalErrorCount).to.eql(0) - return - }) + await shutdown(bc); + expect(bc.internalErrorCount).to.eql(0); + return; + }); if (maxProcs > 1) { it("completes work on multiple child processes", async function () { if (isCI) { // don't fight timeouts on GitHub's slower-than-molasses CI boxes: - bc.options.taskTimeoutMillis = 1500 + bc.options.taskTimeoutMillis = 1500; } - this.slow(1) // always show timing + this.slow(1); // always show timing - const pidSet = new Set() - const errors: Error[] = [] + const pidSet = new Set(); + const errors: Error[] = []; for (let i = 0; i < 20; i++) { // run 4 tasks in parallel: @@ -315,121 +315,121 @@ describe("BatchCluster", function () { ), )) { try { - const result = await p - const { pid } = JSON.parse(result) + const result = await p; + const { pid } = JSON.parse(result); if (isNaN(pid)) { throw new Error( "invalid output: " + JSON.stringify(result), - ) + ); } else { - pidSet.add(pid) + pidSet.add(pid); } } catch (error) { - errors.push(error as Error) + errors.push(error as Error); } } - if (pidSet.size > 2) break + if (pidSet.size > 2) break; } - const pids = [...pidSet.values()] + const pids = [...pidSet.values()]; // console.dir({ pids, errors }) expect(pids.length).to.be.gt( 2, "expected more than a couple child processes", - ) + ); expect(pids.every((ea) => process.pid !== ea)).to.eql( true, "no child pids, " + pids.join(", ") + ", should match this process pid, " + process.pid, - ) + ); expect( errors.filter((ea) => !String(ea).includes("EUNLUCKY")), - ).to.eql([], "Unexpected errors") - }) + ).to.eql([], "Unexpected errors"); + }); } it("calling .end() when new no-ops", async () => { - await bc.end() - expect(bc.ended).to.eql(true) - expect(bc.isIdle).to.eql(true) - expect(bc.pids().length).to.eql(0) - expect(bc.spawnedProcCount).to.eql(0) - expect(events.events).to.eql(expectedEndEvents) - expect(testPids()).to.eql([]) - expect(events.startedPids).to.eql([]) - expect(events.exitedPids).to.eql([]) - postAssertions() - }) + await bc.end(); + expect(bc.ended).to.eql(true); + expect(bc.isIdle).to.eql(true); + expect(bc.pids().length).to.eql(0); + expect(bc.spawnedProcCount).to.eql(0); + expect(events.events).to.eql(expectedEndEvents); + expect(testPids()).to.eql([]); + expect(events.startedPids).to.eql([]); + expect(events.exitedPids).to.eql([]); + postAssertions(); + }); it("calling .end() after running shuts down child procs", async () => { // This just warms up bc to make child procs: const iterations = - maxProcs * (bc.options.maxTasksPerProcess + 1) + maxProcs * (bc.options.maxTasksPerProcess + 1); // we're making exact pid assertions below: don't fight // flakiness. - setFailratePct(0) + setFailratePct(0); - const tasks = await Promise.all(runTasks(bc, iterations)) - assertExpectedResults(tasks) - await shutdown(bc) - console.log(bc.stats()) + const tasks = await Promise.all(runTasks(bc, iterations)); + assertExpectedResults(tasks); + await shutdown(bc); + console.log(bc.stats()); expect(bc.spawnedProcCount).to.be.within( maxProcs, (iterations + maxProcs) * 3, // because flaky - ) - const pids = testPids() - expect(pids.length).to.be.gte(maxProcs) + ); + const pids = testPids(); + expect(pids.length).to.be.gte(maxProcs); // it's ok to miss a pid due to startup flakiness or cancelled // end tasks. - arrayEqualish(events.startedPids, pids, 1) - arrayEqualish(events.exitedPids, pids, 1) - expect(events.events).to.eql(expectedEndEvents) - postAssertions() - }) + arrayEqualish(events.startedPids, pids, 1); + arrayEqualish(events.exitedPids, pids, 1); + expect(events.events).to.eql(expectedEndEvents); + postAssertions(); + }); it( "runs a given batch process roughly " + opts.maxTasksPerProcess + " before recycling", async function () { - if (isWin && isCI) this.timeout(45 * secondMs) + if (isWin && isCI) this.timeout(45 * secondMs); // make sure we hit an EUNLUCKY: - setFailratePct(60) - let expectedResultCount = 0 - const results = await Promise.all(runTasks(bc, maxProcs)) - expectedResultCount += maxProcs - const pids = bc.pids() + setFailratePct(60); + let expectedResultCount = 0; + const results = await Promise.all(runTasks(bc, maxProcs)); + expectedResultCount += maxProcs; + const pids = bc.pids(); const iters = Math.floor( maxProcs * opts.maxTasksPerProcess * 1.5, - ) + ); results.push( ...(await Promise.all( runTasks(bc, iters, expectedResultCount), )), - ) - console.log(bc.stats()) + ); + console.log(bc.stats()); - expectedResultCount += iters - assertExpectedResults(results) - expect(results.length).to.eql(expectedResultCount) + expectedResultCount += iters; + assertExpectedResults(results); + expect(results.length).to.eql(expectedResultCount); // expect some errors: const errorResults = results.filter((ea) => ea.startsWith(ErrorPrefix), - ) - expect(errorResults).to.not.eql([]) + ); + expect(errorResults).to.not.eql([]); // Expect a reasonable number of new pids. Worst case, we // errored after every start, so there may be more then iters // pids spawned. - expect(childProcs.length).to.eql(bc.spawnedProcCount) + expect(childProcs.length).to.eql(bc.spawnedProcCount); expect(bc.spawnedProcCount).to.be.within( results.length / opts.maxTasksPerProcess, results.length * (isWin ? 10 : 5), // because flaky - ) + ); // So, at this point, we should have at least _asked_ the // initial child processes to end because they're "worn". @@ -437,72 +437,72 @@ describe("BatchCluster", function () { // Running vacuumProcs will return a promise that will only // resolve when those procs have shut down. - await bc.vacuumProcs() + await bc.vacuumProcs(); // Expect no prior pids to remain, as long as there were before-pids: // NOTE: On Windows, PIDs can be reused quickly, so we only check this // on non-Windows platforms to avoid flakiness if (pids.length > 0 && !isWin) - expect(bc.pids()).to.not.include.members(pids) + expect(bc.pids()).to.not.include.members(pids); expect(bc.meanTasksPerProc).to.be.within( 0.15, // because flaky (macOS on GHA resulted in 0.21) opts.maxTasksPerProcess, - ) - expect(bc.pids().length).to.be.lte(maxProcs) + ); + expect(bc.pids().length).to.be.lte(maxProcs); expect((await currentTestPids()).length).to.be.lte( bc.spawnedProcCount, - ) // because flaky + ); // because flaky - const unhealthy = bc.countEndedChildProcs("unhealthy") + const unhealthy = bc.countEndedChildProcs("unhealthy"); // If it's a short spec and we don't have any worn procs, we // probably don't have any unhealthy procs: if (healthcheck && bc.countEndedChildProcs("worn") > 2) { - expect(unhealthy).to.be.gte(0) + expect(unhealthy).to.be.gte(0); } if (!healthcheck) { - expect(unhealthy).to.eql(0) + expect(unhealthy).to.eql(0); } - await shutdown(bc) + await shutdown(bc); // (no run count assertions) }, - ) + ); it("recovers from invalid commands", async function () { - this.slow(1) + this.slow(1); assertExpectedResults( await Promise.all(runTasks(bc, maxProcs * 4)), - ) + ); const errorResults = await Promise.all( times(maxProcs * 2, () => bc .enqueueTask(new Task("nonsense", parser)) .catch((err: unknown) => err), ), - ) + ); function convertErrorToString(ea: unknown): string { - if (ea == null) return "[unknown]" - if (ea instanceof Error) return ea.message - if (typeof ea === "string") return ea + if (ea == null) return "[unknown]"; + if (ea instanceof Error) return ea.message; + if (typeof ea === "string") return ea; if (typeof ea === "object") { try { - return JSON.stringify(ea) + return JSON.stringify(ea); } catch { - return "[object Object]" + return "[object Object]"; } } if (typeof ea === "number" || typeof ea === "boolean") { - return String(ea) + return String(ea); } - return "[unknown]" + return "[unknown]"; } filterInPlace(errorResults, (ea) => { - const errorStr = convertErrorToString(ea) - return !errorStr.includes("EUNLUCKY") - }) + const errorStr = convertErrorToString(ea); + return !errorStr.includes("EUNLUCKY"); + }); if ( maxProcs === 1 && ignoreExit === false && @@ -510,97 +510,97 @@ describe("BatchCluster", function () { ) { // We don't expect these to pass with this config: } else if (maxProcs === 1 && errorResults.length === 0) { - console.warn("(all processes were unlucky)") - return this.skip() + console.warn("(all processes were unlucky)"); + return this.skip(); } else { expect( errorResults.some((ea) => String(ea).includes("nonsense"), ), - ).to.eql(true, JSON.stringify(errorResults)) + ).to.eql(true, JSON.stringify(errorResults)); expect( parserErrors.some((ea) => ea.includes("nonsense")), - ).to.eql(true, JSON.stringify(parserErrors)) + ).to.eql(true, JSON.stringify(parserErrors)); } - parserErrors.length = 0 + parserErrors.length = 0; // BC should recover: assertExpectedResults( await Promise.all(runTasks(bc, maxProcs * 4)), - ) + ); // (no run count assertions) - return - }) + return; + }); it("times out slow requests", async () => { const task = new Task( "sleep " + (opts.taskTimeoutMillis + 250), // < make sure it times out parser, - ) + ); await expect( bc.enqueueTask(task), - ).to.eventually.be.rejectedWith(/timeout|EUNLUCKY/) - postAssertions() - }) + ).to.eventually.be.rejectedWith(/timeout|EUNLUCKY/); + postAssertions(); + }); it("accepts single and multi-line responses", async () => { - setFailratePct(0) + setFailratePct(0); if (isCI) { // don't fight timeouts on GitHub's slower-than-molasses CI boxes: - bc.options.taskTimeoutMillis = 1500 + bc.options.taskTimeoutMillis = 1500; } - const expected: string[] = [] + const expected: string[] = []; const results = await Promise.all( times(15, (idx) => { // Make a distribution of single, double, and triple line outputs: - const worlds = times(idx % 3, (ea) => "world " + ea) + const worlds = times(idx % 3, (ea) => "world " + ea); expected.push( [idx + " HELLO", ...worlds].join("\n").toUpperCase(), - ) + ); const cmd = ["upcase " + idx + " hello", ...worlds].join( "
", - ) - return bc.enqueueTask(new Task(cmd, parser)) + ); + return bc.enqueueTask(new Task(cmd, parser)); }), - ) - expect(results).to.eql(expected) + ); + expect(results).to.eql(expected); - postAssertions() - }) + postAssertions(); + }); it("rejects a command that results in FAIL", async function () { - const task = new Task("invalid command", parser) - let error: Error | undefined - let result = "" + const task = new Task("invalid command", parser); + let error: Error | undefined; + let result = ""; try { - result = await bc.enqueueTask(task) + result = await bc.enqueueTask(task); } catch (err: any) { - error = err + error = err; } expect(String(error)).to.match( /invalid command|UNLUCKY/, result, - ) - postAssertions() - }) + ); + postAssertions(); + }); it("rejects a command that emits to stderr", async function () { - const task = new Task("stderr omg this should fail", parser) - let error: Error | undefined - let result = "" + const task = new Task("stderr omg this should fail", parser); + let error: Error | undefined; + let result = ""; try { - result = await bc.enqueueTask(task) + result = await bc.enqueueTask(task); } catch (err: any) { - error = err + error = err; } expect(String(error)).to.match( /omg this should fail|UNLUCKY/, result, - ) - postAssertions() - }) + ); + postAssertions(); + }); }, - ) + ); } } } @@ -608,11 +608,11 @@ describe("BatchCluster", function () { } describe("maxProcs", function () { - const iters = 100 - const maxProcs = 10 - const sleepTimeMs = 250 - let bc: BatchCluster - afterEach(() => shutdown(bc)) + const iters = 100; + const maxProcs = 10; + const sleepTimeMs = 250; + let bc: BatchCluster; + afterEach(() => shutdown(bc)); for (const { minDelayBetweenSpawnMillis, expectTaskMin, @@ -636,7 +636,7 @@ describe("BatchCluster", function () { }, ]) { it(JSON.stringify({ minDelayBetweenSpawnMillis }), async () => { - setFailratePct(0) + setFailratePct(0); const opts = { ...DefaultTestOptions, taskTimeoutMillis: 5_000, // < don't test timeouts here @@ -644,29 +644,29 @@ describe("BatchCluster", function () { maxTasksPerProcess: expectedTaskMax + 5, // < don't recycle procs for this test minDelayBetweenSpawnMillis, processFactory, - } - bc = listen(new BatchCluster(opts)) - expect(bc.isIdle).to.eql(true) + }; + bc = listen(new BatchCluster(opts)); + expect(bc.isIdle).to.eql(true); const tasks = await Promise.all( times(iters, async (i) => { - const start = Date.now() - const task = new Task("sleep " + sleepTimeMs, parser) - const resultP = bc.enqueueTask(task) - expect(bc.isIdle).to.eql(false) + const start = Date.now(); + const task = new Task("sleep " + sleepTimeMs, parser); + const resultP = bc.enqueueTask(task); + expect(bc.isIdle).to.eql(false); const result = JSON.parse(await resultP) as { - pid: number - } & Record - const end = Date.now() - return { i, start, end, ...result } + pid: number; + } & Record; + const end = Date.now(); + return { i, start, end, ...result }; }), - ) - const pid2count = new Map() + ); + const pid2count = new Map(); tasks.forEach((ea) => { - const pid = ea.pid - const count = pid2count.get(pid) ?? 0 - pid2count.set(pid, count + 1) - }) - expect(bc.isIdle).to.eql(true) + const pid = ea.pid; + const count = pid2count.get(pid) ?? 0; + pid2count.set(pid, count + 1); + }); + expect(bc.isIdle).to.eql(true); console.log({ expectTaskMin, expectedTaskMax, @@ -674,24 +674,24 @@ describe("BatchCluster", function () { uniqPids: pid2count.size, pid2count, bcPids: bc.pids(), - }) + }); for (const [, count] of pid2count.entries()) { - expect(count).to.be.within(expectTaskMin, expectedTaskMax) + expect(count).to.be.within(expectTaskMin, expectedTaskMax); } - expect(pid2count.size).to.be.within(expectedProcsMin, expectedProcsMax) - }) + expect(pid2count.size).to.be.within(expectedProcsMin, expectedProcsMax); + }); } - }) + }); describe("setMaxProcs", function () { - const maxProcs = 10 - const sleepTimeMs = 250 - let bc: BatchCluster - afterEach(() => shutdown(bc)) + const maxProcs = 10; + const sleepTimeMs = 250; + let bc: BatchCluster; + afterEach(() => shutdown(bc)); it("supports reducing maxProcs", async () => { // don't fight with flakiness here! - setFailratePct(0) + setFailratePct(0); const opts = { ...DefaultTestOptions, minDelayBetweenSpawnMillis: 0, @@ -699,68 +699,68 @@ describe("BatchCluster", function () { maxProcs, maxTasksPerProcess: 100, // < don't recycle procs for this test processFactory, - } - bc = new BatchCluster(opts) - const firstBatchPromises: Promise[] = [] + }; + bc = new BatchCluster(opts); + const firstBatchPromises: Promise[] = []; while (bc.busyProcCount < maxProcs) { firstBatchPromises.push( bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)), - ) - await delay(25) + ); + await delay(25); } - expect(bc.currentTasks.length).to.be.closeTo(maxProcs, 2) - expect(bc.busyProcCount).to.be.closeTo(maxProcs, 2) - expect(bc.procCount).to.be.closeTo(maxProcs, 2) - const maxProcs2 = maxProcs / 2 - bc.setMaxProcs(maxProcs2) + expect(bc.currentTasks.length).to.be.closeTo(maxProcs, 2); + expect(bc.busyProcCount).to.be.closeTo(maxProcs, 2); + expect(bc.procCount).to.be.closeTo(maxProcs, 2); + const maxProcs2 = maxProcs / 2; + bc.setMaxProcs(maxProcs2); const secondBatchPromises = times(maxProcs, () => bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)), - ) - await Promise.all(firstBatchPromises) - bc.vacuumProcs() + ); + await Promise.all(firstBatchPromises); + bc.vacuumProcs(); // We should be dropping BatchProcesses at this point. - expect(bc.busyProcCount).to.be.within(0, maxProcs2) - expect(bc.procCount).to.be.within(0, maxProcs2) + expect(bc.busyProcCount).to.be.within(0, maxProcs2); + expect(bc.procCount).to.be.within(0, maxProcs2); - await Promise.all(secondBatchPromises) + await Promise.all(secondBatchPromises); - expect(bc.busyProcCount).to.eql(0) // because we're done + expect(bc.busyProcCount).to.eql(0); // because we're done // Assert that there were excess procs shut down: - expect(bc.childEndCounts.tooMany).to.be.closeTo(maxProcs - maxProcs2, 2) + expect(bc.childEndCounts.tooMany).to.be.closeTo(maxProcs - maxProcs2, 2); // don't shut down until bc is idle... (otherwise we'll fail due to // "Error: end() called before task completed // ({\"gracefully\":true,\"source\":\"BatchCluster.closeChildProcesses()\"})" - await until(() => bc.isIdle, 5000) + await until(() => bc.isIdle, 5000); - postAssertions() - }) - }) + postAssertions(); + }); + }); describe(".end() cleanup", () => { - const sleepTimeMs = 1000 // must be longer than non-graceful timeout (currently 250) - let bc: BatchCluster - afterEach(() => shutdown(bc)) + const sleepTimeMs = 1000; // must be longer than non-graceful timeout (currently 250) + let bc: BatchCluster; + afterEach(() => shutdown(bc)); function stats() { // we don't want msBeforeNextSpawn because it'll be wiggly and we're not // freezing time (here) - return omit(bc.stats(), "msBeforeNextSpawn") as Record + return omit(bc.stats(), "msBeforeNextSpawn") as Record; } it("shut down rejects long-running pending tasks", async () => { - setFailratePct(0) + setFailratePct(0); const opts = { ...DefaultTestOptions, taskTimeoutMillis: sleepTimeMs * 4, // < don't test timeouts here processFactory, - } - bc = new BatchCluster(opts) + }; + bc = new BatchCluster(opts); // Wait for one job to run (so the process spins up and we're ready to go) - await Promise.all(runTasks(bc, 1)) + await Promise.all(runTasks(bc, 1)); expect(stats()).to.eql({ pendingTaskCount: 0, @@ -773,9 +773,9 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - const t = bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)) + const t = bc.enqueueTask(new Task("sleep " + sleepTimeMs, parser)); expect(stats()).to.eql({ pendingTaskCount: 1, @@ -788,10 +788,10 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - t.catch((err: unknown) => (caught = err)) - await delay(2) + t.catch((err: unknown) => (caught = err)); + await delay(2); expect(stats()).to.eql({ pendingTaskCount: 0, // < yay it's getting processed @@ -804,11 +804,11 @@ describe("BatchCluster", function () { childEndCounts: {}, ending: false, ended: false, - }) + }); - let caught: unknown - expect(bc.isIdle).to.eql(false) - await bc.end(false) // not graceful just to shut down faster + let caught: unknown; + expect(bc.isIdle).to.eql(false); + await bc.end(false); // not graceful just to shut down faster expect(stats()).to.eql({ pendingTaskCount: 0, @@ -821,15 +821,15 @@ describe("BatchCluster", function () { childEndCounts: { ending: 1 }, ending: true, ended: true, - }) + }); - expect(bc.isIdle).to.eql(true) + expect(bc.isIdle).to.eql(true); expect((caught as Error)?.message).to.include( "Process terminated before task completed", - ) - expect(unhandledRejections).to.eql([]) - }) - }) + ); + expect(unhandledRejections).to.eql([]); + }); + }); describe("maxProcAgeMillis (cull old children)", function () { const opts = { @@ -839,9 +839,9 @@ describe("BatchCluster", function () { spawnTimeoutMillis: 2000, // maxProcAge must be >= this maxProcAgeMillis: 3000, minDelayBetweenSpawnMillis: 0, - } + }; - let bc: BatchCluster + let bc: BatchCluster; beforeEach( () => @@ -851,30 +851,30 @@ describe("BatchCluster", function () { processFactory, }), )), - ) + ); - afterEach(() => shutdown(bc)) + afterEach(() => shutdown(bc)); it("culls old child procs", async () => { assertExpectedResults( await Promise.all(runTasks(bc, opts.maxProcs + 100)), - ) + ); // 0 because we might get unlucky. - expect(bc.pids().length).to.be.within(0, opts.maxProcs) - await delay(opts.maxProcAgeMillis + 100) - await bc.vacuumProcs() + expect(bc.pids().length).to.be.within(0, opts.maxProcs); + await delay(opts.maxProcAgeMillis + 100); + await bc.vacuumProcs(); console.log({ childEndCounts: bc.childEndCounts, procCount: bc.procCount, maxProcs: opts.maxProcs, - }) - expect(bc.countEndedChildProcs("idle")).to.eql(0) - expect(bc.countEndedChildProcs("old")).to.be.gte(2) + }); + expect(bc.countEndedChildProcs("idle")).to.eql(0); + expect(bc.countEndedChildProcs("old")).to.be.gte(2); // Calling .pids calls .procs(), which culls old procs - expect(bc.pids().length).to.be.within(0, opts.maxProcs) - postAssertions() - }) - }) + expect(bc.pids().length).to.be.within(0, opts.maxProcs); + postAssertions(); + }); + }); describe("maxIdleMsPerProcess", function () { const opts = { @@ -882,9 +882,9 @@ describe("BatchCluster", function () { maxProcs: 4, maxIdleMsPerProcess: 1000, maxProcAgeMillis: 30_000, - } + }; - let bc: BatchCluster + let bc: BatchCluster; beforeEach( () => @@ -894,74 +894,76 @@ describe("BatchCluster", function () { processFactory, }), )), - ) + ); - afterEach(() => shutdown(bc)) + afterEach(() => shutdown(bc)); it("culls idle child procs", async () => { - assertExpectedResults(await Promise.all(runTasks(bc, opts.maxProcs + 10))) + assertExpectedResults( + await Promise.all(runTasks(bc, opts.maxProcs + 10)), + ); // 0 because we might get unlucky. - expect(bc.pids().length).to.be.within(0, opts.maxProcs) + expect(bc.pids().length).to.be.within(0, opts.maxProcs); // wait long enough for at least 1 process to be idle and get reaped: - await delay(opts.maxIdleMsPerProcess + 100) - await bc.vacuumProcs() + await delay(opts.maxIdleMsPerProcess + 100); + await bc.vacuumProcs(); console.log({ childEndCounts: bc.childEndCounts, procCount: bc.procCount, maxProcs: opts.maxProcs, - }) - expect(bc.countEndedChildProcs("idle")).to.be.gte(1) - expect(bc.countEndedChildProcs("old")).to.be.lte(1) - expect(bc.countEndedChildProcs("worn")).to.be.lte(2) + }); + expect(bc.countEndedChildProcs("idle")).to.be.gte(1); + expect(bc.countEndedChildProcs("old")).to.be.lte(1); + expect(bc.countEndedChildProcs("worn")).to.be.lte(2); // Calling .pids calls .procs(), which culls old procs if (bc.pids().length > 0) { - await delay(1000) + await delay(1000); } - expect(bc.pids().length).to.eql(0) - postAssertions() - }) - }) + expect(bc.pids().length).to.eql(0); + postAssertions(); + }); + }); describe("maxProcAgeMillis (recycling procs)", () => { - let bc: BatchCluster - let clock: FakeTimers.InstalledClock + let bc: BatchCluster; + let clock: FakeTimers.InstalledClock; beforeEach(() => { clock = FakeTimers.install({ shouldClearNativeTimers: true, shouldAdvanceTime: true, - }) - }) + }); + }); afterEach(() => { - clock.uninstall() - return shutdown(bc) - }) + clock.uninstall(); + return shutdown(bc); + }); for (const { maxProcAgeMillis, ctx, exp } of [ { maxProcAgeMillis: 0, ctx: "procs should not be recycled due to old age", exp: (pidsBefore: number[], pidsAfter: number[]) => { - expect(pidsBefore).to.eql(pidsAfter) - expect(bc.countEndedChildProcs("idle")).to.eql(0) - expect(bc.countEndedChildProcs("old")).to.eql(0) + expect(pidsBefore).to.eql(pidsAfter); + expect(bc.countEndedChildProcs("idle")).to.eql(0); + expect(bc.countEndedChildProcs("old")).to.eql(0); }, }, { maxProcAgeMillis: 5000, ctx: "procs should be recycled due to old age", exp: (pidsBefore: number[], pidsAfter: number[]) => { - expect(pidsBefore).to.not.have.members(pidsAfter) - expect(bc.countEndedChildProcs("idle")).to.eql(0) - expect(bc.countEndedChildProcs("old")).to.be.gte(1) + expect(pidsBefore).to.not.have.members(pidsAfter); + expect(bc.countEndedChildProcs("idle")).to.eql(0); + expect(bc.countEndedChildProcs("old")).to.be.gte(1); }, }, ]) { it("(" + maxProcAgeMillis + "): " + ctx, async function () { // TODO: look into why this fails in CI on windows - if (isWin && isCI) return this.skip() - setFailratePct(0) + if (isWin && isCI) return this.skip(); + setFailratePct(0); bc = listen( new BatchCluster({ @@ -971,17 +973,17 @@ describe("BatchCluster", function () { spawnTimeoutMillis: Math.max(maxProcAgeMillis, 200), processFactory, }), - ) - assertExpectedResults(await Promise.all(runTasks(bc, 2))) - const pidsBefore = bc.pids() - clock.tick(7000) - assertExpectedResults(await Promise.all(runTasks(bc, 2))) - const pidsAfter = bc.pids() - console.dir({ maxProcAgeMillis, pidsBefore, pidsAfter }) - exp(pidsBefore, pidsAfter) - postAssertions() - return - }) + ); + assertExpectedResults(await Promise.all(runTasks(bc, 2))); + const pidsBefore = bc.pids(); + clock.tick(7000); + assertExpectedResults(await Promise.all(runTasks(bc, 2))); + const pidsAfter = bc.pids(); + console.dir({ maxProcAgeMillis, pidsBefore, pidsAfter }); + exp(pidsBefore, pidsAfter); + postAssertions(); + return; + }); } - }) -}) + }); +}); diff --git a/src/BatchCluster.ts b/src/BatchCluster.ts index c352622..3c67016 100644 --- a/src/BatchCluster.ts +++ b/src/BatchCluster.ts @@ -1,51 +1,51 @@ -import events from "node:events" -import process from "node:process" -import timers from "node:timers" -import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter" -import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator" -import type { BatchClusterOptions } from "./BatchClusterOptions" -import type { BatchClusterStats } from "./BatchClusterStats" -import type { BatchProcessOptions } from "./BatchProcessOptions" -import type { ChildProcessFactory } from "./ChildProcessFactory" -import type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" -import { Deferred } from "./Deferred" -import { Logger } from "./Logger" -import { verifyOptions } from "./OptionsVerifier" -import { ProcessPoolManager } from "./ProcessPoolManager" -import { Task } from "./Task" -import { TaskQueueManager } from "./TaskQueueManager" - -export { BatchClusterOptions } from "./BatchClusterOptions" -export { BatchProcess } from "./BatchProcess" -export { Deferred } from "./Deferred" -export * from "./Logger" -export { SimpleParser } from "./Parser" -export { kill, pidExists } from "./Pids" -export { Rate } from "./Rate" -export { Task } from "./Task" +import events from "node:events"; +import process from "node:process"; +import timers from "node:timers"; +import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter"; +import { BatchClusterEventCoordinator } from "./BatchClusterEventCoordinator"; +import type { BatchClusterOptions } from "./BatchClusterOptions"; +import type { BatchClusterStats } from "./BatchClusterStats"; +import type { BatchProcessOptions } from "./BatchProcessOptions"; +import type { ChildProcessFactory } from "./ChildProcessFactory"; +import type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"; +import { Deferred } from "./Deferred"; +import { Logger } from "./Logger"; +import { verifyOptions } from "./OptionsVerifier"; +import { ProcessPoolManager } from "./ProcessPoolManager"; +import { Task } from "./Task"; +import { TaskQueueManager } from "./TaskQueueManager"; + +export { BatchClusterOptions } from "./BatchClusterOptions"; +export { BatchProcess } from "./BatchProcess"; +export { Deferred } from "./Deferred"; +export * from "./Logger"; +export { SimpleParser } from "./Parser"; +export { kill, pidExists } from "./Pids"; +export { Rate } from "./Rate"; +export { Task } from "./Task"; // Type exports organized by source module -export type { Args } from "./Args" +export type { Args } from "./Args"; export type { BatchClusterEmitter, BatchClusterEvents, ChildEndReason, TypedEventEmitter, -} from "./BatchClusterEmitter" -export type { WithObserver } from "./BatchClusterOptions" -export type { BatchClusterStats } from "./BatchClusterStats" -export type { BatchProcessOptions } from "./BatchProcessOptions" -export type { ChildProcessFactory } from "./ChildProcessFactory" -export type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" -export type { HealthCheckStrategy } from "./HealthCheckStrategy" -export type { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -export type { LoggerFunction } from "./Logger" -export type { Parser } from "./Parser" +} from "./BatchClusterEmitter"; +export type { WithObserver } from "./BatchClusterOptions"; +export type { BatchClusterStats } from "./BatchClusterStats"; +export type { BatchProcessOptions } from "./BatchProcessOptions"; +export type { ChildProcessFactory } from "./ChildProcessFactory"; +export type { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"; +export type { HealthCheckStrategy } from "./HealthCheckStrategy"; +export type { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +export type { LoggerFunction } from "./Logger"; +export type { Parser } from "./Parser"; export type { HealthCheckable, ProcessHealthMonitor, -} from "./ProcessHealthMonitor" -export type { TaskOptions } from "./Task" -export type { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" +} from "./ProcessHealthMonitor"; +export type { TaskOptions } from "./Task"; +export type { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"; /** * BatchCluster instances manage 0 or more homogeneous child processes, and @@ -58,29 +58,29 @@ export type { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" * child tasks can be verified and shut down. */ export class BatchCluster { - readonly #logger: () => Logger - readonly options: CombinedBatchProcessOptions - readonly #processPool: ProcessPoolManager - readonly #taskQueue: TaskQueueManager - readonly #eventCoordinator: BatchClusterEventCoordinator - #onIdleRequested = false - #onIdleInterval: NodeJS.Timeout | undefined - #endPromise?: Deferred - readonly emitter = new events.EventEmitter() as BatchClusterEmitter + readonly #logger: () => Logger; + readonly options: CombinedBatchProcessOptions; + readonly #processPool: ProcessPoolManager; + readonly #taskQueue: TaskQueueManager; + readonly #eventCoordinator: BatchClusterEventCoordinator; + #onIdleRequested = false; + #onIdleInterval: NodeJS.Timeout | undefined; + #endPromise?: Deferred; + readonly emitter = new events.EventEmitter() as BatchClusterEmitter; constructor( opts: Partial & BatchProcessOptions & ChildProcessFactory, ) { - this.options = verifyOptions({ ...opts, observer: this.emitter }) - this.#logger = this.options.logger + this.options = verifyOptions({ ...opts, observer: this.emitter }); + this.#logger = this.options.logger; // Initialize the managers this.#processPool = new ProcessPoolManager(this.options, this.emitter, () => this.#onIdleLater(), - ) - this.#taskQueue = new TaskQueueManager(this.#logger, this.emitter) + ); + this.#taskQueue = new TaskQueueManager(this.#logger, this.emitter); // Initialize event coordinator to handle all event processing this.#eventCoordinator = new BatchClusterEventCoordinator( @@ -93,41 +93,41 @@ export class BatchCluster { }, () => this.#onIdleLater(), () => void this.end(), - ) + ); if (this.options.onIdleIntervalMillis > 0) { this.#onIdleInterval = timers.setInterval( () => this.#onIdleLater(), this.options.onIdleIntervalMillis, - ) - this.#onIdleInterval.unref() // < don't prevent node from exiting + ); + this.#onIdleInterval.unref(); // < don't prevent node from exiting } - this.#logger = this.options.logger + this.#logger = this.options.logger; - process.once("beforeExit", this.#beforeExitListener) - process.once("exit", this.#exitListener) + process.once("beforeExit", this.#beforeExitListener); + process.once("exit", this.#exitListener); } /** * @see BatchClusterEvents */ - readonly on = this.emitter.on.bind(this.emitter) + readonly on = this.emitter.on.bind(this.emitter); /** * @see BatchClusterEvents * @since v9.0.0 */ - readonly off = this.emitter.off.bind(this.emitter) + readonly off = this.emitter.off.bind(this.emitter); readonly #beforeExitListener = () => { - void this.end(true) - } + void this.end(true); + }; readonly #exitListener = () => { - void this.end(false) - } + void this.end(false); + }; get ended(): boolean { - return this.#endPromise != null + return this.#endPromise != null; } /** @@ -137,23 +137,23 @@ export class BatchCluster { */ // NOT ASYNC so state transition happens immediately end(gracefully = true): Deferred { - this.#logger().info("BatchCluster.end()", { gracefully }) + this.#logger().info("BatchCluster.end()", { gracefully }); if (this.#endPromise == null) { - this.emitter.emit("beforeEnd") + this.emitter.emit("beforeEnd"); if (this.#onIdleInterval != null) - timers.clearInterval(this.#onIdleInterval) - this.#onIdleInterval = undefined - process.removeListener("beforeExit", this.#beforeExitListener) - process.removeListener("exit", this.#exitListener) + timers.clearInterval(this.#onIdleInterval); + this.#onIdleInterval = undefined; + process.removeListener("beforeExit", this.#beforeExitListener); + process.removeListener("exit", this.#exitListener); this.#endPromise = new Deferred().observe( this.closeChildProcesses(gracefully).then(() => { - this.emitter.emit("end") + this.emitter.emit("end"); }), - ) + ); } - return this.#endPromise + return this.#endPromise; } /** @@ -166,85 +166,85 @@ export class BatchCluster { if (this.ended) { task.reject( new Error("BatchCluster has ended, cannot enqueue " + task.command), - ) + ); } - this.#taskQueue.enqueue(task as Task) + this.#taskQueue.enqueue(task as Task); // Run #onIdle now (not later), to make sure the task gets enqueued asap if // possible - this.#onIdleLater() + this.#onIdleLater(); // (BatchProcess will call our #onIdleLater when tasks settle or when they // exit) - return task.promise + return task.promise; } /** * @return true if all previously-enqueued tasks have settled */ get isIdle(): boolean { - return this.pendingTaskCount === 0 && this.busyProcCount === 0 + return this.pendingTaskCount === 0 && this.busyProcCount === 0; } /** * @return the number of pending tasks */ get pendingTaskCount(): number { - return this.#taskQueue.pendingTaskCount + return this.#taskQueue.pendingTaskCount; } /** * @returns {number} the mean number of tasks completed by child processes */ get meanTasksPerProc(): number { - return this.#eventCoordinator.meanTasksPerProc + return this.#eventCoordinator.meanTasksPerProc; } /** * @return the total number of child processes created by this instance */ get spawnedProcCount(): number { - return this.#processPool.spawnedProcCount + return this.#processPool.spawnedProcCount; } /** * @return the current number of spawned child processes. Some (or all) may be idle. */ get procCount(): number { - return this.#processPool.processCount + return this.#processPool.processCount; } /** * @return the current number of child processes currently servicing tasks */ get busyProcCount(): number { - return this.#processPool.busyProcCount + return this.#processPool.busyProcCount; } get startingProcCount(): number { - return this.#processPool.startingProcCount + return this.#processPool.startingProcCount; } /** * @return the current pending Tasks (mostly for testing) */ get pendingTasks(): readonly Task[] { - return this.#taskQueue.pendingTasks + return this.#taskQueue.pendingTasks; } /** * @return the current running Tasks (mostly for testing) */ get currentTasks(): Task[] { - return this.#processPool.currentTasks() + return this.#processPool.currentTasks(); } /** * For integration tests: */ get internalErrorCount(): number { - return this.#eventCoordinator.internalErrorCount + return this.#eventCoordinator.internalErrorCount; } /** @@ -253,7 +253,7 @@ export class BatchCluster { * @return the spawned PIDs that are still in the process table. */ pids(): number[] { - return this.#processPool.pids() + return this.#processPool.pids(); } /** @@ -272,18 +272,18 @@ export class BatchCluster { childEndCounts: this.childEndCounts, ending: this.#endPromise != null, ended: false === this.#endPromise?.pending, - } + }; } /** * Get ended process counts (used for tests) */ countEndedChildProcs(why: ChildEndReason): number { - return this.#eventCoordinator.countEndedChildProcs(why) + return this.#eventCoordinator.countEndedChildProcs(why); } get childEndCounts(): Record, number> { - return this.#eventCoordinator.childEndCounts + return this.#eventCoordinator.childEndCounts; } /** @@ -291,7 +291,7 @@ export class BatchCluster { * be started automatically to handle new tasks. */ async closeChildProcesses(gracefully = true): Promise { - return this.#processPool.closeChildProcesses(gracefully) + return this.#processPool.closeChildProcesses(gracefully); } /** @@ -300,26 +300,26 @@ export class BatchCluster { * completed. */ setMaxProcs(maxProcs: number) { - this.#processPool.setMaxProcs(maxProcs) + this.#processPool.setMaxProcs(maxProcs); // we may now be able to handle an enqueued task. Vacuum pids and see: - this.#onIdleLater() + this.#onIdleLater(); } readonly #onIdleLater = () => { if (!this.#onIdleRequested) { - this.#onIdleRequested = true - timers.setTimeout(() => this.#onIdle(), 1) + this.#onIdleRequested = true; + timers.setTimeout(() => this.#onIdle(), 1); } - } + }; // NOT ASYNC: updates internal state: #onIdle() { - this.#onIdleRequested = false - void this.vacuumProcs() + this.#onIdleRequested = false; + void this.vacuumProcs(); while (this.#execNextTask()) { // } - void this.#maybeSpawnProcs() + void this.#maybeSpawnProcs(); } /** @@ -330,7 +330,7 @@ export class BatchCluster { */ // NOT ASYNC: updates internal state. only exported for tests. vacuumProcs() { - return this.#processPool.vacuumProcs() + return this.#processPool.vacuumProcs(); } /** @@ -338,15 +338,15 @@ export class BatchCluster { * @return true iff a task was submitted to a child process */ #execNextTask(retries = 1): boolean { - if (this.ended) return false - const readyProc = this.#processPool.findReadyProcess() - return this.#taskQueue.tryAssignNextTask(readyProc, retries) + if (this.ended) return false; + const readyProc = this.#processPool.findReadyProcess(); + return this.#taskQueue.tryAssignNextTask(readyProc, retries); } async #maybeSpawnProcs() { return this.#processPool.maybeSpawnProcs( this.#taskQueue.pendingTaskCount, this.ended, - ) + ); } } diff --git a/src/BatchClusterEmitter.ts b/src/BatchClusterEmitter.ts index 53fd4ce..d6d07ad 100644 --- a/src/BatchClusterEmitter.ts +++ b/src/BatchClusterEmitter.ts @@ -1,9 +1,9 @@ -import { Args } from "./Args" -import { BatchProcess } from "./BatchProcess" -import { Task } from "./Task" -import { WhyNotHealthy } from "./WhyNotHealthy" +import { Args } from "./Args"; +import { BatchProcess } from "./BatchProcess"; +import { Task } from "./Task"; +import { WhyNotHealthy } from "./WhyNotHealthy"; -export type ChildEndReason = WhyNotHealthy | "tooMany" +export type ChildEndReason = WhyNotHealthy | "tooMany"; // Type-safe EventEmitter! Note that this interface is not comprehensive: // EventEmitter has a bunch of other methods, but batch-cluster doesn't use @@ -12,21 +12,21 @@ export interface TypedEventEmitter { once( eventName: E, listener: (...args: Args) => void, - ): this + ): this; on( eventName: E, listener: (...args: Args) => void, - ): this + ): this; off( eventName: E, listener: (...args: Args) => void, - ): this - emit(eventName: E, ...args: Args): boolean + ): this; + emit(eventName: E, ...args: Args): boolean; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - listeners(event: E): Function[] + listeners(event: E): Function[]; - removeAllListeners(eventName?: keyof T): this + removeAllListeners(eventName?: keyof T): this; } /** @@ -39,30 +39,30 @@ export interface BatchClusterEvents { /** * Emitted when a child process has started */ - childStart: (childProcess: BatchProcess) => void + childStart: (childProcess: BatchProcess) => void; /** * Emitted when a child process has ended */ - childEnd: (childProcess: BatchProcess, reason: ChildEndReason) => void + childEnd: (childProcess: BatchProcess, reason: ChildEndReason) => void; /** * Emitted when a child process fails to spin up and run the {@link BatchProcessOptions.versionCommand} successfully within {@link BatchClusterOptions.spawnTimeoutMillis}. * * @param childProcess will be undefined if the error is from {@link ChildProcessFactory.processFactory} */ - startError: (error: Error, childProcess?: BatchProcess) => void + startError: (error: Error, childProcess?: BatchProcess) => void; /** * Emitted when an internal consistency check fails */ - internalError: (error: Error) => void + internalError: (error: Error) => void; /** * Emitted when `.end()` is called because the error rate has exceeded * {@link BatchClusterOptions.maxReasonableProcessFailuresPerMinute} */ - fatalError: (error: Error) => void + fatalError: (error: Error) => void; /** * Emitted when tasks receive data, which may be partial chunks from the task @@ -72,12 +72,12 @@ export interface BatchClusterEvents { data: Buffer | string, task: Task | undefined, proc: BatchProcess, - ) => void + ) => void; /** * Emitted when a task has been resolved */ - taskResolved: (task: Task, proc: BatchProcess) => void + taskResolved: (task: Task, proc: BatchProcess) => void; /** * Emitted when a task times out. Note that a `taskError` event always succeeds these events. @@ -86,12 +86,12 @@ export interface BatchClusterEvents { timeoutMs: number, task: Task, proc: BatchProcess, - ) => void + ) => void; /** * Emitted when a task has an error */ - taskError: (error: Error, task: Task, proc: BatchProcess) => void + taskError: (error: Error, task: Task, proc: BatchProcess) => void; /** * Emitted when child processes write to stdout or stderr without a current @@ -101,28 +101,28 @@ export interface BatchClusterEvents { stdoutData: string | Buffer | null, stderrData: string | Buffer | null, proc: BatchProcess, - ) => void + ) => void; /** * Emitted when a process fails health checks */ - healthCheckError: (error: Error, proc: BatchProcess) => void + healthCheckError: (error: Error, proc: BatchProcess) => void; /** * Emitted when a child process has an error during shutdown */ - endError: (error: Error, proc?: BatchProcess) => void + endError: (error: Error, proc?: BatchProcess) => void; /** * Emitted when this instance is in the process of ending. */ - beforeEnd: () => void + beforeEnd: () => void; /** * Emitted when this instance has ended. No child processes should remain at * this point. */ - end: () => void + end: () => void; } /** @@ -145,4 +145,4 @@ export interface BatchClusterEvents { * See {@link BatchClusterEvents} for a the list of events and their payload * signatures */ -export type BatchClusterEmitter = TypedEventEmitter +export type BatchClusterEmitter = TypedEventEmitter; diff --git a/src/BatchClusterEventCoordinator.spec.ts b/src/BatchClusterEventCoordinator.spec.ts index 07bac7d..ce2fece 100644 --- a/src/BatchClusterEventCoordinator.spec.ts +++ b/src/BatchClusterEventCoordinator.spec.ts @@ -1,157 +1,157 @@ -import events from "node:events" -import { expect } from "./_chai.spec" -import { BatchClusterEmitter } from "./BatchClusterEmitter" +import events from "node:events"; +import { expect } from "./_chai.spec"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; import { BatchClusterEventCoordinator, EventCoordinatorOptions, -} from "./BatchClusterEventCoordinator" -import { BatchProcess } from "./BatchProcess" -import { logger } from "./Logger" -import { Task } from "./Task" +} from "./BatchClusterEventCoordinator"; +import { BatchProcess } from "./BatchProcess"; +import { logger } from "./Logger"; +import { Task } from "./Task"; describe("BatchClusterEventCoordinator", function () { - let eventCoordinator: BatchClusterEventCoordinator - let emitter: BatchClusterEmitter - let onIdleCalledCount = 0 - let endClusterCalledCount = 0 + let eventCoordinator: BatchClusterEventCoordinator; + let emitter: BatchClusterEmitter; + let onIdleCalledCount = 0; + let endClusterCalledCount = 0; const options: EventCoordinatorOptions = { streamFlushMillis: 100, maxReasonableProcessFailuresPerMinute: 5, logger, - } + }; const onIdleLater = () => { - onIdleCalledCount++ - } + onIdleCalledCount++; + }; const endCluster = () => { - endClusterCalledCount++ - } + endClusterCalledCount++; + }; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter + emitter = new events.EventEmitter() as BatchClusterEmitter; eventCoordinator = new BatchClusterEventCoordinator( emitter, options, onIdleLater, endCluster, - ) - onIdleCalledCount = 0 - endClusterCalledCount = 0 - }) + ); + onIdleCalledCount = 0; + endClusterCalledCount = 0; + }); describe("initial state", function () { it("should start with clean statistics", function () { - expect(eventCoordinator.meanTasksPerProc).to.eql(0) - expect(eventCoordinator.internalErrorCount).to.eql(0) - expect(eventCoordinator.startErrorRatePerMinute).to.eql(0) - expect(eventCoordinator.countEndedChildProcs("ended")).to.eql(0) - expect(eventCoordinator.childEndCounts).to.eql({}) - }) + expect(eventCoordinator.meanTasksPerProc).to.eql(0); + expect(eventCoordinator.internalErrorCount).to.eql(0); + expect(eventCoordinator.startErrorRatePerMinute).to.eql(0); + expect(eventCoordinator.countEndedChildProcs("ended")).to.eql(0); + expect(eventCoordinator.childEndCounts).to.eql({}); + }); it("should provide clean event statistics", function () { - const stats = eventCoordinator.getEventStats() - expect(stats.meanTasksPerProc).to.eql(0) - expect(stats.internalErrorCount).to.eql(0) - expect(stats.startErrorRatePerMinute).to.eql(0) - expect(stats.totalChildEndEvents).to.eql(0) - expect(stats.childEndReasons).to.eql([]) - }) - }) + const stats = eventCoordinator.getEventStats(); + expect(stats.meanTasksPerProc).to.eql(0); + expect(stats.internalErrorCount).to.eql(0); + expect(stats.startErrorRatePerMinute).to.eql(0); + expect(stats.totalChildEndEvents).to.eql(0); + expect(stats.childEndReasons).to.eql([]); + }); + }); describe("childEnd event handling", function () { it("should handle childEnd events and update statistics", function () { const mockProcess = { taskCount: 5, pid: 12345, - } as BatchProcess + } as BatchProcess; // Emit childEnd event - emitter.emit("childEnd", mockProcess, "worn") + emitter.emit("childEnd", mockProcess, "worn"); - expect(eventCoordinator.meanTasksPerProc).to.eql(5) - expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(1) - expect(eventCoordinator.childEndCounts.worn).to.eql(1) - expect(onIdleCalledCount).to.eql(1) - }) + expect(eventCoordinator.meanTasksPerProc).to.eql(5); + expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(1); + expect(eventCoordinator.childEndCounts.worn).to.eql(1); + expect(onIdleCalledCount).to.eql(1); + }); it("should track multiple childEnd events", function () { - const mockProcess1 = { taskCount: 3 } as BatchProcess - const mockProcess2 = { taskCount: 7 } as BatchProcess - const mockProcess3 = { taskCount: 5 } as BatchProcess - - emitter.emit("childEnd", mockProcess1, "worn") - emitter.emit("childEnd", mockProcess2, "old") - emitter.emit("childEnd", mockProcess3, "worn") - - expect(eventCoordinator.meanTasksPerProc).to.eql(5) // (3+7+5)/3 - expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2) - expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1) - expect(eventCoordinator.childEndCounts.worn).to.eql(2) - expect(eventCoordinator.childEndCounts.old).to.eql(1) - expect(onIdleCalledCount).to.eql(3) - }) - }) + const mockProcess1 = { taskCount: 3 } as BatchProcess; + const mockProcess2 = { taskCount: 7 } as BatchProcess; + const mockProcess3 = { taskCount: 5 } as BatchProcess; + + emitter.emit("childEnd", mockProcess1, "worn"); + emitter.emit("childEnd", mockProcess2, "old"); + emitter.emit("childEnd", mockProcess3, "worn"); + + expect(eventCoordinator.meanTasksPerProc).to.eql(5); // (3+7+5)/3 + expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2); + expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1); + expect(eventCoordinator.childEndCounts.worn).to.eql(2); + expect(eventCoordinator.childEndCounts.old).to.eql(1); + expect(onIdleCalledCount).to.eql(3); + }); + }); describe("internalError event handling", function () { it("should handle internalError events and increment counter", function () { - const error = new Error("Internal error occurred") + const error = new Error("Internal error occurred"); - emitter.emit("internalError", error) + emitter.emit("internalError", error); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) + expect(eventCoordinator.internalErrorCount).to.eql(1); + }); it("should handle multiple internalError events", function () { - emitter.emit("internalError", new Error("Error 1")) - emitter.emit("internalError", new Error("Error 2")) - emitter.emit("internalError", new Error("Error 3")) + emitter.emit("internalError", new Error("Error 1")); + emitter.emit("internalError", new Error("Error 2")); + emitter.emit("internalError", new Error("Error 3")); - expect(eventCoordinator.internalErrorCount).to.eql(3) - }) - }) + expect(eventCoordinator.internalErrorCount).to.eql(3); + }); + }); describe("noTaskData event handling", function () { it("should handle noTaskData events and increment internal error count", function () { - const mockProcess = { pid: 12345 } as BatchProcess + const mockProcess = { pid: 12345 } as BatchProcess; - emitter.emit("noTaskData", "some stdout", "some stderr", mockProcess) + emitter.emit("noTaskData", "some stdout", "some stderr", mockProcess); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) + expect(eventCoordinator.internalErrorCount).to.eql(1); + }); it("should handle noTaskData with null data", function () { - const mockProcess = { pid: 12345 } as BatchProcess + const mockProcess = { pid: 12345 } as BatchProcess; - emitter.emit("noTaskData", null, null, mockProcess) + emitter.emit("noTaskData", null, null, mockProcess); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) + expect(eventCoordinator.internalErrorCount).to.eql(1); + }); it("should handle noTaskData with buffer data", function () { - const mockProcess = { pid: 12345 } as BatchProcess - const bufferData = Buffer.from("test data") + const mockProcess = { pid: 12345 } as BatchProcess; + const bufferData = Buffer.from("test data"); - emitter.emit("noTaskData", bufferData, null, mockProcess) + emitter.emit("noTaskData", bufferData, null, mockProcess); - expect(eventCoordinator.internalErrorCount).to.eql(1) - }) - }) + expect(eventCoordinator.internalErrorCount).to.eql(1); + }); + }); describe("startError event handling", function () { it("should handle startError events without triggering fatal error", function () { - const error = new Error("Start error") + const error = new Error("Start error"); - emitter.emit("startError", error) + emitter.emit("startError", error); // Rate might be 0 initially due to warmup period expect(eventCoordinator.startErrorRatePerMinute).to.be.greaterThanOrEqual( 0, - ) - expect(endClusterCalledCount).to.eql(0) - expect(onIdleCalledCount).to.eql(1) - }) + ); + expect(endClusterCalledCount).to.eql(0); + expect(onIdleCalledCount).to.eql(1); + }); it("should have logic to trigger fatal error based on rate", function () { // This test verifies the logic exists, but doesn't test timing-dependent rate calculation @@ -160,174 +160,174 @@ describe("BatchClusterEventCoordinator", function () { const testOptions: EventCoordinatorOptions = { ...options, maxReasonableProcessFailuresPerMinute: 5, - } + }; const testCoordinator = new BatchClusterEventCoordinator( emitter, testOptions, onIdleLater, endCluster, - ) + ); // Verify that start error rate tracking is working - emitter.emit("startError", new Error("Test error")) + emitter.emit("startError", new Error("Test error")); expect(testCoordinator.startErrorRatePerMinute).to.be.greaterThanOrEqual( 0, - ) + ); // The actual fatal error triggering depends on Rate class timing // which is tested in the Rate class's own tests - }) + }); it("should not trigger fatal error when rate limit is disabled", function () { const noLimitOptions: EventCoordinatorOptions = { ...options, maxReasonableProcessFailuresPerMinute: 0, // Disabled - } + }; new BatchClusterEventCoordinator( emitter, noLimitOptions, onIdleLater, endCluster, - ) + ); - let fatalErrorEmitted = false + let fatalErrorEmitted = false; emitter.on("fatalError", () => { - fatalErrorEmitted = true - }) + fatalErrorEmitted = true; + }); // Emit many start errors for (let i = 0; i < 20; i++) { - emitter.emit("startError", new Error(`Start error ${i}`)) + emitter.emit("startError", new Error(`Start error ${i}`)); } - expect(fatalErrorEmitted).to.be.false - expect(endClusterCalledCount).to.eql(0) - }) - }) + expect(fatalErrorEmitted).to.be.false; + expect(endClusterCalledCount).to.eql(0); + }); + }); describe("event access", function () { it("should provide access to the underlying emitter", function () { - expect(eventCoordinator.events).to.equal(emitter) - }) + expect(eventCoordinator.events).to.equal(emitter); + }); it("should allow direct event emission through events property", function () { - let eventReceived = false - let receivedData: any + let eventReceived = false; + let receivedData: any; emitter.on("taskData", (data, task, proc) => { - eventReceived = true - receivedData = { data, task, proc } - }) + eventReceived = true; + receivedData = { data, task, proc }; + }); - const mockTask = {} as Task - const mockProcess = {} as BatchProcess - const testData = "test data" + const mockTask = {} as Task; + const mockProcess = {} as BatchProcess; + const testData = "test data"; const result = eventCoordinator.events.emit( "taskData", testData, mockTask, mockProcess, - ) + ); - expect(result).to.be.true - expect(eventReceived).to.be.true - expect(receivedData.data).to.eql(testData) - expect(receivedData.task).to.eql(mockTask) - expect(receivedData.proc).to.eql(mockProcess) - }) + expect(result).to.be.true; + expect(eventReceived).to.be.true; + expect(receivedData.data).to.eql(testData); + expect(receivedData.task).to.eql(mockTask); + expect(receivedData.proc).to.eql(mockProcess); + }); it("should allow direct event listener management through events property", function () { - let eventReceived = false + let eventReceived = false; const listener = () => { - eventReceived = true - } + eventReceived = true; + }; - eventCoordinator.events.on("beforeEnd", listener) - emitter.emit("beforeEnd") - expect(eventReceived).to.be.true + eventCoordinator.events.on("beforeEnd", listener); + emitter.emit("beforeEnd"); + expect(eventReceived).to.be.true; - eventReceived = false - eventCoordinator.events.off("beforeEnd", listener) - emitter.emit("beforeEnd") - expect(eventReceived).to.be.false - }) - }) + eventReceived = false; + eventCoordinator.events.off("beforeEnd", listener); + emitter.emit("beforeEnd"); + expect(eventReceived).to.be.false; + }); + }); describe("statistics and monitoring", function () { beforeEach(function () { // Set up some test data - const mockProcess1 = { taskCount: 10 } as BatchProcess - const mockProcess2 = { taskCount: 20 } as BatchProcess + const mockProcess1 = { taskCount: 10 } as BatchProcess; + const mockProcess2 = { taskCount: 20 } as BatchProcess; - emitter.emit("childEnd", mockProcess1, "worn") - emitter.emit("childEnd", mockProcess2, "old") - emitter.emit("internalError", new Error("Test error")) - emitter.emit("startError", new Error("Start error")) - }) + emitter.emit("childEnd", mockProcess1, "worn"); + emitter.emit("childEnd", mockProcess2, "old"); + emitter.emit("internalError", new Error("Test error")); + emitter.emit("startError", new Error("Start error")); + }); it("should provide comprehensive event statistics", function () { - const stats = eventCoordinator.getEventStats() + const stats = eventCoordinator.getEventStats(); - expect(stats.meanTasksPerProc).to.eql(15) // (10+20)/2 - expect(stats.internalErrorCount).to.eql(1) - expect(stats.startErrorRatePerMinute).to.be.greaterThanOrEqual(0) // Rate might be 0 due to warmup - expect(stats.totalChildEndEvents).to.eql(2) - expect(stats.childEndReasons).to.include("worn") - expect(stats.childEndReasons).to.include("old") - }) + expect(stats.meanTasksPerProc).to.eql(15); // (10+20)/2 + expect(stats.internalErrorCount).to.eql(1); + expect(stats.startErrorRatePerMinute).to.be.greaterThanOrEqual(0); // Rate might be 0 due to warmup + expect(stats.totalChildEndEvents).to.eql(2); + expect(stats.childEndReasons).to.include("worn"); + expect(stats.childEndReasons).to.include("old"); + }); it("should reset statistics correctly", function () { // Verify we have some data - expect(eventCoordinator.meanTasksPerProc).to.eql(15) - expect(eventCoordinator.internalErrorCount).to.eql(1) + expect(eventCoordinator.meanTasksPerProc).to.eql(15); + expect(eventCoordinator.internalErrorCount).to.eql(1); - eventCoordinator.resetStats() + eventCoordinator.resetStats(); // Verify everything is reset - expect(eventCoordinator.meanTasksPerProc).to.eql(0) - expect(eventCoordinator.internalErrorCount).to.eql(0) - expect(eventCoordinator.startErrorRatePerMinute).to.eql(0) - expect(eventCoordinator.childEndCounts).to.eql({}) + expect(eventCoordinator.meanTasksPerProc).to.eql(0); + expect(eventCoordinator.internalErrorCount).to.eql(0); + expect(eventCoordinator.startErrorRatePerMinute).to.eql(0); + expect(eventCoordinator.childEndCounts).to.eql({}); - const stats = eventCoordinator.getEventStats() - expect(stats.totalChildEndEvents).to.eql(0) - expect(stats.childEndReasons).to.eql([]) - }) + const stats = eventCoordinator.getEventStats(); + expect(stats.totalChildEndEvents).to.eql(0); + expect(stats.childEndReasons).to.eql([]); + }); it("should track child end counts accurately", function () { // Add more events of different types - const mockProcess3 = { taskCount: 5 } as BatchProcess - const mockProcess4 = { taskCount: 8 } as BatchProcess + const mockProcess3 = { taskCount: 5 } as BatchProcess; + const mockProcess4 = { taskCount: 8 } as BatchProcess; - emitter.emit("childEnd", mockProcess3, "worn") // Second worn - emitter.emit("childEnd", mockProcess4, "broken") // New type + emitter.emit("childEnd", mockProcess3, "worn"); // Second worn + emitter.emit("childEnd", mockProcess4, "broken"); // New type - expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2) - expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1) - expect(eventCoordinator.countEndedChildProcs("broken")).to.eql(1) - expect(eventCoordinator.countEndedChildProcs("timeout")).to.eql(0) + expect(eventCoordinator.countEndedChildProcs("worn")).to.eql(2); + expect(eventCoordinator.countEndedChildProcs("old")).to.eql(1); + expect(eventCoordinator.countEndedChildProcs("broken")).to.eql(1); + expect(eventCoordinator.countEndedChildProcs("timeout")).to.eql(0); - const childEndCounts = eventCoordinator.childEndCounts - expect(childEndCounts.worn).to.eql(2) - expect(childEndCounts.old).to.eql(1) - expect(childEndCounts.broken).to.eql(1) - }) - }) + const childEndCounts = eventCoordinator.childEndCounts; + expect(childEndCounts.worn).to.eql(2); + expect(childEndCounts.old).to.eql(1); + expect(childEndCounts.broken).to.eql(1); + }); + }); describe("callback integration", function () { it("should call onIdleLater for appropriate events", function () { - const initialCount = onIdleCalledCount + const initialCount = onIdleCalledCount; // Events that should trigger onIdleLater - emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn") - emitter.emit("startError", new Error("Start error")) + emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn"); + emitter.emit("startError", new Error("Start error")); - expect(onIdleCalledCount).to.eql(initialCount + 2) - }) + expect(onIdleCalledCount).to.eql(initialCount + 2); + }); it("should have callback integration for endCluster", function () { // This test verifies that the endCluster callback is properly integrated @@ -338,24 +338,24 @@ describe("BatchClusterEventCoordinator", function () { options, onIdleLater, endCluster, - ) + ); // Verify the coordinator is set up and callbacks are connected - expect(testCoordinator.events).to.equal(emitter) + expect(testCoordinator.events).to.equal(emitter); // The endCluster callback integration is verified through the logic // The actual rate-based triggering is tested in integration scenarios - }) + }); it("should not call endCluster for non-fatal events", function () { - const initialCount = endClusterCalledCount + const initialCount = endClusterCalledCount; // Events that should not trigger endCluster - emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn") - emitter.emit("internalError", new Error("Internal error")) - emitter.emit("noTaskData", "data", null, {} as BatchProcess) - - expect(endClusterCalledCount).to.eql(initialCount) - }) - }) -}) + emitter.emit("childEnd", { taskCount: 5 } as BatchProcess, "worn"); + emitter.emit("internalError", new Error("Internal error")); + emitter.emit("noTaskData", "data", null, {} as BatchProcess); + + expect(endClusterCalledCount).to.eql(initialCount); + }); + }); +}); diff --git a/src/BatchClusterEventCoordinator.ts b/src/BatchClusterEventCoordinator.ts index 0e09989..737e531 100644 --- a/src/BatchClusterEventCoordinator.ts +++ b/src/BatchClusterEventCoordinator.ts @@ -1,17 +1,17 @@ -import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter" -import { BatchProcess } from "./BatchProcess" -import { Logger } from "./Logger" -import { Mean } from "./Mean" -import { Rate } from "./Rate" -import { toS } from "./String" +import { BatchClusterEmitter, ChildEndReason } from "./BatchClusterEmitter"; +import { BatchProcess } from "./BatchProcess"; +import { Logger } from "./Logger"; +import { Mean } from "./Mean"; +import { Rate } from "./Rate"; +import { toS } from "./String"; /** * Configuration for event handling behavior */ export interface EventCoordinatorOptions { - readonly streamFlushMillis: number - readonly maxReasonableProcessFailuresPerMinute: number - readonly logger: () => Logger + readonly streamFlushMillis: number; + readonly maxReasonableProcessFailuresPerMinute: number; + readonly logger: () => Logger; } /** @@ -19,11 +19,11 @@ export interface EventCoordinatorOptions { * Handles event processing, statistics tracking, and automated responses to events. */ export class BatchClusterEventCoordinator { - readonly #logger: () => Logger - #tasksPerProc = new Mean() - #startErrorRate = new Rate() - readonly #childEndCounts = new Map() - #internalErrorCount = 0 + readonly #logger: () => Logger; + #tasksPerProc = new Mean(); + #startErrorRate = new Rate(); + readonly #childEndCounts = new Map(); + #internalErrorCount = 0; constructor( private readonly emitter: BatchClusterEmitter, @@ -31,42 +31,42 @@ export class BatchClusterEventCoordinator { private readonly onIdleLater: () => void, private readonly endCluster: () => void, ) { - this.#logger = options.logger - this.#setupEventHandlers() + this.#logger = options.logger; + this.#setupEventHandlers(); } /** * Set up all event handlers for the BatchCluster */ #setupEventHandlers(): void { - this.emitter.on("childEnd", (bp, why) => this.#handleChildEnd(bp, why)) + this.emitter.on("childEnd", (bp, why) => this.#handleChildEnd(bp, why)); this.emitter.on("internalError", (error) => this.#handleInternalError(error), - ) + ); this.emitter.on("noTaskData", (stdout, stderr, proc) => this.#handleNoTaskData(stdout, stderr, proc), - ) - this.emitter.on("startError", (error) => this.#handleStartError(error)) + ); + this.emitter.on("startError", (error) => this.#handleStartError(error)); } /** * Handle child process end events */ #handleChildEnd(process: BatchProcess, reason: ChildEndReason): void { - this.#tasksPerProc.push(process.taskCount) + this.#tasksPerProc.push(process.taskCount); this.#childEndCounts.set( reason, (this.#childEndCounts.get(reason) ?? 0) + 1, - ) - this.onIdleLater() + ); + this.onIdleLater(); } /** * Handle internal error events */ #handleInternalError(error: Error): void { - this.#logger().error("BatchCluster: INTERNAL ERROR: " + String(error)) - this.#internalErrorCount++ + this.#logger().error("BatchCluster: INTERNAL ERROR: " + String(error)); + this.#internalErrorCount++; } /** @@ -85,16 +85,16 @@ export class BatchClusterEventCoordinator { stderr: toS(stderr), proc_pid: proc?.pid, }, - ) - this.#internalErrorCount++ + ); + this.#internalErrorCount++; } /** * Handle start error events */ #handleStartError(error: Error): void { - this.#logger().warn("BatchCluster.onStartError(): " + String(error)) - this.#startErrorRate.onEvent() + this.#logger().warn("BatchCluster.onStartError(): " + String(error)); + this.#startErrorRate.onEvent(); if ( this.options.maxReasonableProcessFailuresPerMinute > 0 && @@ -109,10 +109,10 @@ export class BatchClusterEventCoordinator { this.#startErrorRate.eventsPerMinute.toFixed(2) + ")", ), - ) - this.endCluster() + ); + this.endCluster(); } else { - this.onIdleLater() + this.onIdleLater(); } } @@ -120,29 +120,29 @@ export class BatchClusterEventCoordinator { * Get the mean number of tasks completed by child processes */ get meanTasksPerProc(): number { - const mean = this.#tasksPerProc.mean - return isNaN(mean) ? 0 : mean + const mean = this.#tasksPerProc.mean; + return isNaN(mean) ? 0 : mean; } /** * Get internal error count */ get internalErrorCount(): number { - return this.#internalErrorCount + return this.#internalErrorCount; } /** * Get start error rate per minute */ get startErrorRatePerMinute(): number { - return this.#startErrorRate.eventsPerMinute + return this.#startErrorRate.eventsPerMinute; } /** * Get count of ended child processes by reason */ countEndedChildProcs(reason: ChildEndReason): number { - return this.#childEndCounts.get(reason) ?? 0 + return this.#childEndCounts.get(reason) ?? 0; } /** @@ -152,7 +152,7 @@ export class BatchClusterEventCoordinator { return Object.fromEntries([...this.#childEndCounts.entries()]) as Record< NonNullable, number - > + >; } /** @@ -168,23 +168,23 @@ export class BatchClusterEventCoordinator { 0, ), childEndReasons: Object.keys(this.childEndCounts), - } + }; } /** * Reset event statistics (useful for testing) */ resetStats(): void { - this.#tasksPerProc = new Mean() - this.#startErrorRate = new Rate() - this.#childEndCounts.clear() - this.#internalErrorCount = 0 + this.#tasksPerProc = new Mean(); + this.#startErrorRate = new Rate(); + this.#childEndCounts.clear(); + this.#internalErrorCount = 0; } /** * Get the underlying emitter for direct event access */ get events(): BatchClusterEmitter { - return this.emitter + return this.emitter; } } diff --git a/src/BatchClusterOptions.spec.ts b/src/BatchClusterOptions.spec.ts index 31f1c8f..0282860 100644 --- a/src/BatchClusterOptions.spec.ts +++ b/src/BatchClusterOptions.spec.ts @@ -1,34 +1,34 @@ -import { BatchCluster } from "./BatchCluster" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { verifyOptions } from "./OptionsVerifier" -import { expect, processFactory } from "./_chai.spec" +import { BatchCluster } from "./BatchCluster"; +import { DefaultTestOptions } from "./DefaultTestOptions.spec"; +import { verifyOptions } from "./OptionsVerifier"; +import { expect, processFactory } from "./_chai.spec"; describe("BatchClusterOptions", () => { - let bc: BatchCluster - afterEach(() => bc?.end(false)) + let bc: BatchCluster; + afterEach(() => bc?.end(false)); describe("verifyOptions()", () => { function errToArr(err: unknown): string[] { - return String(err).split(/\s*[:;]\s*/) + return String(err).split(/\s*[:;]\s*/); } it("allows 0 maxProcAgeMillis", () => { const opts = { ...DefaultTestOptions, maxProcAgeMillis: 0, - } - expect(verifyOptions(opts as any)).to.containSubset(opts) - }) + }; + expect(verifyOptions(opts as any)).to.containSubset(opts); + }); it("requires maxProcAgeMillis to be > spawnTimeoutMillis", () => { - const spawnTimeoutMillis = DefaultTestOptions.taskTimeoutMillis + 1 + const spawnTimeoutMillis = DefaultTestOptions.taskTimeoutMillis + 1; try { bc = new BatchCluster({ processFactory, ...DefaultTestOptions, spawnTimeoutMillis, maxProcAgeMillis: spawnTimeoutMillis - 1, - }) - throw new Error("expected an error due to invalid opts") + }); + throw new Error("expected an error due to invalid opts"); } catch (err) { expect(errToArr(err)).to.eql([ "Error", @@ -36,20 +36,20 @@ describe("BatchClusterOptions", () => { "maxProcAgeMillis must be greater than or equal to " + spawnTimeoutMillis, `the max value of spawnTimeoutMillis (${spawnTimeoutMillis}) and taskTimeoutMillis (${DefaultTestOptions.taskTimeoutMillis})`, - ]) + ]); } - }) + }); it("requires maxProcAgeMillis to be > taskTimeoutMillis", () => { - const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1 + const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1; try { bc = new BatchCluster({ processFactory, ...DefaultTestOptions, taskTimeoutMillis, maxProcAgeMillis: taskTimeoutMillis - 1, - }) - throw new Error("expected an error due to invalid opts") + }); + throw new Error("expected an error due to invalid opts"); } catch (err) { expect(errToArr(err)).to.eql([ "Error", @@ -57,21 +57,21 @@ describe("BatchClusterOptions", () => { "maxProcAgeMillis must be greater than or equal to " + taskTimeoutMillis, `the max value of spawnTimeoutMillis (${DefaultTestOptions.spawnTimeoutMillis}) and taskTimeoutMillis (${taskTimeoutMillis})`, - ]) + ]); } - }) + }); it("allows maxProcAgeMillis to be 0", () => { - const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1 + const taskTimeoutMillis = DefaultTestOptions.spawnTimeoutMillis + 1; bc = new BatchCluster({ processFactory, ...DefaultTestOptions, taskTimeoutMillis, maxProcAgeMillis: 0, - }) + }); - expect(bc.options.maxProcAgeMillis).to.equal(0) - }) + expect(bc.options.maxProcAgeMillis).to.equal(0); + }); it("reports on invalid opts", () => { try { @@ -90,8 +90,8 @@ describe("BatchClusterOptions", () => { onIdleIntervalMillis: -1, endGracefulWaitTimeMillis: -1, streamFlushMillis: -1, - }) - throw new Error("expected an error due to invalid opts") + }); + throw new Error("expected an error due to invalid opts"); } catch (err) { expect(errToArr(err)).to.eql([ "Error", @@ -109,8 +109,8 @@ describe("BatchClusterOptions", () => { "endGracefulWaitTimeMillis must be greater than or equal to 0", "maxReasonableProcessFailuresPerMinute must be greater than or equal to 0", "streamFlushMillis must be greater than or equal to 0", - ]) + ]); } - }) - }) -}) + }); + }); +}); diff --git a/src/BatchClusterOptions.ts b/src/BatchClusterOptions.ts index 64f9710..f81ffc8 100644 --- a/src/BatchClusterOptions.ts +++ b/src/BatchClusterOptions.ts @@ -1,9 +1,9 @@ -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { logger, Logger } from "./Logger" -import { isMac, isWin } from "./Platform" +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { logger, Logger } from "./Logger"; +import { isMac, isWin } from "./Platform"; -export const secondMs = 1000 -export const minuteMs = 60 * secondMs +export const secondMs = 1000; +export const minuteMs = 60 * secondMs; /** * These parameter values have somewhat sensible defaults, but can be @@ -16,7 +16,7 @@ export class BatchClusterOptions { * * Defaults to 1. */ - maxProcs = 1 + maxProcs = 1; /** * Child processes will be recycled when they reach this age. @@ -26,7 +26,7 @@ export class BatchClusterOptions { * * Defaults to 5 minutes. Set to 0 to disable. */ - maxProcAgeMillis = 5 * minuteMs + maxProcAgeMillis = 5 * minuteMs; /** * This is the minimum interval between calls to BatchCluster's #onIdle @@ -35,7 +35,7 @@ export class BatchClusterOptions { * * Must be > 0. Defaults to 10 seconds. */ - onIdleIntervalMillis = 10 * secondMs + onIdleIntervalMillis = 10 * secondMs; /** * If the initial `versionCommand` fails for new spawned processes more @@ -47,7 +47,7 @@ export class BatchClusterOptions { * * Defaults to 10. Set to 0 to disable. */ - maxReasonableProcessFailuresPerMinute = 10 + maxReasonableProcessFailuresPerMinute = 10; /** * Spawning new child processes and servicing a "version" task must not take @@ -57,7 +57,7 @@ export class BatchClusterOptions { * * Defaults to 15 seconds. Set to 0 to disable. */ - spawnTimeoutMillis = 15 * secondMs + spawnTimeoutMillis = 15 * secondMs; /** * If maxProcs > 1, spawning new child processes to process tasks can slow @@ -65,7 +65,7 @@ export class BatchClusterOptions { * * Must be >= 0ms. Defaults to 1.5 seconds. */ - minDelayBetweenSpawnMillis = 1.5 * secondMs + minDelayBetweenSpawnMillis = 1.5 * secondMs; /** * If commands take longer than this, presume the underlying process is dead @@ -75,7 +75,7 @@ export class BatchClusterOptions { * * Defaults to 10 seconds. Set to 0 to disable. */ - taskTimeoutMillis = 10 * secondMs + taskTimeoutMillis = 10 * secondMs; /** * Processes will be recycled after processing `maxTasksPerProcess` tasks. @@ -88,7 +88,7 @@ export class BatchClusterOptions { * * Must be >= 0. Defaults to 500 */ - maxTasksPerProcess = 500 + maxTasksPerProcess = 500; /** * When `this.end()` is called, or Node broadcasts the `beforeExit` event, @@ -99,7 +99,7 @@ export class BatchClusterOptions { * kill signal to shut down. Any pending requests may be interrupted. Must be * >= 0. Defaults to 500ms. */ - endGracefulWaitTimeMillis = 500 + endGracefulWaitTimeMillis = 500; /** * When a task sees a "pass" or "fail" from either stdout or stderr, it needs @@ -123,7 +123,7 @@ export class BatchClusterOptions { */ // These values were found by trial and error using GitHub CI boxes, which // should be the bottom of the barrel, performance-wise, of any computer. - streamFlushMillis = isMac ? 100 : isWin ? 200 : 30 + streamFlushMillis = isMac ? 100 : isWin ? 200 : 30; /** * Should batch-cluster try to clean up after spawned processes that don't @@ -133,7 +133,7 @@ export class BatchClusterOptions { * * Defaults to `true`. */ - cleanupChildProcs = true + cleanupChildProcs = true; /** * If a child process is idle for more than this value (in milliseconds), shut @@ -142,7 +142,7 @@ export class BatchClusterOptions { * A value of ~10 seconds to a couple minutes would be reasonable. Set this to * 0 to disable this feature. */ - maxIdleMsPerProcess = 0 + maxIdleMsPerProcess = 0; /** * How many failed tasks should a process be allowed to process before it is @@ -150,7 +150,7 @@ export class BatchClusterOptions { * * Set this to 0 to disable this feature. */ - maxFailedTasksPerProcess = 2 + maxFailedTasksPerProcess = 2; /** * If `healthCheckCommand` is set, how frequently should we check for @@ -158,22 +158,22 @@ export class BatchClusterOptions { * * Set this to 0 to disable this feature. */ - healthCheckIntervalMillis = 0 + healthCheckIntervalMillis = 0; /** * Verify child processes are still running by checking the OS process table. * * Set this to 0 to disable this feature. */ - pidCheckIntervalMillis = 2 * minuteMs + pidCheckIntervalMillis = 2 * minuteMs; /** * A BatchCluster instance and associated BatchProcess instances will share * this `Logger`. Defaults to the `Logger` instance provided to `setLogger()`. */ - logger: () => Logger = logger + logger: () => Logger = logger; } export interface WithObserver { - observer: BatchClusterEmitter + observer: BatchClusterEmitter; } diff --git a/src/BatchClusterStats.ts b/src/BatchClusterStats.ts index ea9adf6..7257874 100644 --- a/src/BatchClusterStats.ts +++ b/src/BatchClusterStats.ts @@ -1,16 +1,16 @@ -import { ChildEndReason } from "./BatchClusterEmitter" +import { ChildEndReason } from "./BatchClusterEmitter"; export interface BatchClusterStats { - pendingTaskCount: number - currentProcCount: number - readyProcCount: number - maxProcCount: number - internalErrorCount: number - startErrorRatePerMinute: number - msBeforeNextSpawn: number - spawnedProcCount: number - childEndCounts: Record, number> - ending: boolean - ended: boolean - [key: string]: unknown + pendingTaskCount: number; + currentProcCount: number; + readyProcCount: number; + maxProcCount: number; + internalErrorCount: number; + startErrorRatePerMinute: number; + msBeforeNextSpawn: number; + spawnedProcCount: number; + childEndCounts: Record, number>; + ending: boolean; + ended: boolean; + [key: string]: unknown; } diff --git a/src/BatchProcess.ts b/src/BatchProcess.ts index 390761b..e8f8e6f 100644 --- a/src/BatchProcess.ts +++ b/src/BatchProcess.ts @@ -1,56 +1,56 @@ -import child_process from "node:child_process" -import timers from "node:timers" -import { Deferred } from "./Deferred" -import { cleanError } from "./Error" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { Logger } from "./Logger" -import { map } from "./Object" -import { SimpleParser } from "./Parser" -import { pidExists } from "./Pids" -import { ProcessHealthMonitor } from "./ProcessHealthMonitor" -import { ProcessTerminator } from "./ProcessTerminator" -import { StreamContext, StreamHandler } from "./StreamHandler" -import { ensureSuffix } from "./String" -import { Task } from "./Task" -import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" +import child_process from "node:child_process"; +import timers from "node:timers"; +import { Deferred } from "./Deferred"; +import { cleanError } from "./Error"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { Logger } from "./Logger"; +import { map } from "./Object"; +import { SimpleParser } from "./Parser"; +import { pidExists } from "./Pids"; +import { ProcessHealthMonitor } from "./ProcessHealthMonitor"; +import { ProcessTerminator } from "./ProcessTerminator"; +import { StreamContext, StreamHandler } from "./StreamHandler"; +import { ensureSuffix } from "./String"; +import { Task } from "./Task"; +import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"; /** * BatchProcess manages the care and feeding of a single child process. */ export class BatchProcess { - readonly name: string - readonly pid: number - readonly start = Date.now() + readonly name: string; + readonly pid: number; + readonly start = Date.now(); - readonly startupTaskId: number - readonly #logger: () => Logger - readonly #terminator: ProcessTerminator - readonly #healthMonitor: ProcessHealthMonitor - readonly #streamHandler: StreamHandler - #lastJobFinshedAt = Date.now() + readonly startupTaskId: number; + readonly #logger: () => Logger; + readonly #terminator: ProcessTerminator; + readonly #healthMonitor: ProcessHealthMonitor; + readonly #streamHandler: StreamHandler; + #lastJobFinshedAt = Date.now(); // Only set to true when `proc.pid` is no longer in the process table. - #starting = true + #starting = true; - #exited = false + #exited = false; // override for .whyNotHealthy() - #whyNotHealthy?: WhyNotHealthy + #whyNotHealthy?: WhyNotHealthy; - failedTaskCount = 0 + failedTaskCount = 0; - #taskCount = -1 // don't count the startupTask + #taskCount = -1; // don't count the startupTask /** * Should be undefined if this instance is not currently processing a task. */ - #currentTask: Task | undefined + #currentTask: Task | undefined; /** * Getter for current task (required by StreamContext interface) */ get currentTask(): Task | undefined { - return this.#currentTask + return this.#currentTask; } /** @@ -65,11 +65,11 @@ export class BatchProcess { this.#onError(reason as WhyNotHealthy, error), end: (gracefully: boolean, reason: string) => void this.end(gracefully, reason as WhyNotHealthy), - } - } - #currentTaskTimeout: NodeJS.Timeout | undefined + }; + }; + #currentTaskTimeout: NodeJS.Timeout | undefined; - #endPromise: undefined | Deferred + #endPromise: undefined | Deferred; /** * @param onIdle to be called when internal state changes (like the current @@ -81,65 +81,65 @@ export class BatchProcess { private readonly onIdle: () => void, healthMonitor?: ProcessHealthMonitor, ) { - this.name = "BatchProcess(" + proc.pid + ")" - this.#logger = opts.logger - this.#terminator = new ProcessTerminator(opts) + this.name = "BatchProcess(" + proc.pid + ")"; + this.#logger = opts.logger; + this.#terminator = new ProcessTerminator(opts); this.#healthMonitor = - healthMonitor ?? new ProcessHealthMonitor(opts, opts.observer) + healthMonitor ?? new ProcessHealthMonitor(opts, opts.observer); this.#streamHandler = new StreamHandler( { logger: this.#logger }, opts.observer, - ) + ); // don't let node count the child processes as a reason to stay alive - this.proc.unref() + this.proc.unref(); if (proc.pid == null) { - throw new Error("BatchProcess.constructor: child process pid is null") + throw new Error("BatchProcess.constructor: child process pid is null"); } - this.pid = proc.pid + this.pid = proc.pid; - this.proc.on("error", (err) => this.#onError("proc.error", err)) + this.proc.on("error", (err) => this.#onError("proc.error", err)); this.proc.on("close", () => { - void this.end(false, "proc.close") - }) + void this.end(false, "proc.close"); + }); this.proc.on("exit", () => { - void this.end(false, "proc.exit") - }) + void this.end(false, "proc.exit"); + }); this.proc.on("disconnect", () => { - void this.end(false, "proc.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 + const startupTask = new Task(opts.versionCommand, SimpleParser); + this.startupTaskId = startupTask.taskId; if (!this.execTask(startupTask)) { this.opts.observer.emit( "internalError", new Error(this.name + " startup task was not submitted"), - ) + ); } // Initialize health monitoring for this process - this.#healthMonitor.initializeProcess(this.pid) + this.#healthMonitor.initializeProcess(this.pid); // this needs to be at the end of the constructor, to ensure everything is // set up on `this` - this.opts.observer.emit("childStart", this) + this.opts.observer.emit("childStart", this); } get taskCount(): number { - return this.#taskCount + return this.#taskCount; } get starting(): boolean { - return this.#starting + return this.#starting; } /** @@ -147,7 +147,7 @@ export class BatchProcess { * child process exiting) */ get ending(): boolean { - return this.#endPromise != null + return this.#endPromise != null; } /** @@ -157,7 +157,7 @@ export class BatchProcess { * (but expensive!) answer. */ get ended(): boolean { - return true === this.#endPromise?.settled + return true === this.#endPromise?.settled; } /** @@ -167,7 +167,7 @@ export class BatchProcess { * expensive!) answer. */ get exited(): boolean { - return this.#exited + return this.#exited; } /** @@ -177,14 +177,14 @@ export class BatchProcess { * know if a process can handle a new task. */ get whyNotHealthy(): WhyNotHealthy | null { - return this.#healthMonitor.assessHealth(this, this.#whyNotHealthy) + return this.#healthMonitor.assessHealth(this, this.#whyNotHealthy); } /** * @return true if the process doesn't need to be recycled. */ get healthy(): boolean { - return this.whyNotHealthy == null + return this.whyNotHealthy == null; } /** @@ -192,7 +192,7 @@ export class BatchProcess { * process has ended or should be recycled: see {@link BatchProcess.ready}. */ get idle(): boolean { - return this.#currentTask == null + return this.#currentTask == null; } /** @@ -200,7 +200,7 @@ export class BatchProcess { * task, or `undefined` if this process is idle and healthy. */ get whyNotReady(): WhyNotReady | null { - return !this.idle ? "busy" : this.whyNotHealthy + return !this.idle ? "busy" : this.whyNotHealthy; } /** @@ -208,118 +208,118 @@ export class BatchProcess { * new task. */ get ready(): boolean { - return this.whyNotReady == null + return this.whyNotReady == null; } get idleMs(): number { - return this.idle ? Date.now() - this.#lastJobFinshedAt : -1 + return this.idle ? Date.now() - this.#lastJobFinshedAt : -1; } /** * @return true if the child process is in the process table */ running(): boolean { - if (this.#exited) return false + if (this.#exited) return false; - const alive = pidExists(this.pid) + const alive = pidExists(this.pid); if (!alive) { - this.#exited = true + this.#exited = true; // once a PID leaves the process table, it's gone for good. - void this.end(false, "proc.exit") + void this.end(false, "proc.exit"); } - return alive + return alive; } notRunning(): boolean { - return !this.running() + return !this.running(); } maybeRunHealthcheck(): Task | undefined { - return this.#healthMonitor.maybeRunHealthcheck(this) + return this.#healthMonitor.maybeRunHealthcheck(this); } // This must not be async, or new instances aren't started as busy (until the // startup task is complete) execTask(task: Task): boolean { - return this.ready ? this.#execTask(task) : false + return this.ready ? this.#execTask(task) : false; } #execTask(task: Task): boolean { - if (this.ending) return false + if (this.ending) return false; - this.#taskCount++ - this.#currentTask = task as Task - const cmd = ensureSuffix(task.command, "\n") - const isStartupTask = task.taskId === this.startupTaskId + this.#taskCount++; + this.#currentTask = task as Task; + const cmd = ensureSuffix(task.command, "\n"); + const isStartupTask = task.taskId === this.startupTaskId; const taskTimeoutMs = isStartupTask ? this.opts.spawnTimeoutMillis - : this.opts.taskTimeoutMillis + : this.opts.taskTimeoutMillis; if (taskTimeoutMs > 0) { // add the stream flush millis to the taskTimeoutMs, because that time // should not be counted against the task. this.#currentTaskTimeout = timers.setTimeout( () => this.#onTimeout(task as Task, taskTimeoutMs), taskTimeoutMs + this.opts.streamFlushMillis, - ) + ); } // CAREFUL! If you add a .catch or .finally, the pipeline can emit unhandled // rejections: void task.promise.then( () => { - this.#clearCurrentTask(task as Task) + this.#clearCurrentTask(task as Task); // this.#logger().trace("task completed", { task }) if (isStartupTask) { // no need to emit taskResolved for startup tasks. - this.#starting = false + this.#starting = false; } else { - this.opts.observer.emit("taskResolved", task as Task, this) + this.opts.observer.emit("taskResolved", task as Task, this); } // Call _after_ we've cleared the current task: - this.onIdle() + this.onIdle(); }, (error) => { - this.#clearCurrentTask(task as Task) + this.#clearCurrentTask(task as Task); // this.#logger().trace("task failed", { task, err: error }) if (isStartupTask) { this.opts.observer.emit( "startError", error instanceof Error ? error : new Error(String(error)), - ) - void this.end(false, "startError") + ); + void this.end(false, "startError"); } else { this.opts.observer.emit( "taskError", error instanceof Error ? error : new Error(String(error)), task as Task, this, - ) + ); } // Call _after_ we've cleared the current task: - this.onIdle() + this.onIdle(); }, - ) + ); try { - task.onStart(this.opts) - const stdin = this.proc?.stdin + task.onStart(this.opts); + const stdin = this.proc?.stdin; if (stdin == null || stdin.destroyed) { - task.reject(new Error("proc.stdin unexpectedly closed")) - return false + task.reject(new Error("proc.stdin unexpectedly closed")); + return false; } else { stdin.write(cmd, (err) => { if (err != null) { - task.reject(err) + task.reject(err); } - }) - return true + }); + return true; } } catch { // child process went away. We should too. - void this.end(false, "stdin.error") - return false + void this.end(false, "stdin.error"); + return false; } } @@ -337,14 +337,14 @@ export class BatchProcess { end(gracefully = true, reason: WhyNotHealthy): Promise { return (this.#endPromise ??= new Deferred().observe( this.#end(gracefully, (this.#whyNotHealthy ??= reason)), - )).promise + )).promise; } // NOTE: Must only be invoked by this.end(), and only expected to be invoked // once per instance. async #end(gracefully: boolean, reason: WhyNotHealthy) { - const lastTask = this.#currentTask - this.#clearCurrentTask() + const lastTask = this.#currentTask; + this.#clearCurrentTask(); await this.#terminator.terminate( this.proc, @@ -354,79 +354,79 @@ export class BatchProcess { gracefully, this.#exited, () => this.running(), - ) + ); // Clean up health monitoring for this process - this.#healthMonitor.cleanupProcess(this.pid) + this.#healthMonitor.cleanupProcess(this.pid); - this.opts.observer.emit("childEnd", this, reason) + this.opts.observer.emit("childEnd", this, reason); } #onTimeout(task: Task, timeoutMs: number): void { if (task.pending) { - this.opts.observer.emit("taskTimeout", timeoutMs, task, this) - this.#onError("timeout", new Error("waited " + timeoutMs + "ms"), task) + this.opts.observer.emit("taskTimeout", timeoutMs, task, this); + this.#onError("timeout", new Error("waited " + timeoutMs + "ms"), task); } } #onError(reason: WhyNotHealthy, error: Error, task?: Task) { if (task == null) { - task = this.#currentTask + task = this.#currentTask; } - const cleanedError = new Error(reason + ": " + cleanError(error.message)) + const cleanedError = new Error(reason + ": " + cleanError(error.message)); if (error.stack != null) { // Error stacks, if set, will not be redefined from a rethrow: - cleanedError.stack = cleanError(error.stack) + cleanedError.stack = cleanError(error.stack); } this.#logger().warn(this.name + ".onError()", { reason, task: map(task, (t) => t.command), error: cleanedError, - }) + }); if (this.ending) { // .#end is already disconnecting the error listeners, but in any event, // we don't really care about errors after we've been told to shut down. - return + return; } // clear the task before ending so the onExit from end() doesn't retry the task: - this.#clearCurrentTask() - void this.end(false, reason) + this.#clearCurrentTask(); + void this.end(false, reason); if (task != null && this.taskCount === 1) { this.#logger().warn( this.name + ".onError(): startup task failed: " + String(cleanedError), - ) - this.opts.observer.emit("startError", cleanedError) + ); + this.opts.observer.emit("startError", cleanedError); } if (task != null) { if (task.pending) { - task.reject(cleanedError) + task.reject(cleanedError); } else { this.opts.observer.emit( "internalError", new Error( `${this.name}.onError(${cleanedError}) cannot reject already-fulfilled task.`, ), - ) + ); } } } #clearCurrentTask(task?: Task) { - const taskFailed = task?.state === "rejected" + const taskFailed = task?.state === "rejected"; if (taskFailed) { - this.#healthMonitor.recordJobFailure(this.pid) + this.#healthMonitor.recordJobFailure(this.pid); } else if (task != null) { - this.#healthMonitor.recordJobSuccess(this.pid) + this.#healthMonitor.recordJobSuccess(this.pid); } - if (task != null && task.taskId !== this.#currentTask?.taskId) return - map(this.#currentTaskTimeout, (ea) => clearTimeout(ea)) - this.#currentTaskTimeout = undefined - this.#currentTask = undefined - this.#lastJobFinshedAt = Date.now() + if (task != null && task.taskId !== this.#currentTask?.taskId) return; + map(this.#currentTaskTimeout, (ea) => clearTimeout(ea)); + this.#currentTaskTimeout = undefined; + this.#currentTask = undefined; + this.#lastJobFinshedAt = Date.now(); } } diff --git a/src/BatchProcessOptions.ts b/src/BatchProcessOptions.ts index 220eca8..fcc021e 100644 --- a/src/BatchProcessOptions.ts +++ b/src/BatchProcessOptions.ts @@ -10,7 +10,7 @@ export interface BatchProcessOptions { * be invoked immediately after spawn. This command must return before any * tasks will be given to a given process. */ - versionCommand: string + versionCommand: string; /** * If provided, and healthCheckIntervalMillis is greater than 0, or the @@ -19,19 +19,19 @@ export interface BatchProcessOptions { * If the command outputs to stderr or returns a fail string, the process will * be considered unhealthy and recycled. */ - healthCheckCommand?: string | undefined + healthCheckCommand?: string | undefined; /** * Expected text to print if a command passes. Cannot be blank. Strings will * be interpreted as a regular expression fragment. */ - pass: string | RegExp + pass: string | RegExp; /** * Expected text to print if a command fails. Cannot be blank. Strings will * be interpreted as a regular expression fragment. */ - fail: string | RegExp + fail: string | RegExp; /** * Command to end the child batch process. If not provided (or undefined), @@ -39,5 +39,5 @@ export interface BatchProcessOptions { * and if it does not shut down within `endGracefulWaitTimeMillis`, it will be * SIGHUP'ed. */ - exitCommand?: string | undefined + exitCommand?: string | undefined; } diff --git a/src/ChildProcessFactory.ts b/src/ChildProcessFactory.ts index fa03034..fdb1ada 100644 --- a/src/ChildProcessFactory.ts +++ b/src/ChildProcessFactory.ts @@ -1,4 +1,4 @@ -import child_process from "node:child_process" +import child_process from "node:child_process"; /** * These are required parameters for a given BatchCluster. @@ -16,5 +16,5 @@ export interface ChildProcessFactory { */ readonly processFactory: () => | child_process.ChildProcess - | Promise + | Promise; } diff --git a/src/CombinedBatchProcessOptions.ts b/src/CombinedBatchProcessOptions.ts index 9321e5d..e3cd3e8 100644 --- a/src/CombinedBatchProcessOptions.ts +++ b/src/CombinedBatchProcessOptions.ts @@ -1,8 +1,8 @@ -import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" -import { ChildProcessFactory } from "./ChildProcessFactory" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"; +import { ChildProcessFactory } from "./ChildProcessFactory"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; export type CombinedBatchProcessOptions = BatchClusterOptions & InternalBatchProcessOptions & ChildProcessFactory & - WithObserver + WithObserver; diff --git a/src/DefaultTestOptions.spec.ts b/src/DefaultTestOptions.spec.ts index 4fe6eab..63a51e2 100644 --- a/src/DefaultTestOptions.spec.ts +++ b/src/DefaultTestOptions.spec.ts @@ -1,6 +1,6 @@ -import { BatchClusterOptions } from "./BatchClusterOptions" +import { BatchClusterOptions } from "./BatchClusterOptions"; -const bco = new BatchClusterOptions() +const bco = new BatchClusterOptions(); export const DefaultTestOptions = { ...bco, @@ -18,4 +18,4 @@ export const DefaultTestOptions = { // we shouldn't need these overrides... // ...(isCI ? { streamFlushMillis: bco.streamFlushMillis * 3 } : {}), // onIdleIntervalMillis: 1000, -} +}; diff --git a/src/Deferred.spec.ts b/src/Deferred.spec.ts index 39347ef..5c84f4b 100644 --- a/src/Deferred.spec.ts +++ b/src/Deferred.spec.ts @@ -1,67 +1,67 @@ -import { Deferred } from "./Deferred" -import { expect } from "./_chai.spec" +import { Deferred } from "./Deferred"; +import { expect } from "./_chai.spec"; describe("Deferred", () => { it("is born pending", () => { - const d = new Deferred() - expect(d.pending).to.eql(true) - expect(d.fulfilled).to.eql(false) - expect(d.rejected).to.eql(false) - }) + const d = new Deferred(); + expect(d.pending).to.eql(true); + expect(d.fulfilled).to.eql(false); + expect(d.rejected).to.eql(false); + }); it("resolves out of pending", () => { - const d = new Deferred() - const expected = "result" - d.resolve(expected) - expect(d.pending).to.eql(false) - expect(d.fulfilled).to.eql(true) - expect(d.rejected).to.eql(false) - return expect(d).to.become(expected) - }) + const d = new Deferred(); + const expected = "result"; + d.resolve(expected); + expect(d.pending).to.eql(false); + expect(d.fulfilled).to.eql(true); + expect(d.rejected).to.eql(false); + return expect(d).to.become(expected); + }); it("rejects out of pending", () => { - const d = new Deferred() - expect(d.reject("boom")).to.eql(true) - expect(d.pending).to.eql(false) - expect(d.fulfilled).to.eql(false) - expect(d.rejected).to.eql(true) - return expect(d).to.eventually.be.rejectedWith(/boom/) - }) + const d = new Deferred(); + expect(d.reject("boom")).to.eql(true); + expect(d.pending).to.eql(false); + expect(d.fulfilled).to.eql(false); + expect(d.rejected).to.eql(true); + return expect(d).to.eventually.be.rejectedWith(/boom/); + }); it("resolved ignores subsequent resolutions", () => { - const d = new Deferred() - expect(d.resolve(123)).to.eql(true) - expect(d.resolve(456)).to.eql(false) - expect(d.pending).to.eql(false) - expect(d.fulfilled).to.eql(true) - expect(d.rejected).to.eql(false) - return expect(d).to.become(123) - }) + const d = new Deferred(); + expect(d.resolve(123)).to.eql(true); + expect(d.resolve(456)).to.eql(false); + expect(d.pending).to.eql(false); + expect(d.fulfilled).to.eql(true); + expect(d.rejected).to.eql(false); + return expect(d).to.become(123); + }); it("resolved respects subsequent rejections", () => { - const d = new Deferred() - expect(d.resolve(123)).to.eql(true) - expect(d.reject("boom")).to.eql(false) - expect(d.pending).to.eql(false) + const d = new Deferred(); + expect(d.resolve(123)).to.eql(true); + expect(d.reject("boom")).to.eql(false); + expect(d.pending).to.eql(false); // CAUTION: THIS IS WEIRD. The promise is resolved, but something later // wanted to reject, so we assume the rejected state, even though we can't // reach back in the promise chain and un-resolve the promise. - expect(d.fulfilled).to.eql(false) - expect(d.rejected).to.eql(true) - return expect(d).to.become(123) - }) + expect(d.fulfilled).to.eql(false); + expect(d.rejected).to.eql(true); + return expect(d).to.become(123); + }); it("rejected ignores subsequent resolutions", () => { - const d = new Deferred() - expect(d.reject("first boom")).to.eql(true) - expect(d.resolve(456)).to.eql(false) - return expect(d).to.eventually.be.rejectedWith(/first boom/) - }) + const d = new Deferred(); + expect(d.reject("first boom")).to.eql(true); + expect(d.resolve(456)).to.eql(false); + return expect(d).to.eventually.be.rejectedWith(/first boom/); + }); it("rejected ignores subsequent rejections", () => { - const d = new Deferred() - expect(d.reject("first boom")).to.eql(true) - expect(d.reject("second boom")).to.eql(false) - return expect(d).to.eventually.be.rejectedWith(/first boom/) - }) -}) + const d = new Deferred(); + expect(d.reject("first boom")).to.eql(true); + expect(d.reject("second boom")).to.eql(false); + return expect(d).to.eventually.be.rejectedWith(/first boom/); + }); +}); diff --git a/src/Deferred.ts b/src/Deferred.ts index 796959f..f62e9f6 100644 --- a/src/Deferred.ts +++ b/src/Deferred.ts @@ -13,107 +13,107 @@ enum State { * `fulfilled`, or `rejected` state of the promise. */ export class Deferred implements PromiseLike { - readonly [Symbol.toStringTag] = "Deferred" - readonly promise: Promise - #resolve!: (value: T | PromiseLike) => void - #reject!: (reason?: unknown) => void - #state: State = State.pending + readonly [Symbol.toStringTag] = "Deferred"; + readonly promise: Promise; + #resolve!: (value: T | PromiseLike) => void; + #reject!: (reason?: unknown) => void; + #state: State = State.pending; constructor() { this.promise = new Promise((resolve, reject) => { - this.#resolve = resolve - this.#reject = reject - }) + this.#resolve = resolve; + this.#reject = reject; + }); } /** * @return `true` iff neither `resolve` nor `rejected` have been invoked */ get pending(): boolean { - return this.#state === State.pending + return this.#state === State.pending; } /** * @return `true` iff `resolve` has been invoked */ get fulfilled(): boolean { - return this.#state === State.fulfilled + return this.#state === State.fulfilled; } /** * @return `true` iff `rejected` has been invoked */ get rejected(): boolean { - return this.#state === State.rejected + return this.#state === State.rejected; } /** * @return `true` iff `resolve` or `rejected` have been invoked */ get settled(): boolean { - return this.#state !== State.pending + return this.#state !== State.pending; } then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ): Promise { - return this.promise.then(onfulfilled, onrejected) + return this.promise.then(onfulfilled, onrejected); } catch( onrejected?: ((reason: unknown) => TResult | PromiseLike) | null, ): Promise { - return this.promise.catch(onrejected) + return this.promise.catch(onrejected); } resolve(value: T): boolean { if (this.settled) { - return false + return false; } else { - this.#state = State.fulfilled - this.#resolve(value) - return true + this.#state = State.fulfilled; + this.#resolve(value); + return true; } } reject(reason?: Error | string): boolean { - const wasSettled = this.settled + const wasSettled = this.settled; // This isn't great: the wrapped Promise may be in a different state than // #state: but the caller wanted to reject, so even if it already was // resolved, let's try to respect that. - this.#state = State.rejected + this.#state = State.rejected; if (wasSettled) { - return false + return false; } else { - this.#reject(reason) - return true + this.#reject(reason); + return true; } } observe(p: Promise): this { - void observe(this, p) - return this + void observe(this, p); + return this; } observeQuietly(p: Promise): Deferred { - void observeQuietly(this, p) - return this as Deferred + void observeQuietly(this, p); + return this as Deferred; } } async function observe(d: Deferred, p: Promise) { try { - d.resolve(await p) + d.resolve(await p); } catch (err: unknown) { - d.reject(err instanceof Error ? err : new Error(String(err))) + d.reject(err instanceof Error ? err : new Error(String(err))); } } async function observeQuietly(d: Deferred, p: Promise) { try { - d.resolve(await p) + d.resolve(await p); } catch { - d.resolve(undefined as T) + d.resolve(undefined as T); } } diff --git a/src/Error.ts b/src/Error.ts index 853753d..2af1c89 100644 --- a/src/Error.ts +++ b/src/Error.ts @@ -1,4 +1,4 @@ -import { toNotBlank } from "./String" +import { toNotBlank } from "./String"; /** * When we wrap errors, an Error always prefixes the toString() and stack with @@ -7,7 +7,7 @@ import { toNotBlank } from "./String" export function tryEach(arr: (() => void)[]): void { for (const f of arr) { try { - f() + f(); } catch { // } @@ -17,7 +17,7 @@ export function tryEach(arr: (() => void)[]): void { export function cleanError(s: unknown): string { return String(s) .trim() - .replace(/^error: /i, "") + .replace(/^error: /i, ""); } export function asError(err: unknown): Error { @@ -31,5 +31,5 @@ export function asError(err: unknown): Error { ) ?? toNotBlank(err) ?? "(unknown)", - ) + ); } diff --git a/src/HealthCheckStrategy.ts b/src/HealthCheckStrategy.ts index eeb0b28..8360e53 100644 --- a/src/HealthCheckStrategy.ts +++ b/src/HealthCheckStrategy.ts @@ -1,6 +1,6 @@ -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { HealthCheckable } from "./ProcessHealthMonitor" -import { WhyNotHealthy } from "./WhyNotHealthy" +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { HealthCheckable } from "./ProcessHealthMonitor"; +import { WhyNotHealthy } from "./WhyNotHealthy"; /** * Strategy interface for different health check approaches @@ -9,7 +9,7 @@ export interface HealthCheckStrategy { assess( process: HealthCheckable, options: InternalBatchProcessOptions, - ): WhyNotHealthy | null + ): WhyNotHealthy | null; } /** @@ -18,11 +18,11 @@ export interface HealthCheckStrategy { export class LifecycleHealthCheck implements HealthCheckStrategy { assess(process: HealthCheckable): WhyNotHealthy | null { if (process.ended) { - return "ended" + return "ended"; } else if (process.ending) { - return "ending" + return "ending"; } - return null + return null; } } @@ -32,9 +32,9 @@ export class LifecycleHealthCheck implements HealthCheckStrategy { export class StreamHealthCheck implements HealthCheckStrategy { assess(process: HealthCheckable): WhyNotHealthy | null { if (process.proc.stdin == null || process.proc.stdin.destroyed) { - return "closed" + return "closed"; } - return null + return null; } } @@ -50,9 +50,9 @@ export class TaskLimitHealthCheck implements HealthCheckStrategy { options.maxTasksPerProcess > 0 && process.taskCount >= options.maxTasksPerProcess ) { - return "worn" + return "worn"; } - return null + return null; } } @@ -68,9 +68,9 @@ export class IdleTimeHealthCheck implements HealthCheckStrategy { options.maxIdleMsPerProcess > 0 && process.idleMs > options.maxIdleMsPerProcess ) { - return "idle" + return "idle"; } - return null + return null; } } @@ -86,9 +86,9 @@ export class FailureCountHealthCheck implements HealthCheckStrategy { options.maxFailedTasksPerProcess > 0 && process.failedTaskCount >= options.maxFailedTasksPerProcess ) { - return "broken" + return "broken"; } - return null + return null; } } @@ -104,9 +104,9 @@ export class AgeHealthCheck implements HealthCheckStrategy { options.maxProcAgeMillis > 0 && process.start + options.maxProcAgeMillis < Date.now() ) { - return "old" + return "old"; } - return null + return null; } } @@ -122,9 +122,9 @@ export class TaskTimeoutHealthCheck implements HealthCheckStrategy { options.taskTimeoutMillis > 0 && (process.currentTask?.runtimeMs ?? 0) > options.taskTimeoutMillis ) { - return "timeout" + return "timeout"; } - return null + return null; } } @@ -140,18 +140,18 @@ export class CompositeHealthCheckStrategy implements HealthCheckStrategy { new FailureCountHealthCheck(), new AgeHealthCheck(), new TaskTimeoutHealthCheck(), - ] + ]; assess( process: HealthCheckable, options: InternalBatchProcessOptions, ): WhyNotHealthy | null { for (const strategy of this.strategies) { - const result = strategy.assess(process, options) + const result = strategy.assess(process, options); if (result != null) { - return result + return result; } } - return null + return null; } } diff --git a/src/InternalBatchProcessOptions.ts b/src/InternalBatchProcessOptions.ts index 284e34c..deaf07d 100644 --- a/src/InternalBatchProcessOptions.ts +++ b/src/InternalBatchProcessOptions.ts @@ -1,10 +1,10 @@ -import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" -import { BatchProcessOptions } from "./BatchProcessOptions" +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"; +import { BatchProcessOptions } from "./BatchProcessOptions"; export interface InternalBatchProcessOptions extends BatchProcessOptions, BatchClusterOptions, WithObserver { - passRE: RegExp - failRE: RegExp + passRE: RegExp; + failRE: RegExp; } diff --git a/src/Logger.ts b/src/Logger.ts index 98bf2ef..ea72f7f 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,21 +1,21 @@ -import util from "node:util" -import { map } from "./Object" -import { notBlank } from "./String" +import util from "node:util"; +import { map } from "./Object"; +import { notBlank } from "./String"; export type LoggerFunction = ( message: string, ...optionalParams: unknown[] -) => void +) => void; /** * Simple interface for logging. */ export interface Logger { - trace: LoggerFunction - debug: LoggerFunction - info: LoggerFunction - warn: LoggerFunction - error: LoggerFunction + trace: LoggerFunction; + debug: LoggerFunction; + info: LoggerFunction; + warn: LoggerFunction; + error: LoggerFunction; } export const LogLevels: (keyof Logger)[] = [ @@ -24,11 +24,11 @@ export const LogLevels: (keyof Logger)[] = [ "info", "warn", "error", -] +]; -const _debuglog = util.debuglog("batch-cluster") +const _debuglog = util.debuglog("batch-cluster"); -const noop = () => undefined +const noop = () => undefined; /** * Default `Logger` implementation. @@ -61,16 +61,16 @@ export const ConsoleLogger: Logger = Object.freeze({ */ warn: (...args: unknown[]) => { // eslint-disable-next-line no-console - console.warn(...args) + console.warn(...args); }, /** * Delegates to `console.error` */ error: (...args: unknown[]) => { // eslint-disable-next-line no-console - console.error(...args) + console.error(...args); }, -}) +}); /** * `Logger` that disables all logging. @@ -81,37 +81,37 @@ export const NoLogger: Logger = Object.freeze({ info: noop, warn: noop, error: noop, -}) +}); -let _logger: Logger = _debuglog.enabled ? ConsoleLogger : NoLogger +let _logger: Logger = _debuglog.enabled ? ConsoleLogger : NoLogger; export function setLogger(l: Logger): void { if (LogLevels.some((ea) => typeof l[ea] !== "function")) { - throw new Error("invalid logger, must implement " + LogLevels.join(", ")) + throw new Error("invalid logger, must implement " + LogLevels.join(", ")); } - _logger = l + _logger = l; } export function logger(): Logger { - return _logger + return _logger; } export const Log = { withLevels: (delegate: Logger): Logger => { - const timestamped: Logger = {} as Logger + const timestamped: Logger = {} as Logger; LogLevels.forEach((ea) => { - const prefix = (ea + " ").substring(0, 5) + " | " + const prefix = (ea + " ").substring(0, 5) + " | "; timestamped[ea] = (message?: unknown, ...optionalParams: unknown[]) => { if (notBlank(String(message))) { - delegate[ea](prefix + String(message), ...optionalParams) + delegate[ea](prefix + String(message), ...optionalParams); } - } - }) - return timestamped + }; + }); + return timestamped; }, withTimestamps: (delegate: Logger) => { - const timestamped: Logger = {} as Logger + const timestamped: Logger = {} as Logger; LogLevels.forEach( (level) => (timestamped[level] = ( @@ -124,17 +124,17 @@ export const Log = { ...optionalParams, ), )), - ) - return timestamped + ); + return timestamped; }, filterLevels: (l: Logger, minLogLevel: keyof Logger) => { - const minLogLevelIndex = LogLevels.indexOf(minLogLevel) - const filtered: Logger = {} as Logger + const minLogLevelIndex = LogLevels.indexOf(minLogLevel); + const filtered: Logger = {} as Logger; LogLevels.forEach( (ea, idx) => (filtered[ea] = idx < minLogLevelIndex ? noop : l[ea].bind(l)), - ) - return filtered + ); + return filtered; }, -} +}; diff --git a/src/Mean.ts b/src/Mean.ts index fe1f70b..0ee0204 100644 --- a/src/Mean.ts +++ b/src/Mean.ts @@ -1,39 +1,39 @@ export class Mean { - private _n: number - private _min?: number = undefined - private _max?: number = undefined + private _n: number; + private _min?: number = undefined; + private _max?: number = undefined; constructor( n = 0, private sum = 0, ) { - this._n = n + this._n = n; } push(x: number): void { - this._n++ - this.sum += x - this._min = this._min == null || this._min > x ? x : this._min - this._max = this._max == null || this._max < x ? x : this._max + this._n++; + this.sum += x; + this._min = this._min == null || this._min > x ? x : this._min; + this._max = this._max == null || this._max < x ? x : this._max; } get n(): number { - return this._n + return this._n; } get min(): number | undefined { - return this._min + return this._min; } get max(): number | undefined { - return this._max + return this._max; } get mean(): number { - return this.sum / this.n + return this.sum / this.n; } clone(): Mean { - return new Mean(this.n, this.sum) + return new Mean(this.n, this.sum); } } diff --git a/src/Mutex.ts b/src/Mutex.ts index accab3a..9085bdc 100644 --- a/src/Mutex.ts +++ b/src/Mutex.ts @@ -1,35 +1,35 @@ -import { filterInPlace } from "./Array" -import { Deferred } from "./Deferred" +import { filterInPlace } from "./Array"; +import { Deferred } from "./Deferred"; /** * Aggregate promises efficiently */ export class Mutex { - private _pushCount = 0 - private readonly _arr: Deferred[] = [] + private _pushCount = 0; + private readonly _arr: Deferred[] = []; private get arr() { - filterInPlace(this._arr, (ea) => ea.pending) - return this._arr + filterInPlace(this._arr, (ea) => ea.pending); + return this._arr; } get pushCount(): number { - return this._pushCount + return this._pushCount; } push(f: () => Promise): Promise { - this._pushCount++ - const p = f() + this._pushCount++; + const p = f(); // Don't cause awaitAll to die if a task rejects: - this.arr.push(new Deferred().observeQuietly(p)) - return p + this.arr.push(new Deferred().observeQuietly(p)); + return p; } /** * Run f() after all prior-enqueued promises have resolved. */ serial(f: () => Promise): Promise { - return this.push(() => this.awaitAll().then(() => f())) + return this.push(() => this.awaitAll().then(() => f())); } /** @@ -37,21 +37,21 @@ export class Mutex { * all pending have resolved. */ runIfIdle(f: () => Promise): undefined | Promise { - return this.pending ? undefined : this.serial(f) + return this.pending ? undefined : this.serial(f); } get pendingCount(): number { // Don't need vacuuming, so we can use this._arr: - return this._arr.reduce((sum, ea) => sum + (ea.pending ? 1 : 0), 0) + return this._arr.reduce((sum, ea) => sum + (ea.pending ? 1 : 0), 0); } get pending(): boolean { - return this.pendingCount > 0 + return this.pendingCount > 0; } get settled(): boolean { // this.arr is a getter that does vacuuming - return this.arr.length === 0 + return this.arr.length === 0; } /** @@ -61,6 +61,6 @@ export class Mutex { awaitAll(): Promise { return this.arr.length === 0 ? Promise.resolve(undefined) - : Promise.all(this.arr.map((ea) => ea.promise)).then(() => undefined) + : Promise.all(this.arr.map((ea) => ea.promise)).then(() => undefined); } } diff --git a/src/Object.spec.ts b/src/Object.spec.ts index ea4446b..2cc614f 100644 --- a/src/Object.spec.ts +++ b/src/Object.spec.ts @@ -1,24 +1,24 @@ -import { map } from "./Object" -import { expect } from "./_chai.spec" +import { map } from "./Object"; +import { expect } from "./_chai.spec"; describe("Object", () => { describe("map()", () => { it("skips if target is null", () => { expect( map(null, () => { - throw new Error("unexpected") + throw new Error("unexpected"); }), - ).to.eql(undefined) - }) + ).to.eql(undefined); + }); it("skips if target is undefined", () => { expect( map(undefined, () => { - throw new Error("unexpected") + throw new Error("unexpected"); }), - ).to.eql(undefined) - }) + ).to.eql(undefined); + }); it("passes defined target to f", () => { - expect(map(123, (ea) => String(ea))).to.eql("123") - }) - }) -}) + expect(map(123, (ea) => String(ea))).to.eql("123"); + }); + }); +}); diff --git a/src/Object.ts b/src/Object.ts index 7bb4839..6067571 100644 --- a/src/Object.ts +++ b/src/Object.ts @@ -6,32 +6,32 @@ export function map( obj: T | undefined | null, f: (t: T) => R, ): R | undefined { - return obj != null ? f(obj) : undefined + return obj != null ? f(obj) : undefined; } export function isFunction(obj: unknown): obj is () => unknown { - return typeof obj === "function" + return typeof obj === "function"; } export function fromEntries( arr: [string | undefined, unknown][], ): Record { - const o: Record = {} + const o: Record = {}; for (const [key, value] of arr) { if (key != null) { - o[key] = value + o[key] = value; } } - return o + return o; } export function omit, S extends keyof T>( t: T, ...keysToOmit: S[] ): Omit { - const result = { ...t } + const result = { ...t }; for (const ea of keysToOmit) { - delete result[ea] + delete result[ea]; } - return result + return result; } diff --git a/src/OptionsVerifier.ts b/src/OptionsVerifier.ts index bd776fd..946caaf 100644 --- a/src/OptionsVerifier.ts +++ b/src/OptionsVerifier.ts @@ -1,8 +1,8 @@ -import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions" -import { BatchProcessOptions } from "./BatchProcessOptions" -import { ChildProcessFactory } from "./ChildProcessFactory" -import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" -import { blank, toS } from "./String" +import { BatchClusterOptions, WithObserver } from "./BatchClusterOptions"; +import { BatchProcessOptions } from "./BatchProcessOptions"; +import { ChildProcessFactory } from "./ChildProcessFactory"; +import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"; +import { blank, toS } from "./String"; /** * Verifies and sanitizes the provided options for BatchCluster. @@ -26,14 +26,14 @@ export function verifyOptions( ...opts, passRE: toRe(opts.pass), failRE: toRe(opts.fail), - } as CombinedBatchProcessOptions + } as CombinedBatchProcessOptions; - const errors: string[] = [] + const errors: string[] = []; function notBlank(fieldName: keyof CombinedBatchProcessOptions) { - const v = toS(result[fieldName]) + const v = toS(result[fieldName]); if (blank(v)) { - errors.push(fieldName + " must not be blank") + errors.push(fieldName + " must not be blank"); } } @@ -42,20 +42,20 @@ export function verifyOptions( value: number, why?: string, ) { - const v = result[fieldName] as number + const v = result[fieldName] as number; if (v < value) { - const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}` - errors.push(msg) + const msg = `${fieldName} must be greater than or equal to ${value}${blank(why) ? "" : ": " + why}`; + errors.push(msg); } } - notBlank("versionCommand") - notBlank("pass") - notBlank("fail") + notBlank("versionCommand"); + notBlank("pass"); + notBlank("fail"); - gte("maxTasksPerProcess", 1) + gte("maxTasksPerProcess", 1); - gte("maxProcs", 1) + gte("maxProcs", 1); if ( opts.maxProcAgeMillis != null && @@ -66,25 +66,25 @@ export function verifyOptions( "maxProcAgeMillis", Math.max(result.spawnTimeoutMillis, result.taskTimeoutMillis), `the max value of spawnTimeoutMillis (${result.spawnTimeoutMillis}) and taskTimeoutMillis (${result.taskTimeoutMillis})`, - ) + ); } // 0 disables: - gte("minDelayBetweenSpawnMillis", 0) - gte("onIdleIntervalMillis", 0) - gte("endGracefulWaitTimeMillis", 0) - gte("maxReasonableProcessFailuresPerMinute", 0) - gte("streamFlushMillis", 0) + gte("minDelayBetweenSpawnMillis", 0); + gte("onIdleIntervalMillis", 0); + gte("endGracefulWaitTimeMillis", 0); + gte("maxReasonableProcessFailuresPerMinute", 0); + gte("streamFlushMillis", 0); if (errors.length > 0) { throw new Error( "BatchCluster was given invalid options: " + errors.join("; "), - ) + ); } - return result + return result; } function escapeRegExp(s: string) { - return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&") + return toS(s).replace(/[-.,\\^$*+?()|[\]{}]/g, "\\$&"); } function toRe(s: string | RegExp) { return s instanceof RegExp @@ -98,5 +98,5 @@ function toRe(s: string | RegExp) { .map((ea) => escapeRegExp(ea)) .join(".*"), ) - : new RegExp(escapeRegExp(s)) + : new RegExp(escapeRegExp(s)); } diff --git a/src/Parser.ts b/src/Parser.ts index 8c2b011..b9940ce 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,4 +1,4 @@ -import { notBlank } from "./String" +import { notBlank } from "./String"; /** * Parser implementations convert stdout and stderr from the underlying child @@ -24,14 +24,14 @@ export type Parser = ( stdout: string, stderr: string | undefined, passed: boolean, -) => T | Promise +) => T | Promise; export const SimpleParser: Parser = ( stdout: string, stderr: string | undefined, passed: boolean, ) => { - if (!passed) throw new Error("task failed") - if (notBlank(stderr)) throw new Error(stderr) - return stdout -} + if (!passed) throw new Error("task failed"); + if (notBlank(stderr)) throw new Error(stderr); + return stdout; +}; diff --git a/src/Pids.ts b/src/Pids.ts index 9f9903a..65339c5 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -4,19 +4,19 @@ * table. The PID may be paused or a zombie, though. */ export function pidExists(pid: number | undefined): boolean { - if (pid == null || !isFinite(pid) || pid <= 0) return false + if (pid == null || !isFinite(pid) || pid <= 0) return false; try { // signal 0 can be used to test for the existence of a process // see https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processkillpid-signal - return process.kill(pid, 0) + return process.kill(pid, 0); } catch (err: unknown) { // We're expecting err.code to be either "EPERM" (if we don't have // permission to send `pid` and message), or "ESRCH" if that pid doesn't // exist. EPERM means it _does_ exist! - if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true + if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true; // failed to get priority--assume the pid is gone. - return false + return false; } } @@ -28,12 +28,12 @@ export function pidExists(pid: number | undefined): boolean { * permissions to send the signal, the pid will be forced to shut down. Defaults to false. */ export function kill(pid: number | undefined, force = false): boolean { - if (pid == null || !isFinite(pid) || pid <= 0) return false + if (pid == null || !isFinite(pid) || pid <= 0) return false; try { - return process.kill(pid, force ? "SIGKILL" : undefined) + return process.kill(pid, force ? "SIGKILL" : undefined); } catch (err) { - if (!String(err).includes("ESRCH")) throw err - return false + if (!String(err).includes("ESRCH")) throw err; + return false; // failed to get priority--assume the pid is gone. } } diff --git a/src/Platform.ts b/src/Platform.ts index 77e18da..74a6cf2 100644 --- a/src/Platform.ts +++ b/src/Platform.ts @@ -1,7 +1,7 @@ -import os from "node:os" +import os from "node:os"; -const _platform = os.platform() +const _platform = os.platform(); -export const isWin = ["win32", "cygwin"].includes(_platform) -export const isMac = _platform === "darwin" -export const isLinux = _platform === "linux" +export const isWin = ["win32", "cygwin"].includes(_platform); +export const isMac = _platform === "darwin"; +export const isLinux = _platform === "linux"; diff --git a/src/ProcessHealthMonitor.spec.ts b/src/ProcessHealthMonitor.spec.ts index 74b40b5..8a2ed75 100644 --- a/src/ProcessHealthMonitor.spec.ts +++ b/src/ProcessHealthMonitor.spec.ts @@ -1,18 +1,18 @@ -import events from "node:events" -import { expect, processFactory } from "./_chai.spec" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { verifyOptions } from "./OptionsVerifier" -import { HealthCheckable, ProcessHealthMonitor } from "./ProcessHealthMonitor" -import { Task } from "./Task" +import events from "node:events"; +import { expect, processFactory } from "./_chai.spec"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { DefaultTestOptions } from "./DefaultTestOptions.spec"; +import { verifyOptions } from "./OptionsVerifier"; +import { HealthCheckable, ProcessHealthMonitor } from "./ProcessHealthMonitor"; +import { Task } from "./Task"; describe("ProcessHealthMonitor", function () { - let healthMonitor: ProcessHealthMonitor - let emitter: BatchClusterEmitter - let mockProcess: HealthCheckable + let healthMonitor: ProcessHealthMonitor; + let emitter: BatchClusterEmitter; + let mockProcess: HealthCheckable; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter + emitter = new events.EventEmitter() as BatchClusterEmitter; const options = verifyOptions({ ...DefaultTestOptions, @@ -25,9 +25,9 @@ describe("ProcessHealthMonitor", function () { maxFailedTasksPerProcess: 3, maxProcAgeMillis: 20000, // Must be > spawnTimeoutMillis taskTimeoutMillis: 1000, - }) + }); - healthMonitor = new ProcessHealthMonitor(options, emitter) + healthMonitor = new ProcessHealthMonitor(options, emitter); // Create a healthy mock process mockProcess = { @@ -41,225 +41,228 @@ describe("ProcessHealthMonitor", function () { ended: false, proc: { stdin: { destroyed: false } }, currentTask: null, - } - }) + }; + }); describe("process lifecycle", function () { it("should initialize process health monitoring", function () { - healthMonitor.initializeProcess(mockProcess.pid) + healthMonitor.initializeProcess(mockProcess.pid); - const state = healthMonitor.getProcessHealthState(mockProcess.pid) - expect(state).to.not.be.undefined - expect(state?.healthCheckFailures).to.eql(0) - expect(state?.lastJobFailed).to.be.false - }) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); + expect(state).to.not.be.undefined; + expect(state?.healthCheckFailures).to.eql(0); + expect(state?.lastJobFailed).to.be.false; + }); it("should cleanup process health monitoring", function () { - healthMonitor.initializeProcess(mockProcess.pid) + healthMonitor.initializeProcess(mockProcess.pid); expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.not.be - .undefined + .undefined; - healthMonitor.cleanupProcess(mockProcess.pid) + healthMonitor.cleanupProcess(mockProcess.pid); expect(healthMonitor.getProcessHealthState(mockProcess.pid)).to.be - .undefined - }) - }) + .undefined; + }); + }); describe("health assessment", function () { beforeEach(function () { - healthMonitor.initializeProcess(mockProcess.pid) - }) + healthMonitor.initializeProcess(mockProcess.pid); + }); it("should assess healthy process as healthy", function () { - const healthReason = healthMonitor.assessHealth(mockProcess) - expect(healthReason).to.be.null - expect(healthMonitor.isHealthy(mockProcess)).to.be.true - }) + const healthReason = healthMonitor.assessHealth(mockProcess); + expect(healthReason).to.be.null; + expect(healthMonitor.isHealthy(mockProcess)).to.be.true; + }); it("should detect ended process", function () { - const endedProcess = { ...mockProcess, ended: true } + const endedProcess = { ...mockProcess, ended: true }; - const healthReason = healthMonitor.assessHealth(endedProcess) - expect(healthReason).to.eql("ended") - expect(healthMonitor.isHealthy(endedProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(endedProcess); + expect(healthReason).to.eql("ended"); + expect(healthMonitor.isHealthy(endedProcess)).to.be.false; + }); it("should detect ending process", function () { - const endingProcess = { ...mockProcess, ending: true } + const endingProcess = { ...mockProcess, ending: true }; - const healthReason = healthMonitor.assessHealth(endingProcess) - expect(healthReason).to.eql("ending") - expect(healthMonitor.isHealthy(endingProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(endingProcess); + expect(healthReason).to.eql("ending"); + expect(healthMonitor.isHealthy(endingProcess)).to.be.false; + }); it("should detect closed stdin", function () { const closedProcess = { ...mockProcess, proc: { stdin: { destroyed: true } }, - } + }; - const healthReason = healthMonitor.assessHealth(closedProcess) - expect(healthReason).to.eql("closed") - expect(healthMonitor.isHealthy(closedProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(closedProcess); + expect(healthReason).to.eql("closed"); + expect(healthMonitor.isHealthy(closedProcess)).to.be.false; + }); it("should detect null stdin", function () { const nullStdinProcess = { ...mockProcess, proc: { stdin: null }, - } + }; - const healthReason = healthMonitor.assessHealth(nullStdinProcess) - expect(healthReason).to.eql("closed") - expect(healthMonitor.isHealthy(nullStdinProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(nullStdinProcess); + expect(healthReason).to.eql("closed"); + expect(healthMonitor.isHealthy(nullStdinProcess)).to.be.false; + }); it("should detect worn process (too many tasks)", function () { - const wornProcess = { ...mockProcess, taskCount: 5 } + const wornProcess = { ...mockProcess, taskCount: 5 }; - const healthReason = healthMonitor.assessHealth(wornProcess) - expect(healthReason).to.eql("worn") - expect(healthMonitor.isHealthy(wornProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(wornProcess); + expect(healthReason).to.eql("worn"); + expect(healthMonitor.isHealthy(wornProcess)).to.be.false; + }); it("should detect idle process (idle too long)", function () { - const idleProcess = { ...mockProcess, idleMs: 3000 } + const idleProcess = { ...mockProcess, idleMs: 3000 }; - const healthReason = healthMonitor.assessHealth(idleProcess) - expect(healthReason).to.eql("idle") - expect(healthMonitor.isHealthy(idleProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(idleProcess); + expect(healthReason).to.eql("idle"); + expect(healthMonitor.isHealthy(idleProcess)).to.be.false; + }); it("should detect broken process (too many failed tasks)", function () { - const brokenProcess = { ...mockProcess, failedTaskCount: 3 } + const brokenProcess = { ...mockProcess, failedTaskCount: 3 }; - const healthReason = healthMonitor.assessHealth(brokenProcess) - expect(healthReason).to.eql("broken") - expect(healthMonitor.isHealthy(brokenProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(brokenProcess); + expect(healthReason).to.eql("broken"); + expect(healthMonitor.isHealthy(brokenProcess)).to.be.false; + }); it("should detect old process", function () { const oldProcess = { ...mockProcess, start: Date.now() - 25000, // 25 seconds ago (older than maxProcAgeMillis) - } + }; - const healthReason = healthMonitor.assessHealth(oldProcess) - expect(healthReason).to.eql("old") - expect(healthMonitor.isHealthy(oldProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(oldProcess); + expect(healthReason).to.eql("old"); + expect(healthMonitor.isHealthy(oldProcess)).to.be.false; + }); it("should detect timed out task", function () { // Create a mock task that simulates a long runtime const mockTask = { runtimeMs: 1500, // longer than 1000ms timeout - } as Task + } as Task; const timedOutProcess = { ...mockProcess, currentTask: mockTask, - } + }; - const healthReason = healthMonitor.assessHealth(timedOutProcess) - expect(healthReason).to.eql("timeout") - expect(healthMonitor.isHealthy(timedOutProcess)).to.be.false - }) + const healthReason = healthMonitor.assessHealth(timedOutProcess); + expect(healthReason).to.eql("timeout"); + expect(healthMonitor.isHealthy(timedOutProcess)).to.be.false; + }); it("should respect override reason", function () { - const healthReason = healthMonitor.assessHealth(mockProcess, "startError") - expect(healthReason).to.eql("startError") - expect(healthMonitor.isHealthy(mockProcess, "startError")).to.be.false - }) + const healthReason = healthMonitor.assessHealth( + mockProcess, + "startError", + ); + expect(healthReason).to.eql("startError"); + expect(healthMonitor.isHealthy(mockProcess, "startError")).to.be.false; + }); it("should detect unhealthy process after health check failures", function () { // Simulate a health check failure - const state = healthMonitor.getProcessHealthState(mockProcess.pid) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.healthCheckFailures = 1 + state.healthCheckFailures = 1; } - const healthReason = healthMonitor.assessHealth(mockProcess) - expect(healthReason).to.eql("unhealthy") - expect(healthMonitor.isHealthy(mockProcess)).to.be.false - }) - }) + const healthReason = healthMonitor.assessHealth(mockProcess); + expect(healthReason).to.eql("unhealthy"); + expect(healthMonitor.isHealthy(mockProcess)).to.be.false; + }); + }); describe("readiness assessment", function () { beforeEach(function () { - healthMonitor.initializeProcess(mockProcess.pid) - }) + healthMonitor.initializeProcess(mockProcess.pid); + }); it("should assess idle healthy process as ready", function () { - const readinessReason = healthMonitor.assessReadiness(mockProcess) - expect(readinessReason).to.be.null - expect(healthMonitor.isReady(mockProcess)).to.be.true - }) + const readinessReason = healthMonitor.assessReadiness(mockProcess); + expect(readinessReason).to.be.null; + expect(healthMonitor.isReady(mockProcess)).to.be.true; + }); it("should detect busy process", function () { - const busyProcess = { ...mockProcess, idle: false } + const busyProcess = { ...mockProcess, idle: false }; - const readinessReason = healthMonitor.assessReadiness(busyProcess) - expect(readinessReason).to.eql("busy") - expect(healthMonitor.isReady(busyProcess)).to.be.false - }) + const readinessReason = healthMonitor.assessReadiness(busyProcess); + expect(readinessReason).to.eql("busy"); + expect(healthMonitor.isReady(busyProcess)).to.be.false; + }); it("should detect unhealthy idle process", function () { - const unhealthyProcess = { ...mockProcess, ended: true } + const unhealthyProcess = { ...mockProcess, ended: true }; - const readinessReason = healthMonitor.assessReadiness(unhealthyProcess) - expect(readinessReason).to.eql("ended") - expect(healthMonitor.isReady(unhealthyProcess)).to.be.false - }) - }) + const readinessReason = healthMonitor.assessReadiness(unhealthyProcess); + expect(readinessReason).to.eql("ended"); + expect(healthMonitor.isReady(unhealthyProcess)).to.be.false; + }); + }); describe("job state tracking", function () { beforeEach(function () { - healthMonitor.initializeProcess(mockProcess.pid) - }) + healthMonitor.initializeProcess(mockProcess.pid); + }); it("should record job failures", function () { - healthMonitor.recordJobFailure(mockProcess.pid) + healthMonitor.recordJobFailure(mockProcess.pid); - const state = healthMonitor.getProcessHealthState(mockProcess.pid) - expect(state?.lastJobFailed).to.be.true - }) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); + expect(state?.lastJobFailed).to.be.true; + }); it("should record job successes", function () { // First record a failure - healthMonitor.recordJobFailure(mockProcess.pid) + healthMonitor.recordJobFailure(mockProcess.pid); expect( healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed, - ).to.be.true + ).to.be.true; // Then record a success - healthMonitor.recordJobSuccess(mockProcess.pid) + healthMonitor.recordJobSuccess(mockProcess.pid); expect( healthMonitor.getProcessHealthState(mockProcess.pid)?.lastJobFailed, - ).to.be.false - }) + ).to.be.false; + }); it("should handle recording for non-existent process gracefully", function () { // Should not throw when recording for unknown PID expect(() => { - healthMonitor.recordJobFailure(99999) - healthMonitor.recordJobSuccess(99999) - }).to.not.throw() - }) - }) + healthMonitor.recordJobFailure(99999); + healthMonitor.recordJobSuccess(99999); + }).to.not.throw(); + }); + }); describe("health check execution", function () { let mockBatchProcess: HealthCheckable & { - execTask: (task: Task) => boolean - } + execTask: (task: Task) => boolean; + }; beforeEach(function () { - healthMonitor.initializeProcess(mockProcess.pid) + healthMonitor.initializeProcess(mockProcess.pid); mockBatchProcess = { ...mockProcess, execTask: () => true, // Mock successful task execution - } - }) + }; + }); it("should skip health check when no command configured", function () { // Create monitor with no health check command @@ -268,117 +271,117 @@ describe("ProcessHealthMonitor", function () { processFactory, observer: emitter, healthCheckCommand: "", - }) - const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter) + }); + const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter); - const result = noHealthCheckMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.be.undefined - }) + const result = noHealthCheckMonitor.maybeRunHealthcheck(mockBatchProcess); + expect(result).to.be.undefined; + }); it("should skip health check when process not ready", function () { - const unreadyProcess = { ...mockBatchProcess, idle: false } + const unreadyProcess = { ...mockBatchProcess, idle: false }; - const result = healthMonitor.maybeRunHealthcheck(unreadyProcess) - expect(result).to.be.undefined - }) + const result = healthMonitor.maybeRunHealthcheck(unreadyProcess); + expect(result).to.be.undefined; + }); it("should run health check after job failure", function () { - healthMonitor.recordJobFailure(mockProcess.pid) + healthMonitor.recordJobFailure(mockProcess.pid); - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.not.be.undefined - expect(result?.command).to.eql("healthcheck") - }) + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + expect(result).to.not.be.undefined; + expect(result?.command).to.eql("healthcheck"); + }); it("should run health check after interval expires", function () { // Mock an old health check - const state = healthMonitor.getProcessHealthState(mockProcess.pid) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.lastHealthCheck = Date.now() - 2000 // 2 seconds ago + state.lastHealthCheck = Date.now() - 2000; // 2 seconds ago } - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.not.be.undefined - expect(result?.command).to.eql("healthcheck") - }) + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + expect(result).to.not.be.undefined; + expect(result?.command).to.eql("healthcheck"); + }); it("should not run health check when interval hasn't expired", function () { // Health check was just done - const state = healthMonitor.getProcessHealthState(mockProcess.pid) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.lastHealthCheck = Date.now() + state.lastHealthCheck = Date.now(); } - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.be.undefined - }) + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + expect(result).to.be.undefined; + }); it("should handle failed task execution gracefully", function () { const failingProcess = { ...mockBatchProcess, execTask: () => false, // Mock failed task execution - } + }; - healthMonitor.recordJobFailure(mockProcess.pid) + healthMonitor.recordJobFailure(mockProcess.pid); - const result = healthMonitor.maybeRunHealthcheck(failingProcess) - expect(result).to.be.undefined - }) - }) + const result = healthMonitor.maybeRunHealthcheck(failingProcess); + expect(result).to.be.undefined; + }); + }); describe("health statistics", function () { it("should provide accurate health statistics", function () { // Initialize multiple processes - healthMonitor.initializeProcess(1) - healthMonitor.initializeProcess(2) - healthMonitor.initializeProcess(3) + healthMonitor.initializeProcess(1); + healthMonitor.initializeProcess(2); + healthMonitor.initializeProcess(3); // Add some failures - const state1 = healthMonitor.getProcessHealthState(1) - const state2 = healthMonitor.getProcessHealthState(2) - if (state1 != null) state1.healthCheckFailures = 2 - if (state2 != null) state2.healthCheckFailures = 1 + const state1 = healthMonitor.getProcessHealthState(1); + const state2 = healthMonitor.getProcessHealthState(2); + if (state1 != null) state1.healthCheckFailures = 2; + if (state2 != null) state2.healthCheckFailures = 1; - const stats = healthMonitor.getHealthStats() - expect(stats.monitoredProcesses).to.eql(3) - expect(stats.totalHealthCheckFailures).to.eql(3) - expect(stats.processesWithFailures).to.eql(2) - }) + const stats = healthMonitor.getHealthStats(); + expect(stats.monitoredProcesses).to.eql(3); + expect(stats.totalHealthCheckFailures).to.eql(3); + expect(stats.processesWithFailures).to.eql(2); + }); it("should reset health check failures", function () { - healthMonitor.initializeProcess(mockProcess.pid) + healthMonitor.initializeProcess(mockProcess.pid); // Add some failures - const state = healthMonitor.getProcessHealthState(mockProcess.pid) + const state = healthMonitor.getProcessHealthState(mockProcess.pid); if (state != null) { - state.healthCheckFailures = 5 + state.healthCheckFailures = 5; } - expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(5) + expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(5); - healthMonitor.resetHealthCheckFailures(mockProcess.pid) + healthMonitor.resetHealthCheckFailures(mockProcess.pid); - expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(0) - expect(healthMonitor.isHealthy(mockProcess)).to.be.true - }) - }) + expect(healthMonitor.getHealthStats().totalHealthCheckFailures).to.eql(0); + expect(healthMonitor.isHealthy(mockProcess)).to.be.true; + }); + }); describe("edge cases", function () { it("should handle assessment of process without initialized state", function () { // Don't initialize the process - const healthReason = healthMonitor.assessHealth(mockProcess) - expect(healthReason).to.be.null // Should still work, just no health check state - }) + const healthReason = healthMonitor.assessHealth(mockProcess); + expect(healthReason).to.be.null; // Should still work, just no health check state + }); it("should handle health check on process without state", function () { const mockBatchProcess = { ...mockProcess, execTask: () => true, - } + }; // Don't initialize the process - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess) - expect(result).to.be.undefined - }) - }) -}) + const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + expect(result).to.be.undefined; + }); + }); +}); diff --git a/src/ProcessHealthMonitor.ts b/src/ProcessHealthMonitor.ts index 0bc69d4..c6515c9 100644 --- a/src/ProcessHealthMonitor.ts +++ b/src/ProcessHealthMonitor.ts @@ -1,30 +1,30 @@ -import { BatchClusterEmitter } from "./BatchClusterEmitter" +import { BatchClusterEmitter } from "./BatchClusterEmitter"; import { CompositeHealthCheckStrategy, HealthCheckStrategy, -} from "./HealthCheckStrategy" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { SimpleParser } from "./Parser" -import { blank } from "./String" -import { Task } from "./Task" -import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy" +} from "./HealthCheckStrategy"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { SimpleParser } from "./Parser"; +import { blank } from "./String"; +import { Task } from "./Task"; +import { WhyNotHealthy, WhyNotReady } from "./WhyNotHealthy"; /** * Interface for objects that can be health checked */ export interface HealthCheckable { - readonly pid: number - readonly start: number - readonly taskCount: number - readonly failedTaskCount: number - readonly idleMs: number - readonly idle: boolean - readonly ending: boolean - readonly ended: boolean + readonly pid: number; + readonly start: number; + readonly taskCount: number; + readonly failedTaskCount: number; + readonly idleMs: number; + readonly idle: boolean; + readonly ending: boolean; + readonly ended: boolean; readonly proc: { - stdin?: { destroyed?: boolean } | null - } - readonly currentTask?: Task | null | undefined + stdin?: { destroyed?: boolean } | null; + }; + readonly currentTask?: Task | null | undefined; } /** @@ -35,20 +35,20 @@ export class ProcessHealthMonitor { readonly #healthCheckStates = new Map< number, { - lastHealthCheck: number - healthCheckFailures: number - lastJobFailed: boolean + lastHealthCheck: number; + healthCheckFailures: number; + lastJobFailed: boolean; } - >() + >(); - private readonly healthStrategy: HealthCheckStrategy + private readonly healthStrategy: HealthCheckStrategy; constructor( private readonly options: InternalBatchProcessOptions, private readonly emitter: BatchClusterEmitter, healthStrategy?: HealthCheckStrategy, ) { - this.healthStrategy = healthStrategy ?? new CompositeHealthCheckStrategy() + this.healthStrategy = healthStrategy ?? new CompositeHealthCheckStrategy(); } /** @@ -59,23 +59,23 @@ export class ProcessHealthMonitor { lastHealthCheck: Date.now(), healthCheckFailures: 0, lastJobFailed: false, - }) + }); } /** * Clean up health monitoring for a process */ cleanupProcess(pid: number): void { - this.#healthCheckStates.delete(pid) + this.#healthCheckStates.delete(pid); } /** * Record that a job failed for a process */ recordJobFailure(pid: number): void { - const state = this.#healthCheckStates.get(pid) + const state = this.#healthCheckStates.get(pid); if (state != null) { - state.lastJobFailed = true + state.lastJobFailed = true; } } @@ -83,9 +83,9 @@ export class ProcessHealthMonitor { * Record that a job succeeded for a process */ recordJobSuccess(pid: number): void { - const state = this.#healthCheckStates.get(pid) + const state = this.#healthCheckStates.get(pid); if (state != null) { - state.lastJobFailed = false + state.lastJobFailed = false; } } @@ -96,21 +96,21 @@ export class ProcessHealthMonitor { process: HealthCheckable, overrideReason?: WhyNotHealthy, ): WhyNotHealthy | null { - if (overrideReason != null) return overrideReason + if (overrideReason != null) return overrideReason; - const state = this.#healthCheckStates.get(process.pid) + const state = this.#healthCheckStates.get(process.pid); if (state != null && state.healthCheckFailures > 0) { - return "unhealthy" + return "unhealthy"; } - return this.healthStrategy.assess(process, this.options) + return this.healthStrategy.assess(process, this.options); } /** * Check if a process is healthy */ isHealthy(process: HealthCheckable, overrideReason?: WhyNotHealthy): boolean { - return this.assessHealth(process, overrideReason) == null + return this.assessHealth(process, overrideReason) == null; } /** @@ -120,14 +120,14 @@ export class ProcessHealthMonitor { process: HealthCheckable, overrideReason?: WhyNotHealthy, ): WhyNotReady | null { - return !process.idle ? "busy" : this.assessHealth(process, overrideReason) + return !process.idle ? "busy" : this.assessHealth(process, overrideReason); } /** * Check if a process is ready to handle tasks */ isReady(process: HealthCheckable, overrideReason?: WhyNotHealthy): boolean { - return this.assessReadiness(process, overrideReason) == null + return this.assessReadiness(process, overrideReason) == null; } /** @@ -136,15 +136,15 @@ export class ProcessHealthMonitor { maybeRunHealthcheck( process: HealthCheckable & { execTask: (task: Task) => boolean }, ): Task | undefined { - const hcc = this.options.healthCheckCommand + const hcc = this.options.healthCheckCommand; // if there's no health check command, no-op. - if (hcc == null || blank(hcc)) return + if (hcc == null || blank(hcc)) return; // if the prior health check failed, .ready will be false - if (!this.isReady(process)) return + if (!this.isReady(process)) return; - const state = this.#healthCheckStates.get(process.pid) - if (state == null) return + const state = this.#healthCheckStates.get(process.pid); + if (state == null) return; if ( state.lastJobFailed || @@ -152,8 +152,8 @@ export class ProcessHealthMonitor { Date.now() - state.lastHealthCheck > this.options.healthCheckIntervalMillis) ) { - state.lastHealthCheck = Date.now() - const t = new Task(hcc, SimpleParser) + state.lastHealthCheck = Date.now(); + const t = new Task(hcc, SimpleParser); t.promise .catch((err) => { this.emitter.emit( @@ -161,37 +161,37 @@ export class ProcessHealthMonitor { err instanceof Error ? err : new Error(String(err)), // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument process as any, // Type assertion for event emission - ) - state.healthCheckFailures++ + ); + state.healthCheckFailures++; // BatchCluster will see we're unhealthy and reap us later }) .finally(() => { - state.lastHealthCheck = Date.now() - }) + state.lastHealthCheck = Date.now(); + }); // Execute the health check task on the process if (process.execTask(t as Task)) { - return t as Task + return t as Task; } } - return + return; } /** * Get health statistics for monitoring */ getHealthStats(): { - monitoredProcesses: number - totalHealthCheckFailures: number - processesWithFailures: number + monitoredProcesses: number; + totalHealthCheckFailures: number; + processesWithFailures: number; } { - let totalFailures = 0 - let processesWithFailures = 0 + let totalFailures = 0; + let processesWithFailures = 0; for (const state of this.#healthCheckStates.values()) { - totalFailures += state.healthCheckFailures + totalFailures += state.healthCheckFailures; if (state.healthCheckFailures > 0) { - processesWithFailures++ + processesWithFailures++; } } @@ -199,16 +199,16 @@ export class ProcessHealthMonitor { monitoredProcesses: this.#healthCheckStates.size, totalHealthCheckFailures: totalFailures, processesWithFailures, - } + }; } /** * Reset health check failures for a process (useful for recovery scenarios) */ resetHealthCheckFailures(pid: number): void { - const state = this.#healthCheckStates.get(pid) + const state = this.#healthCheckStates.get(pid); if (state != null) { - state.healthCheckFailures = 0 + state.healthCheckFailures = 0; } } @@ -216,6 +216,6 @@ export class ProcessHealthMonitor { * Get health check state for a specific process */ getProcessHealthState(pid: number) { - return this.#healthCheckStates.get(pid) + return this.#healthCheckStates.get(pid); } } diff --git a/src/ProcessPoolManager.spec.ts b/src/ProcessPoolManager.spec.ts index a372dc8..111e384 100644 --- a/src/ProcessPoolManager.spec.ts +++ b/src/ProcessPoolManager.spec.ts @@ -1,253 +1,256 @@ -import events from "node:events" +import events from "node:events"; import { currentTestPids, expect, processFactory, setFailratePct, setIgnoreExit, -} from "./_chai.spec" -import { delay, until } from "./Async" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { DefaultTestOptions } from "./DefaultTestOptions.spec" -import { verifyOptions } from "./OptionsVerifier" -import { ProcessPoolManager } from "./ProcessPoolManager" +} from "./_chai.spec"; +import { delay, until } from "./Async"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { DefaultTestOptions } from "./DefaultTestOptions.spec"; +import { verifyOptions } from "./OptionsVerifier"; +import { ProcessPoolManager } from "./ProcessPoolManager"; describe("ProcessPoolManager", function () { - let poolManager: ProcessPoolManager - let emitter: BatchClusterEmitter + let poolManager: ProcessPoolManager; + let emitter: BatchClusterEmitter; const onIdle = () => { // callback for when pool manager needs to signal idle state - } + }; beforeEach(function () { - setFailratePct(0) // no failures for pool manager tests - setIgnoreExit(false) - emitter = new events.EventEmitter() as BatchClusterEmitter + setFailratePct(0); // no failures for pool manager tests + setIgnoreExit(false); + emitter = new events.EventEmitter() as BatchClusterEmitter; const options = verifyOptions({ ...DefaultTestOptions, processFactory, observer: emitter, - }) + }); - poolManager = new ProcessPoolManager(options, emitter, onIdle) - }) + poolManager = new ProcessPoolManager(options, emitter, onIdle); + }); afterEach(async function () { if (poolManager != null) { - await poolManager.closeChildProcesses(false) + await poolManager.closeChildProcesses(false); // Wait for processes to actually exit - await until(async () => (await currentTestPids()).length === 0, 5000) + await until(async () => (await currentTestPids()).length === 0, 5000); } - }) + }); describe("initial state", function () { it("should start with no processes", function () { - expect(poolManager.procCount).to.eql(0) - expect(poolManager.busyProcCount).to.eql(0) - expect(poolManager.startingProcCount).to.eql(0) - expect(poolManager.spawnedProcCount).to.eql(0) - expect(poolManager.processes).to.eql([]) - expect(poolManager.findReadyProcess()).to.be.undefined - }) + expect(poolManager.procCount).to.eql(0); + expect(poolManager.busyProcCount).to.eql(0); + expect(poolManager.startingProcCount).to.eql(0); + expect(poolManager.spawnedProcCount).to.eql(0); + expect(poolManager.processes).to.eql([]); + expect(poolManager.findReadyProcess()).to.be.undefined; + }); it("should return empty pids array", function () { - expect(poolManager.pids()).to.eql([]) - }) - }) + expect(poolManager.pids()).to.eql([]); + }); + }); describe("process spawning", function () { it("should spawn processes when there are pending tasks", async function () { - const pendingTaskCount = 2 - await poolManager.maybeSpawnProcs(pendingTaskCount, false) + const pendingTaskCount = 2; + await poolManager.maybeSpawnProcs(pendingTaskCount, false); - expect(poolManager.procCount).to.be.greaterThan(0) - expect(poolManager.spawnedProcCount).to.be.greaterThan(0) + expect(poolManager.procCount).to.be.greaterThan(0); + expect(poolManager.spawnedProcCount).to.be.greaterThan(0); // Wait for processes to be ready - await until(() => poolManager.findReadyProcess() != null, 2000) - expect(poolManager.findReadyProcess()).to.not.be.undefined - }) + await until(() => poolManager.findReadyProcess() != null, 2000); + expect(poolManager.findReadyProcess()).to.not.be.undefined; + }); it("should not spawn more processes than maxProcs", async function () { - const maxProcs = 2 - poolManager.setMaxProcs(maxProcs) + const maxProcs = 2; + poolManager.setMaxProcs(maxProcs); // Try to spawn more than maxProcs - await poolManager.maybeSpawnProcs(5, false) + await poolManager.maybeSpawnProcs(5, false); - expect(poolManager.procCount).to.be.at.most(maxProcs) - }) + expect(poolManager.procCount).to.be.at.most(maxProcs); + }); it("should not spawn processes when ended", async function () { - await poolManager.maybeSpawnProcs(2, true) // ended = true + await poolManager.maybeSpawnProcs(2, true); // ended = true - expect(poolManager.procCount).to.eql(0) - expect(poolManager.spawnedProcCount).to.eql(0) - }) + expect(poolManager.procCount).to.eql(0); + expect(poolManager.spawnedProcCount).to.eql(0); + }); it("should spawn multiple processes for multiple pending tasks", async function () { - const pendingTaskCount = 3 - poolManager.setMaxProcs(4) + const pendingTaskCount = 3; + poolManager.setMaxProcs(4); - await poolManager.maybeSpawnProcs(pendingTaskCount, false) + await poolManager.maybeSpawnProcs(pendingTaskCount, false); // Should spawn up to the number of pending tasks or maxProcs - expect(poolManager.procCount).to.be.at.least(1) - expect(poolManager.procCount).to.be.at.most(Math.min(pendingTaskCount, 4)) - }) - }) + expect(poolManager.procCount).to.be.at.least(1); + expect(poolManager.procCount).to.be.at.most( + Math.min(pendingTaskCount, 4), + ); + }); + }); describe("process management", function () { beforeEach(async function () { // Spawn some processes for testing - await poolManager.maybeSpawnProcs(2, false) - await until(() => poolManager.procCount >= 1, 2000) - }) + await poolManager.maybeSpawnProcs(2, false); + await until(() => poolManager.procCount >= 1, 2000); + }); it("should track process PIDs", function () { - const pids = poolManager.pids() - expect(pids.length).to.be.greaterThan(0) - expect(pids.every((pid) => typeof pid === "number" && pid > 0)).to.be.true - }) + const pids = poolManager.pids(); + expect(pids.length).to.be.greaterThan(0); + expect(pids.every((pid) => typeof pid === "number" && pid > 0)).to.be + .true; + }); it("should find ready processes", async function () { - await until(() => poolManager.findReadyProcess() != null, 2000) - const readyProcess = poolManager.findReadyProcess() - expect(readyProcess).to.not.be.undefined - expect(readyProcess?.ready).to.be.true - }) + await until(() => poolManager.findReadyProcess() != null, 2000); + const readyProcess = poolManager.findReadyProcess(); + expect(readyProcess).to.not.be.undefined; + expect(readyProcess?.ready).to.be.true; + }); it("should vacuum unhealthy processes", async function () { // Wait for processes to be ready - await until(() => poolManager.findReadyProcess() != null, 2000) + await until(() => poolManager.findReadyProcess() != null, 2000); - const initialCount = poolManager.procCount - expect(initialCount).to.be.greaterThan(0) + const initialCount = poolManager.procCount; + expect(initialCount).to.be.greaterThan(0); // Vacuum should not remove healthy processes - await poolManager.vacuumProcs() - expect(poolManager.procCount).to.eql(initialCount) - }) + await poolManager.vacuumProcs(); + expect(poolManager.procCount).to.eql(initialCount); + }); it("should reduce process count when maxProcs is lowered", async function () { // Ensure we have multiple processes - poolManager.setMaxProcs(3) - await poolManager.maybeSpawnProcs(3, false) - await until(() => poolManager.procCount >= 2, 2000) + poolManager.setMaxProcs(3); + await poolManager.maybeSpawnProcs(3, false); + await until(() => poolManager.procCount >= 2, 2000); - const initialCount = poolManager.procCount + const initialCount = poolManager.procCount; // Reduce maxProcs - poolManager.setMaxProcs(1) - await poolManager.vacuumProcs() + poolManager.setMaxProcs(1); + await poolManager.vacuumProcs(); // Should eventually reduce to 1 process (may take time for idle processes to be reaped) - await until(() => poolManager.procCount <= 1, 3000) - expect(poolManager.procCount).to.be.at.most(1) - expect(poolManager.procCount).to.be.lessThanOrEqual(initialCount) - }) - }) + await until(() => poolManager.procCount <= 1, 3000); + expect(poolManager.procCount).to.be.at.most(1); + expect(poolManager.procCount).to.be.lessThanOrEqual(initialCount); + }); + }); describe("process lifecycle", function () { it("should close all processes gracefully", async function () { - await poolManager.maybeSpawnProcs(2, false) - await until(() => poolManager.procCount >= 1, 2000) + await poolManager.maybeSpawnProcs(2, false); + await until(() => poolManager.procCount >= 1, 2000); - const initialPids = poolManager.pids() - expect(initialPids.length).to.be.greaterThan(0) + const initialPids = poolManager.pids(); + expect(initialPids.length).to.be.greaterThan(0); - await poolManager.closeChildProcesses(true) + await poolManager.closeChildProcesses(true); - expect(poolManager.procCount).to.eql(0) + expect(poolManager.procCount).to.eql(0); // Wait for processes to actually exit await until(async () => { - const remainingPids = await currentTestPids() + const remainingPids = await currentTestPids(); return ( remainingPids.filter((pid) => initialPids.includes(pid)).length === 0 - ) - }, 5000) - }) + ); + }, 5000); + }); it("should close all processes forcefully", async function () { - await poolManager.maybeSpawnProcs(2, false) - await until(() => poolManager.procCount >= 1, 2000) + await poolManager.maybeSpawnProcs(2, false); + await until(() => poolManager.procCount >= 1, 2000); - const initialPids = poolManager.pids() - expect(initialPids.length).to.be.greaterThan(0) + const initialPids = poolManager.pids(); + expect(initialPids.length).to.be.greaterThan(0); - await poolManager.closeChildProcesses(false) + await poolManager.closeChildProcesses(false); - expect(poolManager.procCount).to.eql(0) + expect(poolManager.procCount).to.eql(0); // Wait for processes to actually exit await until(async () => { - const remainingPids = await currentTestPids() + const remainingPids = await currentTestPids(); return ( remainingPids.filter((pid) => initialPids.includes(pid)).length === 0 - ) - }, 5000) - }) - }) + ); + }, 5000); + }); + }); describe("process counting", function () { it("should track starting processes", async function () { // Start spawning processes but don't wait for completion - const spawnPromise = poolManager.maybeSpawnProcs(2, false) + const spawnPromise = poolManager.maybeSpawnProcs(2, false); // Should show starting processes initially - await delay(50) // Give it a moment to start - const totalProcs = poolManager.procCount - const startingProcs = poolManager.startingProcCount + await delay(50); // Give it a moment to start + const totalProcs = poolManager.procCount; + const startingProcs = poolManager.startingProcCount; - expect(totalProcs).to.be.greaterThan(0) - expect(startingProcs).to.be.greaterThan(0) + expect(totalProcs).to.be.greaterThan(0); + expect(startingProcs).to.be.greaterThan(0); - await spawnPromise + await spawnPromise; // Wait for processes to be ready - await until(() => poolManager.startingProcCount === 0, 2000) - expect(poolManager.startingProcCount).to.eql(0) - }) + await until(() => poolManager.startingProcCount === 0, 2000); + expect(poolManager.startingProcCount).to.eql(0); + }); it("should track busy vs idle processes", async function () { - await poolManager.maybeSpawnProcs(1, false) - await until(() => poolManager.findReadyProcess() != null, 2000) + await poolManager.maybeSpawnProcs(1, false); + await until(() => poolManager.findReadyProcess() != null, 2000); // Initially all processes should be idle (not busy) - expect(poolManager.busyProcCount).to.eql(0) + expect(poolManager.busyProcCount).to.eql(0); - const readyProcess = poolManager.findReadyProcess() - expect(readyProcess).to.not.be.undefined - expect(readyProcess?.idle).to.be.true - }) - }) + const readyProcess = poolManager.findReadyProcess(); + expect(readyProcess).to.not.be.undefined; + expect(readyProcess?.idle).to.be.true; + }); + }); describe("event integration", function () { it("should work with emitter for process lifecycle events", async function () { - const childStartEvents: any[] = [] - const childEndEvents: any[] = [] + const childStartEvents: any[] = []; + const childEndEvents: any[] = []; emitter.on("childStart", (proc) => { - childStartEvents.push(proc) - }) + childStartEvents.push(proc); + }); emitter.on("childEnd", (proc, reason) => { - childEndEvents.push({ proc, reason }) - }) + childEndEvents.push({ proc, reason }); + }); - await poolManager.maybeSpawnProcs(1, false) - await until(() => childStartEvents.length >= 1, 2000) + await poolManager.maybeSpawnProcs(1, false); + await until(() => childStartEvents.length >= 1, 2000); - expect(childStartEvents.length).to.be.greaterThan(0) + expect(childStartEvents.length).to.be.greaterThan(0); - await poolManager.closeChildProcesses(true) - await until(() => childEndEvents.length >= 1, 2000) + await poolManager.closeChildProcesses(true); + await until(() => childEndEvents.length >= 1, 2000); - expect(childEndEvents.length).to.be.greaterThan(0) - expect(childEndEvents[0].reason).to.eql("ending") - }) - }) -}) + expect(childEndEvents.length).to.be.greaterThan(0); + expect(childEndEvents[0].reason).to.eql("ending"); + }); + }); +}); diff --git a/src/ProcessPoolManager.ts b/src/ProcessPoolManager.ts index fd62fed..97d7034 100644 --- a/src/ProcessPoolManager.ts +++ b/src/ProcessPoolManager.ts @@ -1,54 +1,54 @@ -import timers from "node:timers" -import { count, filterInPlace } from "./Array" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { BatchProcess } from "./BatchProcess" -import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions" -import { asError } from "./Error" -import { Logger } from "./Logger" -import { ProcessHealthMonitor } from "./ProcessHealthMonitor" -import { Task } from "./Task" -import { Timeout, thenOrTimeout } from "./Timeout" +import timers from "node:timers"; +import { count, filterInPlace } from "./Array"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { BatchProcess } from "./BatchProcess"; +import { CombinedBatchProcessOptions } from "./CombinedBatchProcessOptions"; +import { asError } from "./Error"; +import { Logger } from "./Logger"; +import { ProcessHealthMonitor } from "./ProcessHealthMonitor"; +import { Task } from "./Task"; +import { Timeout, thenOrTimeout } from "./Timeout"; /** * Manages the lifecycle of a pool of BatchProcess instances. * Handles spawning, health monitoring, and cleanup of child processes. */ export class ProcessPoolManager { - readonly #procs: BatchProcess[] = [] - readonly #logger: () => Logger - readonly #healthMonitor: ProcessHealthMonitor - #nextSpawnTime = 0 - #lastPidsCheckTime = 0 - #spawnedProcs = 0 + readonly #procs: BatchProcess[] = []; + readonly #logger: () => Logger; + readonly #healthMonitor: ProcessHealthMonitor; + #nextSpawnTime = 0; + #lastPidsCheckTime = 0; + #spawnedProcs = 0; constructor( private readonly options: CombinedBatchProcessOptions, private readonly emitter: BatchClusterEmitter, private readonly onIdle: () => void, ) { - this.#logger = options.logger - this.#healthMonitor = new ProcessHealthMonitor(options, emitter) + this.#logger = options.logger; + this.#healthMonitor = new ProcessHealthMonitor(options, emitter); } /** * Get all current processes */ get processes(): readonly BatchProcess[] { - return this.#procs + return this.#procs; } /** * Get the current number of spawned child processes */ get procCount(): number { - return this.#procs.length + return this.#procs.length; } /** * Alias for procCount to match BatchCluster interface */ get processCount(): number { - return this.procCount + return this.procCount; } /** @@ -59,7 +59,7 @@ export class ProcessPoolManager { this.#procs, // don't count procs that are starting up as "busy": (ea) => !ea.starting && !ea.ending && !ea.idle, - ) + ); } /** @@ -70,48 +70,48 @@ export class ProcessPoolManager { this.#procs, // don't count procs that are starting up as "busy": (ea) => ea.starting && !ea.ending, - ) + ); } /** * Get the current number of ready processes */ get readyProcCount(): number { - return count(this.#procs, (ea) => ea.ready) + return count(this.#procs, (ea) => ea.ready); } /** * Get the total number of child processes created by this instance */ get spawnedProcCount(): number { - return this.#spawnedProcs + return this.#spawnedProcs; } /** * Get the milliseconds until the next spawn is allowed */ get msBeforeNextSpawn(): number { - return Math.max(0, this.#nextSpawnTime - Date.now()) + return Math.max(0, this.#nextSpawnTime - Date.now()); } /** * Get all currently running tasks from all processes */ currentTasks(): Task[] { - const tasks: Task[] = [] + const tasks: Task[] = []; for (const proc of this.#procs) { if (proc.currentTask != null) { - tasks.push(proc.currentTask) + tasks.push(proc.currentTask); } } - return tasks + return tasks; } /** * Find the first ready process that can handle a new task */ findReadyProcess(): BatchProcess | undefined { - return this.#procs.find((ea) => ea.ready) + return this.#procs.find((ea) => ea.ready); } /** @@ -119,28 +119,28 @@ export class ProcessPoolManager { * @return the spawned PIDs that are still in the process table. */ pids(): number[] { - const arr: number[] = [] + const arr: number[] = []; for (const proc of [...this.#procs]) { if (proc != null && proc.running()) { - arr.push(proc.pid) + arr.push(proc.pid); } } - return arr + return arr; } /** * Shut down any currently-running child processes. */ async closeChildProcesses(gracefully = true): Promise { - const procs = [...this.#procs] - this.#procs.length = 0 + const procs = [...this.#procs]; + this.#procs.length = 0; await Promise.all( procs.map((proc) => proc .end(gracefully, "ending") .catch((err) => this.emitter.emit("endError", asError(err), proc)), ), - ) + ); } /** @@ -148,9 +148,9 @@ export class ProcessPoolManager { * Removes unhealthy processes and enforces maxProcs limit. */ vacuumProcs(): Promise { - this.#maybeCheckPids() - const endPromises: Promise[] = [] - let pidsToReap = Math.max(0, this.#procs.length - this.options.maxProcs) + this.#maybeCheckPids(); + const endPromises: Promise[] = []; + let pidsToReap = Math.max(0, this.#procs.length - this.options.maxProcs); filterInPlace(this.#procs, (proc) => { // Only check `.idle` (not `.ready`) procs. We don't want to reap busy @@ -161,17 +161,18 @@ export class ProcessPoolManager { // within filterInPlace because #procs.length only changes at iteration // completion: the prior impl resulted in all idle pids getting reaped // when maxProcs was reduced. - const why = proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null) + const why = + proc.whyNotHealthy ?? (--pidsToReap >= 0 ? "tooMany" : null); if (why != null) { - endPromises.push(proc.end(true, why)) - return false + endPromises.push(proc.end(true, why)); + return false; } - proc.maybeRunHealthcheck() + proc.maybeRunHealthcheck(); } - return true - }) + return true; + }); - return Promise.all(endPromises) + return Promise.all(endPromises); } /** @@ -181,34 +182,34 @@ export class ProcessPoolManager { pendingTaskCount: number, ended: boolean, ): Promise { - let procsToSpawn = this.#procsToSpawn(pendingTaskCount) + let procsToSpawn = this.#procsToSpawn(pendingTaskCount); if (ended || this.#nextSpawnTime > Date.now() || procsToSpawn === 0) { - return + return; } // prevent concurrent runs: - this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() + this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay(); for (let i = 0; i < procsToSpawn; i++) { if (ended) { - break + break; } // Kick the lock down the road: - this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay() - this.#spawnedProcs++ + this.#nextSpawnTime = Date.now() + this.#maxSpawnDelay(); + this.#spawnedProcs++; try { - const proc = this.#spawnNewProc() + const proc = this.#spawnNewProc(); const result = await thenOrTimeout( proc, this.options.spawnTimeoutMillis, - ) + ); if (result === Timeout) { void proc .then((bp) => { - void bp.end(false, "startError") + void bp.end(false, "startError"); this.emitter.emit( "startError", asError( @@ -217,18 +218,18 @@ export class ProcessPoolManager { "ms", ), bp, - ) + ); }) .catch((err) => { // this should only happen if the processFactory throws a // rejection: - this.emitter.emit("startError", asError(err)) - }) + this.emitter.emit("startError", asError(err)); + }); } else { this.#logger().debug( "ProcessPoolManager.maybeSpawnProcs() started healthy child process", { pid: result.pid }, - ) + ); } // tasks may have been popped off or setMaxProcs may have reduced @@ -236,26 +237,26 @@ export class ProcessPoolManager { procsToSpawn = Math.min( this.#procsToSpawn(pendingTaskCount), procsToSpawn, - ) + ); } catch (err) { - this.emitter.emit("startError", asError(err)) + this.emitter.emit("startError", asError(err)); } } // YAY WE MADE IT. // Only let more children get spawned after minDelay: - const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis) - this.#nextSpawnTime = Date.now() + delay + const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis); + this.#nextSpawnTime = Date.now() + delay; // And schedule #onIdle for that time: - timers.setTimeout(this.onIdle, delay).unref() + timers.setTimeout(this.onIdle, delay).unref(); } /** * Update the maximum number of processes allowed */ setMaxProcs(maxProcs: number): void { - this.options.maxProcs = maxProcs + this.options.maxProcs = maxProcs; } #maybeCheckPids(): void { @@ -264,46 +265,49 @@ export class ProcessPoolManager { this.options.pidCheckIntervalMillis > 0 && this.#lastPidsCheckTime + this.options.pidCheckIntervalMillis < Date.now() ) { - this.#lastPidsCheckTime = Date.now() - void this.pids() + this.#lastPidsCheckTime = Date.now(); + void this.pids(); } } #maxSpawnDelay(): number { // 10s delay is certainly long enough for .spawn() to return, even on a // loaded windows machine. - return Math.max(10_000, this.options.spawnTimeoutMillis) + return Math.max(10_000, this.options.spawnTimeoutMillis); } #procsToSpawn(pendingTaskCount: number): number { - const remainingCapacity = this.options.maxProcs - this.#procs.length + const remainingCapacity = this.options.maxProcs - this.#procs.length; // take into account starting procs, so one task doesn't result in multiple // processes being spawned: - const requestedCapacity = pendingTaskCount - this.startingProcCount + const requestedCapacity = pendingTaskCount - this.startingProcCount; - const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity)) + const atLeast0 = Math.max( + 0, + Math.min(remainingCapacity, requestedCapacity), + ); return this.options.minDelayBetweenSpawnMillis === 0 ? // we can spin up multiple processes in parallel. atLeast0 : // Don't spin up more than 1: - Math.min(1, atLeast0) + Math.min(1, atLeast0); } // must only be called by this.maybeSpawnProcs() async #spawnNewProc(): Promise { // no matter how long it takes to spawn, always push the result into #procs // so we don't leak child processes: - const procOrPromise = this.options.processFactory() - const proc = await procOrPromise + const procOrPromise = this.options.processFactory(); + const proc = await procOrPromise; const result = new BatchProcess( proc, this.options, this.onIdle, this.#healthMonitor, - ) - this.#procs.push(result) - return result + ); + this.#procs.push(result); + return result; } } diff --git a/src/ProcessTerminator.spec.ts b/src/ProcessTerminator.spec.ts index 68f29f7..5831ec3 100644 --- a/src/ProcessTerminator.spec.ts +++ b/src/ProcessTerminator.spec.ts @@ -1,37 +1,37 @@ -import events from "node:events" -import stream from "node:stream" -import { expect } from "./_chai.spec" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { logger } from "./Logger" -import { SimpleParser } from "./Parser" -import { ProcessTerminator } from "./ProcessTerminator" -import { Task } from "./Task" +import events from "node:events"; +import stream from "node:stream"; +import { expect } from "./_chai.spec"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { logger } from "./Logger"; +import { SimpleParser } from "./Parser"; +import { ProcessTerminator } from "./ProcessTerminator"; +import { Task } from "./Task"; describe("ProcessTerminator", function () { - let terminator: ProcessTerminator - let mockProcess: MockChildProcess - let emitter: BatchClusterEmitter - let options: InternalBatchProcessOptions - let isRunningResult: boolean - let childEndEvents: { process: any; reason: string }[] + let terminator: ProcessTerminator; + let mockProcess: MockChildProcess; + let emitter: BatchClusterEmitter; + let options: InternalBatchProcessOptions; + let isRunningResult: boolean; + let childEndEvents: { process: any; reason: string }[]; // Mock child process class class MockChildProcess extends events.EventEmitter { - pid = 12345 - stdin = new MockWritableStream() - stdout = new MockReadableStream() - stderr = new MockReadableStream() - killed = false - disconnected = false + pid = 12345; + stdin = new MockWritableStream(); + stdout = new MockReadableStream(); + stderr = new MockReadableStream(); + killed = false; + disconnected = false; kill() { - this.killed = true - return true + this.killed = true; + return true; } disconnect() { - this.disconnected = true + this.disconnected = true; } unref() { @@ -40,53 +40,53 @@ describe("ProcessTerminator", function () { } class MockWritableStream extends stream.Writable { - override destroyed = false - override writable = true - data: string[] = [] + override destroyed = false; + override writable = true; + data: string[] = []; override _write(chunk: any, _encoding: any, callback: any) { - this.data.push(chunk.toString()) - callback() + this.data.push(chunk.toString()); + callback(); } override end(data?: any): this { if (data != null) { - this.data.push(data.toString()) + this.data.push(data.toString()); } - this.writable = false - super.end() - return this + this.writable = false; + super.end(); + return this; } override destroy(): this { - this.destroyed = true - super.destroy() - return this + this.destroyed = true; + super.destroy(); + return this; } } class MockReadableStream extends stream.Readable { - override destroyed = false + override destroyed = false; override _read() { // no-op for tests } override destroy(): this { - this.destroyed = true - super.destroy() - return this + this.destroyed = true; + super.destroy(); + return this; } } beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter - childEndEvents = [] + emitter = new events.EventEmitter() as BatchClusterEmitter; + childEndEvents = []; // Track childEnd events emitter.on("childEnd", (process: any, reason: string) => { - childEndEvents.push({ process, reason }) - }) + childEndEvents.push({ process, reason }); + }); options = { logger, @@ -113,31 +113,31 @@ describe("ProcessTerminator", function () { maxReasonableProcessFailuresPerMinute: 10, minDelayBetweenSpawnMillis: 100, pidCheckIntervalMillis: 150, - } + }; - terminator = new ProcessTerminator(options) - mockProcess = new MockChildProcess() - isRunningResult = true - }) + terminator = new ProcessTerminator(options); + mockProcess = new MockChildProcess(); + isRunningResult = true; + }); function createMockTask( taskId = 1, command = "test", pending = true, ): Task { - const task = new Task(command, SimpleParser) + const task = new Task(command, SimpleParser); if (!pending) { // Simulate task completion by calling onStdout with PASS token - task.onStart(options) - task.onStdout("PASS") + task.onStart(options); + task.onStdout("PASS"); } // Override taskId for testing - Object.defineProperty(task, "taskId", { value: taskId, writable: true }) - return task as Task + Object.defineProperty(task, "taskId", { value: taskId, writable: true }); + return task as Task; } function mockIsRunning(): boolean { - return isRunningResult + return isRunningResult; } describe("basic termination", function () { @@ -150,19 +150,19 @@ describe("ProcessTerminator", function () { true, // graceful false, // not exited mockIsRunning, - ) + ); // Should send exit command - expect(mockProcess.stdin.data).to.include("exit\n") + expect(mockProcess.stdin.data).to.include("exit\n"); // Should destroy streams - expect(mockProcess.stdin.destroyed).to.be.true - expect(mockProcess.stdout.destroyed).to.be.true - expect(mockProcess.stderr.destroyed).to.be.true + expect(mockProcess.stdin.destroyed).to.be.true; + expect(mockProcess.stdout.destroyed).to.be.true; + expect(mockProcess.stderr.destroyed).to.be.true; // Should disconnect - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should terminate process forcefully when not graceful", async function () { await terminator.terminate( @@ -173,11 +173,11 @@ describe("ProcessTerminator", function () { false, // not graceful false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.include("exit\n") - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.stdin.data).to.include("exit\n"); + expect(mockProcess.disconnected).to.be.true; + }); it("should handle process that is already exited", async function () { await terminator.terminate( @@ -188,24 +188,24 @@ describe("ProcessTerminator", function () { true, true, // already exited mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.include("exit\n") - expect(mockProcess.disconnected).to.be.true - }) - }) + expect(mockProcess.stdin.data).to.include("exit\n"); + expect(mockProcess.disconnected).to.be.true; + }); + }); describe("task completion handling", function () { it("should wait for non-startup task to complete gracefully", async function () { - const task = createMockTask(1, "test command", true) - let taskCompleted = false + const task = createMockTask(1, "test command", true); + let taskCompleted = false; // Simulate task completion after a delay setTimeout(() => { - taskCompleted = true - task.onStart(options) - task.onStdout("PASS") // Complete the task - }, 50) + taskCompleted = true; + task.onStart(options); + task.onStdout("PASS"); // Complete the task + }, 50); await terminator.terminate( mockProcess as any, @@ -215,21 +215,21 @@ describe("ProcessTerminator", function () { true, // graceful false, mockIsRunning, - ) + ); - expect(taskCompleted).to.be.true - expect(task.state !== "pending").to.be.true - }) + expect(taskCompleted).to.be.true; + expect(task.state !== "pending").to.be.true; + }); it("should reject pending task if termination timeout occurs", async function () { - const task = createMockTask(1, "slow task", true) - let taskRejected = false - let rejectionReason = "" + const task = createMockTask(1, "slow task", true); + let taskRejected = false; + let rejectionReason = ""; task.promise.catch((err) => { - taskRejected = true - rejectionReason = err.message - }) + taskRejected = true; + rejectionReason = err.message; + }); await terminator.terminate( mockProcess as any, @@ -239,16 +239,16 @@ describe("ProcessTerminator", function () { false, // not graceful - shorter timeout false, mockIsRunning, - ) + ); - expect(taskRejected).to.be.true + expect(taskRejected).to.be.true; expect(rejectionReason).to.include( "Process terminated before task completed", - ) - }) + ); + }); it("should skip task completion wait for startup task", async function () { - const startupTask = createMockTask(999, "version", true) + const startupTask = createMockTask(999, "version", true); await terminator.terminate( mockProcess as any, @@ -258,11 +258,11 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Should not wait for or reject startup task - expect(startupTask.pending).to.be.true - }) + expect(startupTask.pending).to.be.true; + }); it("should skip task completion wait when no current task", async function () { await terminator.terminate( @@ -273,23 +273,23 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Should complete without errors - expect(mockProcess.disconnected).to.be.true - }) - }) + expect(mockProcess.disconnected).to.be.true; + }); + }); describe("stream handling", function () { it("should remove error listeners from all streams", async function () { // Add some error listeners const errorHandler = () => { // no-op for test - } - mockProcess.on("error", errorHandler) - mockProcess.stdin.on("error", errorHandler) - mockProcess.stdout.on("error", errorHandler) - mockProcess.stderr.on("error", errorHandler) + }; + mockProcess.on("error", errorHandler); + mockProcess.stdin.on("error", errorHandler); + mockProcess.stdout.on("error", errorHandler); + mockProcess.stderr.on("error", errorHandler); await terminator.terminate( mockProcess as any, @@ -299,17 +299,17 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Error listeners should be removed - expect(mockProcess.listenerCount("error")).to.equal(0) - expect(mockProcess.stdin.listenerCount("error")).to.equal(0) - expect(mockProcess.stdout.listenerCount("error")).to.equal(0) - expect(mockProcess.stderr.listenerCount("error")).to.equal(0) - }) + expect(mockProcess.listenerCount("error")).to.equal(0); + expect(mockProcess.stdin.listenerCount("error")).to.equal(0); + expect(mockProcess.stdout.listenerCount("error")).to.equal(0); + expect(mockProcess.stderr.listenerCount("error")).to.equal(0); + }); it("should send exit command if stdin is writable", async function () { - mockProcess.stdin.writable = true + mockProcess.stdin.writable = true; await terminator.terminate( mockProcess as any, @@ -319,13 +319,13 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.include("exit\n") - }) + expect(mockProcess.stdin.data).to.include("exit\n"); + }); it("should skip exit command if stdin is not writable", async function () { - mockProcess.stdin.writable = false + mockProcess.stdin.writable = false; await terminator.terminate( mockProcess as any, @@ -335,14 +335,14 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.data).to.be.empty - }) + expect(mockProcess.stdin.data).to.be.empty; + }); it("should handle missing exit command gracefully", async function () { - const optionsNoExit = { ...options, exitCommand: undefined } - const terminatorNoExit = new ProcessTerminator(optionsNoExit) + const optionsNoExit = { ...options, exitCommand: undefined }; + const terminatorNoExit = new ProcessTerminator(optionsNoExit); await terminatorNoExit.terminate( mockProcess as any, @@ -352,12 +352,12 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); // Should complete without sending exit command - expect(mockProcess.stdin.data).to.be.empty - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.stdin.data).to.be.empty; + expect(mockProcess.disconnected).to.be.true; + }); it("should destroy all streams", async function () { await terminator.terminate( @@ -368,31 +368,31 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.stdin.destroyed).to.be.true - expect(mockProcess.stdout.destroyed).to.be.true - expect(mockProcess.stderr.destroyed).to.be.true - }) - }) + expect(mockProcess.stdin.destroyed).to.be.true; + expect(mockProcess.stdout.destroyed).to.be.true; + expect(mockProcess.stderr.destroyed).to.be.true; + }); + }); describe("graceful shutdown", function () { it("should wait for process to exit gracefully", async function () { - let killCalled = false + let killCalled = false; mockProcess.kill = () => { - killCalled = true - return true - } + killCalled = true; + return true; + }; // Simulate process still running initially, then stopping - let callCount = 0 + let callCount = 0; const mockIsRunningGraceful = () => { - callCount++ + callCount++; if (callCount <= 2) { - return true // Still running for first few checks + return true; // Still running for first few checks } - return false // Then stops running - } + return false; // Then stops running + }; await terminator.terminate( mockProcess as any, @@ -402,18 +402,18 @@ describe("ProcessTerminator", function () { true, // graceful false, // not already exited mockIsRunningGraceful, - ) + ); - expect(killCalled).to.be.false // Should not need to kill - }) + expect(killCalled).to.be.false; // Should not need to kill + }); it("should send SIGTERM if process doesn't exit gracefully", async function () { - let killCalled = false + let killCalled = false; mockProcess.kill = () => { - killCalled = true - isRunningResult = false // Process stops after kill signal - return true - } + killCalled = true; + isRunningResult = false; // Process stops after kill signal + return true; + }; await terminator.terminate( mockProcess as any, @@ -423,20 +423,20 @@ describe("ProcessTerminator", function () { true, // graceful false, // not already exited mockIsRunning, // Always returns true until killed - ) + ); - expect(killCalled).to.be.true - }) + expect(killCalled).to.be.true; + }); it("should skip graceful shutdown when cleanup disabled", async function () { - const optionsNoCleanup = { ...options, cleanupChildProcs: false } - const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup) + const optionsNoCleanup = { ...options, cleanupChildProcs: false }; + const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup); - let killCalled = false + let killCalled = false; mockProcess.kill = () => { - killCalled = true - return true - } + killCalled = true; + return true; + }; await terminatorNoCleanup.terminate( mockProcess as any, @@ -446,20 +446,20 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(killCalled).to.be.false - }) + expect(killCalled).to.be.false; + }); it("should skip graceful shutdown when wait time is 0", async function () { - const optionsNoWait = { ...options, endGracefulWaitTimeMillis: 0 } - const terminatorNoWait = new ProcessTerminator(optionsNoWait) + const optionsNoWait = { ...options, endGracefulWaitTimeMillis: 0 }; + const terminatorNoWait = new ProcessTerminator(optionsNoWait); - let killCalled = false + let killCalled = false; mockProcess.kill = () => { - killCalled = true - return true - } + killCalled = true; + return true; + }; await terminatorNoWait.terminate( mockProcess as any, @@ -469,16 +469,16 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(killCalled).to.be.false - }) - }) + expect(killCalled).to.be.false; + }); + }); describe("force killing", function () { it("should complete termination even with stubborn process", async function () { // Process keeps running even after signals - const mockIsRunningStubborn = () => true + const mockIsRunningStubborn = () => true; // Should complete without throwing await terminator.terminate( @@ -489,16 +489,16 @@ describe("ProcessTerminator", function () { true, false, mockIsRunningStubborn, - ) + ); // Should still disconnect and destroy streams - expect(mockProcess.disconnected).to.be.true - expect(mockProcess.stdin.destroyed).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + expect(mockProcess.stdin.destroyed).to.be.true; + }); it("should complete termination when cleanup disabled", async function () { - const optionsNoCleanup = { ...options, cleanupChildProcs: false } - const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup) + const optionsNoCleanup = { ...options, cleanupChildProcs: false }; + const terminatorNoCleanup = new ProcessTerminator(optionsNoCleanup); await terminatorNoCleanup.terminate( mockProcess as any, @@ -508,15 +508,15 @@ describe("ProcessTerminator", function () { true, false, () => true, // Always running - ) + ); // Should still complete basic cleanup - expect(mockProcess.disconnected).to.be.true - expect(mockProcess.stdin.destroyed).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + expect(mockProcess.stdin.destroyed).to.be.true; + }); it("should handle process with no PID gracefully", async function () { - ;(mockProcess as any).pid = undefined + (mockProcess as any).pid = undefined; await terminator.terminate( mockProcess as any, @@ -526,19 +526,19 @@ describe("ProcessTerminator", function () { true, false, () => true, - ) + ); // Should complete without issues - expect(mockProcess.disconnected).to.be.true - expect(mockProcess.stdin.destroyed).to.be.true - }) - }) + expect(mockProcess.disconnected).to.be.true; + expect(mockProcess.stdin.destroyed).to.be.true; + }); + }); describe("error handling", function () { it("should handle stdin.end() errors gracefully", async function () { mockProcess.stdin.end = () => { - throw new Error("EPIPE") - } + throw new Error("EPIPE"); + }; // Should not throw await terminator.terminate( @@ -549,15 +549,15 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should handle stream destruction errors gracefully", async function () { mockProcess.stdout.destroy = () => { - throw new Error("Stream error") - } + throw new Error("Stream error"); + }; // Should not throw await terminator.terminate( @@ -568,15 +568,15 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should handle disconnect errors gracefully", async function () { mockProcess.disconnect = () => { - throw new Error("Disconnect error") - } + throw new Error("Disconnect error"); + }; // Should not throw await terminator.terminate( @@ -587,13 +587,13 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) - }) - }) + ); + }); + }); describe("edge cases", function () { it("should handle null stderr stream", async function () { - ;(mockProcess as any).stderr = null + (mockProcess as any).stderr = null; await terminator.terminate( mockProcess as any, @@ -603,13 +603,13 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) + expect(mockProcess.disconnected).to.be.true; + }); it("should handle already completed task", async function () { - const completedTask = createMockTask(1, "completed", false) + const completedTask = createMockTask(1, "completed", false); await terminator.terminate( mockProcess as any, @@ -619,14 +619,14 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(completedTask.state !== "pending").to.be.true - expect(mockProcess.disconnected).to.be.true - }) + expect(completedTask.state !== "pending").to.be.true; + expect(mockProcess.disconnected).to.be.true; + }); it("should handle process with undefined PID", async function () { - ;(mockProcess as any).pid = undefined + (mockProcess as any).pid = undefined; await terminator.terminate( mockProcess as any, @@ -636,16 +636,16 @@ describe("ProcessTerminator", function () { true, false, mockIsRunning, - ) + ); - expect(mockProcess.disconnected).to.be.true - }) - }) + expect(mockProcess.disconnected).to.be.true; + }); + }); describe("timing and timeouts", function () { it("should respect graceful task timeout", async function () { - const slowTask = createMockTask(1, "slow", true) - const startTime = Date.now() + const slowTask = createMockTask(1, "slow", true); + const startTime = Date.now(); await terminator.terminate( mockProcess as any, @@ -655,18 +655,18 @@ describe("ProcessTerminator", function () { true, // graceful - should wait up to 2000ms for task false, mockIsRunning, - ) + ); - const elapsed = Date.now() - startTime + const elapsed = Date.now() - startTime; // Should have waited some time but not too long - expect(elapsed).to.be.greaterThan(50) - expect(elapsed).to.be.lessThan(4000) - expect(slowTask.pending).to.be.false // Should be rejected - }) + expect(elapsed).to.be.greaterThan(50); + expect(elapsed).to.be.lessThan(4000); + expect(slowTask.pending).to.be.false; // Should be rejected + }); it("should respect non-graceful task timeout", async function () { - const slowTask = createMockTask(1, "slow", true) - const startTime = Date.now() + const slowTask = createMockTask(1, "slow", true); + const startTime = Date.now(); await terminator.terminate( mockProcess as any, @@ -676,12 +676,12 @@ describe("ProcessTerminator", function () { false, // not graceful - should wait only 250ms for task false, mockIsRunning, - ) + ); - const elapsed = Date.now() - startTime + const elapsed = Date.now() - startTime; // Should have waited less time - expect(elapsed).to.be.lessThan(1000) - expect(slowTask.pending).to.be.false // Should be rejected - }) - }) -}) + expect(elapsed).to.be.lessThan(1000); + expect(slowTask.pending).to.be.false; // Should be rejected + }); + }); +}); diff --git a/src/ProcessTerminator.ts b/src/ProcessTerminator.ts index 547bedf..a457b49 100644 --- a/src/ProcessTerminator.ts +++ b/src/ProcessTerminator.ts @@ -1,21 +1,21 @@ -import child_process from "node:child_process" -import { until } from "./Async" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { Logger } from "./Logger" -import { kill } from "./Pids" -import { destroy } from "./Stream" -import { ensureSuffix } from "./String" -import { Task } from "./Task" -import { thenOrTimeout } from "./Timeout" +import child_process from "node:child_process"; +import { until } from "./Async"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { Logger } from "./Logger"; +import { kill } from "./Pids"; +import { destroy } from "./Stream"; +import { ensureSuffix } from "./String"; +import { Task } from "./Task"; +import { thenOrTimeout } from "./Timeout"; /** * Utility class for managing process termination lifecycle */ export class ProcessTerminator { - readonly #logger: () => Logger + readonly #logger: () => Logger; constructor(private readonly opts: InternalBatchProcessOptions) { - this.#logger = opts.logger + this.#logger = opts.logger; } /** @@ -42,26 +42,26 @@ export class ProcessTerminator { isRunning: () => boolean, ): Promise { // Wait for current task to complete if graceful termination requested - await this.#waitForTaskCompletion(lastTask, startupTaskId, gracefully) + await this.#waitForTaskCompletion(lastTask, startupTaskId, gracefully); // Remove error listeners to prevent EPIPE errors during termination - this.#removeErrorListeners(proc) + this.#removeErrorListeners(proc); // Send exit command to process - this.#sendExitCommand(proc) + this.#sendExitCommand(proc); // Destroy streams - this.#destroyStreams(proc) + this.#destroyStreams(proc); // Handle graceful shutdown with timeouts - await this.#handleGracefulShutdown(proc, gracefully, isExited, isRunning) + await this.#handleGracefulShutdown(proc, gracefully, isExited, isRunning); // Force kill if still running - this.#forceKillIfRunning(proc, processName, isRunning) + this.#forceKillIfRunning(proc, processName, isRunning); // Final cleanup try { - proc.disconnect?.() + proc.disconnect?.(); } catch { // Ignore disconnect errors } @@ -75,12 +75,12 @@ export class ProcessTerminator { ): Promise { // Don't wait for startup tasks or if no task is running if (lastTask == null || lastTask.taskId === startupTaskId) { - return + return; } try { // Wait for the process to complete and streams to flush - await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250) + await thenOrTimeout(lastTask.promise, gracefully ? 2000 : 250); } catch { // Ignore errors during task completion wait } @@ -94,7 +94,7 @@ export class ProcessTerminator { lastTask, })})`, ), - ) + ); } } @@ -102,22 +102,22 @@ export class ProcessTerminator { // Remove error listeners to prevent EPIPE errors during termination // See https://github.com/nodejs/node/issues/26828 for (const stream of [proc, proc.stdin, proc.stdout, proc.stderr]) { - stream?.removeAllListeners("error") + stream?.removeAllListeners("error"); } } #sendExitCommand(proc: child_process.ChildProcess): void { if (proc.stdin?.writable !== true) { - return + return; } const exitCmd = this.opts.exitCommand == null ? null - : ensureSuffix(this.opts.exitCommand, "\n") + : ensureSuffix(this.opts.exitCommand, "\n"); try { - proc.stdin.end(exitCmd) + proc.stdin.end(exitCmd); } catch { // Ignore errors when sending exit command } @@ -125,9 +125,9 @@ export class ProcessTerminator { #destroyStreams(proc: child_process.ChildProcess): void { // Destroy all streams to ensure cleanup - destroy(proc.stdin) - destroy(proc.stdout) - destroy(proc.stderr) + destroy(proc.stdin); + destroy(proc.stdout); + destroy(proc.stderr); } async #handleGracefulShutdown( @@ -142,25 +142,25 @@ export class ProcessTerminator { this.opts.endGracefulWaitTimeMillis <= 0 || isExited ) { - return + return; } // Wait for the exit command to take effect await this.#awaitNotRunning( this.opts.endGracefulWaitTimeMillis / 2, isRunning, - ) + ); // If still running, send kill signal if (isRunning() && proc.pid != null) { - proc.kill() + proc.kill(); } // Wait for the signal handler to work await this.#awaitNotRunning( this.opts.endGracefulWaitTimeMillis / 2, isRunning, - ) + ); } #forceKillIfRunning( @@ -171,8 +171,8 @@ export class ProcessTerminator { if (this.opts.cleanupChildProcs && proc.pid != null && isRunning()) { this.#logger().warn( `${processName}.terminate(): force-killing still-running child.`, - ) - kill(proc.pid, true) + ); + kill(proc.pid, true); } } @@ -180,6 +180,6 @@ export class ProcessTerminator { timeout: number, isRunning: () => boolean, ): Promise { - await until(() => !isRunning(), timeout) + await until(() => !isRunning(), timeout); } } diff --git a/src/Rate.spec.ts b/src/Rate.spec.ts index c2c6c1a..5abb1aa 100644 --- a/src/Rate.spec.ts +++ b/src/Rate.spec.ts @@ -1,79 +1,79 @@ -import FakeTimers from "@sinonjs/fake-timers" -import { minuteMs } from "./BatchClusterOptions" -import { Rate } from "./Rate" -import { expect, times } from "./_chai.spec" +import FakeTimers from "@sinonjs/fake-timers"; +import { minuteMs } from "./BatchClusterOptions"; +import { Rate } from "./Rate"; +import { expect, times } from "./_chai.spec"; describe("Rate", () => { - const now = Date.now() - const r = new Rate() - let clock: FakeTimers.InstalledClock + const now = Date.now(); + const r = new Rate(); + let clock: FakeTimers.InstalledClock; beforeEach(() => { - clock = FakeTimers.install({ now: now }) + clock = FakeTimers.install({ now: now }); // clear() must be called _after_ setting up fake timers - r.clear() - }) + r.clear(); + }); afterEach(() => { - clock.uninstall() - }) + clock.uninstall(); + }); function expectRate(rate: Rate, epm: number, tol = 0.1) { - expect(rate.eventsPerMs).to.be.withinToleranceOf(epm, tol) - expect(rate.eventsPerSecond).to.be.withinToleranceOf(epm * 1000, tol) - expect(rate.eventsPerMinute).to.be.withinToleranceOf(epm * 60 * 1000, tol) + expect(rate.eventsPerMs).to.be.withinToleranceOf(epm, tol); + expect(rate.eventsPerSecond).to.be.withinToleranceOf(epm * 1000, tol); + expect(rate.eventsPerMinute).to.be.withinToleranceOf(epm * 60 * 1000, tol); } it("is born with a rate of 0", () => { - expectRate(r, 0) - }) + expectRate(r, 0); + }); it("maintains a rate of 0 after time with no events", () => { - clock.tick(minuteMs) - expectRate(r, 0) - }) + clock.tick(minuteMs); + expectRate(r, 0); + }); for (const cnt of [1, 2, 3, 4]) { it( "decays the rate from " + cnt + " simultaneous event(s) as time elapses", () => { - times(cnt, () => r.onEvent()) - expectRate(r, 0) - clock.tick(100) - expectRate(r, 0) - clock.tick(r.warmupMs - 100 + 1) - expectRate(r, cnt / r.warmupMs) - clock.tick(r.warmupMs) - expectRate(r, cnt / (2 * r.warmupMs)) - clock.tick(r.warmupMs) - expectRate(r, cnt / (3 * r.warmupMs)) - clock.tick(r.periodMs - 3 * r.warmupMs) - expectRate(r, 0) - expect(r.msSinceLastEvent).to.be.closeTo(r.periodMs, 5) + times(cnt, () => r.onEvent()); + expectRate(r, 0); + clock.tick(100); + expectRate(r, 0); + clock.tick(r.warmupMs - 100 + 1); + expectRate(r, cnt / r.warmupMs); + clock.tick(r.warmupMs); + expectRate(r, cnt / (2 * r.warmupMs)); + clock.tick(r.warmupMs); + expectRate(r, cnt / (3 * r.warmupMs)); + clock.tick(r.periodMs - 3 * r.warmupMs); + expectRate(r, 0); + expect(r.msSinceLastEvent).to.be.closeTo(r.periodMs, 5); }, - ) + ); } for (const events of [4, 32, 256, 1024]) { it( "calculates average rate for " + events + " events, and then decays", () => { - const period = r.periodMs + const period = r.periodMs; times(events, () => { - clock.tick(r.periodMs / events) - r.onEvent() - }) - const tickMs = r.periodMs / 4 - expectRate(r, events / period, 0.3) - clock.tick(tickMs) - expectRate(r, 0.75 * (events / period), 0.3) - clock.tick(tickMs) - expectRate(r, 0.5 * (events / period), 0.3) - clock.tick(tickMs) - expectRate(r, 0.25 * (events / period), 0.5) - clock.tick(tickMs) - expectRate(r, 0) + clock.tick(r.periodMs / events); + r.onEvent(); + }); + const tickMs = r.periodMs / 4; + expectRate(r, events / period, 0.3); + clock.tick(tickMs); + expectRate(r, 0.75 * (events / period), 0.3); + clock.tick(tickMs); + expectRate(r, 0.5 * (events / period), 0.3); + clock.tick(tickMs); + expectRate(r, 0.25 * (events / period), 0.5); + clock.tick(tickMs); + expectRate(r, 0); }, - ) + ); } -}) +}); diff --git a/src/Rate.ts b/src/Rate.ts index 3719208..dd99e53 100644 --- a/src/Rate.ts +++ b/src/Rate.ts @@ -1,4 +1,4 @@ -import { minuteMs, secondMs } from "./BatchClusterOptions" +import { minuteMs, secondMs } from "./BatchClusterOptions"; // Implementation notes: @@ -12,10 +12,10 @@ import { minuteMs, secondMs } from "./BatchClusterOptions" // a large periodMs. export class Rate { - #start = Date.now() - readonly #priorEventTimestamps: number[] = [] - #lastEventTs: number | null = null - #eventCount = 0 + #start = Date.now(); + readonly #priorEventTimestamps: number[] = []; + #lastEventTs: number | null = null; + #eventCount = 0; /** * @param periodMs the length of time to retain event timestamps for computing @@ -29,57 +29,57 @@ export class Rate { ) {} onEvent(): void { - this.#eventCount++ - const now = Date.now() - this.#priorEventTimestamps.push(now) - this.#lastEventTs = now + this.#eventCount++; + const now = Date.now(); + this.#priorEventTimestamps.push(now); + this.#lastEventTs = now; } #vacuum() { - const expired = Date.now() - this.periodMs + const expired = Date.now() - this.periodMs; const firstValidIndex = this.#priorEventTimestamps.findIndex( (ea) => ea > expired, - ) - if (firstValidIndex === -1) this.#priorEventTimestamps.length = 0 + ); + if (firstValidIndex === -1) this.#priorEventTimestamps.length = 0; else if (firstValidIndex > 0) { - this.#priorEventTimestamps.splice(0, firstValidIndex) + this.#priorEventTimestamps.splice(0, firstValidIndex); } } get eventCount(): number { - return this.#eventCount + return this.#eventCount; } get msSinceLastEvent(): number | null { - return this.#lastEventTs == null ? null : Date.now() - this.#lastEventTs + return this.#lastEventTs == null ? null : Date.now() - this.#lastEventTs; } get msPerEvent(): number | null { - const msSinceStart = Date.now() - this.#start - if (this.#lastEventTs == null || msSinceStart < this.warmupMs) return null - this.#vacuum() - const events = this.#priorEventTimestamps.length - return events === 0 ? null : Math.min(this.periodMs, msSinceStart) / events + const msSinceStart = Date.now() - this.#start; + if (this.#lastEventTs == null || msSinceStart < this.warmupMs) return null; + this.#vacuum(); + const events = this.#priorEventTimestamps.length; + return events === 0 ? null : Math.min(this.periodMs, msSinceStart) / events; } get eventsPerMs(): number { - const mpe = this.msPerEvent - return mpe == null ? 0 : mpe < 1 ? 1 : 1 / mpe + const mpe = this.msPerEvent; + return mpe == null ? 0 : mpe < 1 ? 1 : 1 / mpe; } get eventsPerSecond(): number { - return this.eventsPerMs * secondMs + return this.eventsPerMs * secondMs; } get eventsPerMinute(): number { - return this.eventsPerMs * minuteMs + return this.eventsPerMs * minuteMs; } clear(): this { - this.#start = Date.now() - this.#priorEventTimestamps.length = 0 - this.#lastEventTs = null - this.#eventCount = 0 - return this + this.#start = Date.now(); + this.#priorEventTimestamps.length = 0; + this.#lastEventTs = null; + this.#eventCount = 0; + return this; } } diff --git a/src/Stream.ts b/src/Stream.ts index 498eafb..352dfb1 100644 --- a/src/Stream.ts +++ b/src/Stream.ts @@ -1,12 +1,12 @@ -import { Readable, Writable } from "node:stream" +import { Readable, Writable } from "node:stream"; export function destroy(stream: Readable | Writable | null) { try { // .end() may result in an EPIPE when the child process exits. We don't // care. We just want to make sure the stream is closed. - stream?.removeAllListeners("error") + stream?.removeAllListeners("error"); // It's fine to call .destroy() on a stream that's already destroyed. - stream?.destroy?.() + stream?.destroy?.(); } catch { // don't care } diff --git a/src/StreamHandler.spec.ts b/src/StreamHandler.spec.ts index 0d1230a..f8647c2 100644 --- a/src/StreamHandler.spec.ts +++ b/src/StreamHandler.spec.ts @@ -1,32 +1,32 @@ -import child_process from "node:child_process" -import events from "node:events" -import { expect, processFactory } from "./_chai.spec" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { logger } from "./Logger" +import child_process from "node:child_process"; +import events from "node:events"; +import { expect, processFactory } from "./_chai.spec"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { logger } from "./Logger"; import { StreamContext, StreamHandler, StreamHandlerOptions, -} from "./StreamHandler" -import { Task } from "./Task" +} from "./StreamHandler"; +import { Task } from "./Task"; describe("StreamHandler", function () { - let streamHandler: StreamHandler - let emitter: BatchClusterEmitter - let mockContext: StreamContext - let onErrorCalls: { reason: string; error: Error }[] = [] - let endCalls: { gracefully: boolean; reason: string }[] = [] + let streamHandler: StreamHandler; + let emitter: BatchClusterEmitter; + let mockContext: StreamContext; + let onErrorCalls: { reason: string; error: Error }[] = []; + let endCalls: { gracefully: boolean; reason: string }[] = []; const options: StreamHandlerOptions = { logger, - } + }; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter - streamHandler = new StreamHandler(options, emitter) + emitter = new events.EventEmitter() as BatchClusterEmitter; + streamHandler = new StreamHandler(options, emitter); - onErrorCalls = [] - endCalls = [] + onErrorCalls = []; + endCalls = []; // Create a mock context that simulates BatchProcess behavior mockContext = { @@ -34,56 +34,56 @@ describe("StreamHandler", function () { isEnding: () => false, getCurrentTask: () => undefined, onError: (reason: string, error: Error) => { - onErrorCalls.push({ reason, error }) + onErrorCalls.push({ reason, error }); }, end: (gracefully: boolean, reason: string) => { - endCalls.push({ gracefully, reason }) + endCalls.push({ gracefully, reason }); }, - } - }) + }; + }); describe("initial state", function () { it("should initialize correctly", function () { - expect(streamHandler).to.not.be.undefined + expect(streamHandler).to.not.be.undefined; - const stats = streamHandler.getStats() - expect(stats.handlerActive).to.be.true - expect(stats.emitterConnected).to.be.true - }) - }) + const stats = streamHandler.getStats(); + expect(stats.handlerActive).to.be.true; + expect(stats.emitterConnected).to.be.true; + }); + }); describe("stream setup", function () { - let mockProcess: child_process.ChildProcess + let mockProcess: child_process.ChildProcess; beforeEach(async function () { // Create a real process for testing stream setup - mockProcess = await processFactory() - }) + mockProcess = await processFactory(); + }); afterEach(function () { if (mockProcess && !mockProcess.killed) { - mockProcess.kill() + mockProcess.kill(); } - }) + }); it("should set up stream listeners on a child process", function () { expect(() => { - streamHandler.setupStreamListeners(mockProcess, mockContext) - }).to.not.throw() + streamHandler.setupStreamListeners(mockProcess, mockContext); + }).to.not.throw(); // Verify streams exist - expect(mockProcess.stdin).to.not.be.null - expect(mockProcess.stdout).to.not.be.null - expect(mockProcess.stderr).to.not.be.null - }) + expect(mockProcess.stdin).to.not.be.null; + expect(mockProcess.stdout).to.not.be.null; + expect(mockProcess.stderr).to.not.be.null; + }); it("should throw error if stdin is missing", function () { - const invalidProcess = { stdin: null } as child_process.ChildProcess + const invalidProcess = { stdin: null } as child_process.ChildProcess; expect(() => { - streamHandler.setupStreamListeners(invalidProcess, mockContext) - }).to.throw("Given proc had no stdin") - }) + streamHandler.setupStreamListeners(invalidProcess, mockContext); + }).to.throw("Given proc had no stdin"); + }); it("should throw error if stdout is missing", function () { const invalidProcess = { @@ -93,31 +93,31 @@ describe("StreamHandler", function () { }, }, // Mock stdin with on method stdout: null, - } as any as child_process.ChildProcess + } as any as child_process.ChildProcess; expect(() => { - streamHandler.setupStreamListeners(invalidProcess, mockContext) - }).to.throw("Given proc had no stdout") - }) - }) + streamHandler.setupStreamListeners(invalidProcess, mockContext); + }).to.throw("Given proc had no stdout"); + }); + }); describe("stdout processing", function () { - let mockTask: Task - let taskDataEvents: { data: any; task: any; context: any }[] = [] - let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = [] + let mockTask: Task; + let taskDataEvents: { data: any; task: any; context: any }[] = []; + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []; beforeEach(function () { - taskDataEvents = [] - noTaskDataEvents = [] + taskDataEvents = []; + noTaskDataEvents = []; // Set up event listeners emitter.on("taskData", (data, task, context) => { - taskDataEvents.push({ data, task, context }) - }) + taskDataEvents.push({ data, task, context }); + }); emitter.on("noTaskData", (stdout, stderr, context) => { - noTaskDataEvents.push({ stdout, stderr, context }) - }) + noTaskDataEvents.push({ stdout, stderr, context }); + }); // Create a mock task mockTask = { @@ -125,73 +125,73 @@ describe("StreamHandler", function () { onStdout: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should process stdout data with active task", function () { - mockContext.getCurrentTask = () => mockTask - const testData = "test output" + mockContext.getCurrentTask = () => mockTask; + const testData = "test output"; - streamHandler.processStdout(testData, mockContext) + streamHandler.processStdout(testData, mockContext); - expect(taskDataEvents).to.have.length(1) - expect(taskDataEvents[0]?.data).to.eql(testData) - expect(taskDataEvents[0]?.task).to.eql(mockTask) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(taskDataEvents).to.have.length(1); + expect(taskDataEvents[0]?.data).to.eql(testData); + expect(taskDataEvents[0]?.task).to.eql(mockTask); + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should ignore stdout data when process is ending", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => true - const testData = "test output" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => true; + const testData = "test output"; - streamHandler.processStdout(testData, mockContext) + streamHandler.processStdout(testData, mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(taskDataEvents).to.have.length(0); + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should emit noTaskData and end process for stdout without task", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => false - const testData = "unexpected output" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; + const testData = "unexpected output"; - streamHandler.processStdout(testData, mockContext) + streamHandler.processStdout(testData, mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(1) - expect(noTaskDataEvents[0]?.stdout).to.eql(testData) - expect(noTaskDataEvents[0]?.stderr).to.be.null - expect(endCalls).to.have.length(1) - expect(endCalls[0]?.gracefully).to.be.false - expect(endCalls[0]?.reason).to.eql("stdout.error") - }) + expect(taskDataEvents).to.have.length(0); + expect(noTaskDataEvents).to.have.length(1); + expect(noTaskDataEvents[0]?.stdout).to.eql(testData); + expect(noTaskDataEvents[0]?.stderr).to.be.null; + expect(endCalls).to.have.length(1); + expect(endCalls[0]?.gracefully).to.be.false; + expect(endCalls[0]?.reason).to.eql("stdout.error"); + }); it("should ignore blank stdout data", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => false + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; - streamHandler.processStdout("", mockContext) - streamHandler.processStdout(" ", mockContext) - streamHandler.processStdout("\n", mockContext) + streamHandler.processStdout("", mockContext); + streamHandler.processStdout(" ", mockContext); + streamHandler.processStdout("\n", mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(taskDataEvents).to.have.length(0); + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should handle null stdout data", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => false + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; - streamHandler.processStdout(null as any, mockContext) + streamHandler.processStdout(null as any, mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(taskDataEvents).to.have.length(0); + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should not process stdout when task is not pending", function () { const nonPendingTask = { @@ -199,32 +199,32 @@ describe("StreamHandler", function () { onStdout: () => { /* mock implementation */ }, - } as unknown as Task + } as unknown as Task; - mockContext.getCurrentTask = () => nonPendingTask - mockContext.isEnding = () => false - const testData = "test output" + mockContext.getCurrentTask = () => nonPendingTask; + mockContext.isEnding = () => false; + const testData = "test output"; - streamHandler.processStdout(testData, mockContext) + streamHandler.processStdout(testData, mockContext); - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(1) - expect(endCalls).to.have.length(1) - expect(endCalls[0]?.reason).to.eql("stdout.error") - }) - }) + expect(taskDataEvents).to.have.length(0); + expect(noTaskDataEvents).to.have.length(1); + expect(endCalls).to.have.length(1); + expect(endCalls[0]?.reason).to.eql("stdout.error"); + }); + }); describe("stderr processing", function () { - let mockTask: Task - let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = [] + let mockTask: Task; + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []; beforeEach(function () { - noTaskDataEvents = [] + noTaskDataEvents = []; // Set up event listeners emitter.on("noTaskData", (stdout, stderr, context) => { - noTaskDataEvents.push({ stdout, stderr, context }) - }) + noTaskDataEvents.push({ stdout, stderr, context }); + }); // Create a mock task mockTask = { @@ -232,56 +232,56 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should process stderr data with active task", function () { - mockContext.getCurrentTask = () => mockTask - const testData = "error output" + mockContext.getCurrentTask = () => mockTask; + const testData = "error output"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should ignore stderr data when process is ending", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => true - const testData = "error output" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => true; + const testData = "error output"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should emit noTaskData and end process for stderr without task", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => false - const testData = "unexpected error" + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; + const testData = "unexpected error"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(1) - expect(noTaskDataEvents[0]?.stdout).to.be.null - expect(noTaskDataEvents[0]?.stderr).to.eql(testData) - expect(endCalls).to.have.length(1) - expect(endCalls[0]?.gracefully).to.be.false - expect(endCalls[0]?.reason).to.eql("stderr") - }) + expect(noTaskDataEvents).to.have.length(1); + expect(noTaskDataEvents[0]?.stdout).to.be.null; + expect(noTaskDataEvents[0]?.stderr).to.eql(testData); + expect(endCalls).to.have.length(1); + expect(endCalls[0]?.gracefully).to.be.false; + expect(endCalls[0]?.reason).to.eql("stderr"); + }); it("should ignore blank stderr data", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => false + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => false; - streamHandler.processStderr("", mockContext) - streamHandler.processStderr(" ", mockContext) - streamHandler.processStderr("\n", mockContext) + streamHandler.processStderr("", mockContext); + streamHandler.processStderr(" ", mockContext); + streamHandler.processStderr("\n", mockContext); - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should not process stderr when task is not pending", function () { const nonPendingTask = { @@ -289,54 +289,54 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task + } as unknown as Task; - mockContext.getCurrentTask = () => nonPendingTask - mockContext.isEnding = () => false - const testData = "error output" + mockContext.getCurrentTask = () => nonPendingTask; + mockContext.isEnding = () => false; + const testData = "error output"; - streamHandler.processStderr(testData, mockContext) + streamHandler.processStderr(testData, mockContext); - expect(noTaskDataEvents).to.have.length(1) - expect(endCalls).to.have.length(1) - expect(endCalls[0]?.reason).to.eql("stderr") - }) - }) + expect(noTaskDataEvents).to.have.length(1); + expect(endCalls).to.have.length(1); + expect(endCalls[0]?.reason).to.eql("stderr"); + }); + }); describe("utility methods", function () { it("should correctly identify blank data", function () { - expect(streamHandler.isBlankData("")).to.be.true - expect(streamHandler.isBlankData(" ")).to.be.true - expect(streamHandler.isBlankData("\n")).to.be.true - expect(streamHandler.isBlankData("\t")).to.be.true - expect(streamHandler.isBlankData(null)).to.be.true - expect(streamHandler.isBlankData(undefined)).to.be.true - - expect(streamHandler.isBlankData("text")).to.be.false - expect(streamHandler.isBlankData(" text ")).to.be.false - expect(streamHandler.isBlankData(Buffer.from("data"))).to.be.false - }) + expect(streamHandler.isBlankData("")).to.be.true; + expect(streamHandler.isBlankData(" ")).to.be.true; + expect(streamHandler.isBlankData("\n")).to.be.true; + expect(streamHandler.isBlankData("\t")).to.be.true; + expect(streamHandler.isBlankData(null)).to.be.true; + expect(streamHandler.isBlankData(undefined)).to.be.true; + + expect(streamHandler.isBlankData("text")).to.be.false; + expect(streamHandler.isBlankData(" text ")).to.be.false; + expect(streamHandler.isBlankData(Buffer.from("data"))).to.be.false; + }); it("should provide handler statistics", function () { - const stats = streamHandler.getStats() + const stats = streamHandler.getStats(); - expect(stats).to.have.property("handlerActive") - expect(stats).to.have.property("emitterConnected") - expect(stats.handlerActive).to.be.true - expect(stats.emitterConnected).to.be.true - }) - }) + expect(stats).to.have.property("handlerActive"); + expect(stats).to.have.property("emitterConnected"); + expect(stats.handlerActive).to.be.true; + expect(stats.emitterConnected).to.be.true; + }); + }); describe("buffer handling", function () { - let mockTask: Task - let taskDataEvents: { data: any; task: any; context: any }[] = [] + let mockTask: Task; + let taskDataEvents: { data: any; task: any; context: any }[] = []; beforeEach(function () { - taskDataEvents = [] + taskDataEvents = []; emitter.on("taskData", (data, task, context) => { - taskDataEvents.push({ data, task, context }) - }) + taskDataEvents.push({ data, task, context }); + }); mockTask = { pending: true, @@ -346,46 +346,46 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should handle Buffer data in stdout", function () { - mockContext.getCurrentTask = () => mockTask - const bufferData = Buffer.from("test buffer data") + mockContext.getCurrentTask = () => mockTask; + const bufferData = Buffer.from("test buffer data"); - streamHandler.processStdout(bufferData, mockContext) + streamHandler.processStdout(bufferData, mockContext); - expect(taskDataEvents).to.have.length(1) - expect(taskDataEvents[0]?.data).to.eql(bufferData) - }) + expect(taskDataEvents).to.have.length(1); + expect(taskDataEvents[0]?.data).to.eql(bufferData); + }); it("should handle Buffer data in stderr", function () { - mockContext.getCurrentTask = () => mockTask - const bufferData = Buffer.from("error buffer data") + mockContext.getCurrentTask = () => mockTask; + const bufferData = Buffer.from("error buffer data"); // Should not throw and should process normally expect(() => { - streamHandler.processStderr(bufferData, mockContext) - }).to.not.throw() - }) - }) + streamHandler.processStderr(bufferData, mockContext); + }).to.not.throw(); + }); + }); describe("integration scenarios", function () { - let mockTask: Task - let taskDataEvents: { data: any; task: any; context: any }[] = [] - let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = [] + let mockTask: Task; + let taskDataEvents: { data: any; task: any; context: any }[] = []; + let noTaskDataEvents: { stdout: any; stderr: any; context: any }[] = []; beforeEach(function () { - taskDataEvents = [] - noTaskDataEvents = [] + taskDataEvents = []; + noTaskDataEvents = []; emitter.on("taskData", (data, task, context) => { - taskDataEvents.push({ data, task, context }) - }) + taskDataEvents.push({ data, task, context }); + }); emitter.on("noTaskData", (stdout, stderr, context) => { - noTaskDataEvents.push({ stdout, stderr, context }) - }) + noTaskDataEvents.push({ stdout, stderr, context }); + }); mockTask = { pending: true, @@ -395,47 +395,47 @@ describe("StreamHandler", function () { onStderr: () => { /* mock implementation */ }, - } as unknown as Task - }) + } as unknown as Task; + }); it("should handle mixed stdout and stderr with active task", function () { - mockContext.getCurrentTask = () => mockTask + mockContext.getCurrentTask = () => mockTask; - streamHandler.processStdout("stdout data", mockContext) - streamHandler.processStderr("stderr data", mockContext) + streamHandler.processStdout("stdout data", mockContext); + streamHandler.processStderr("stderr data", mockContext); - expect(taskDataEvents).to.have.length(1) - expect(taskDataEvents[0]?.data).to.eql("stdout data") - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) + expect(taskDataEvents).to.have.length(1); + expect(taskDataEvents[0]?.data).to.eql("stdout data"); + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); it("should handle task completion scenario", function () { // Start with active task - mockContext.getCurrentTask = () => mockTask - streamHandler.processStdout("initial output", mockContext) + mockContext.getCurrentTask = () => mockTask; + streamHandler.processStdout("initial output", mockContext); - expect(taskDataEvents).to.have.length(1) + expect(taskDataEvents).to.have.length(1); // Task completes, no current task - mockContext.getCurrentTask = () => undefined - streamHandler.processStdout("stray output", mockContext) + mockContext.getCurrentTask = () => undefined; + streamHandler.processStdout("stray output", mockContext); - expect(noTaskDataEvents).to.have.length(1) - expect(endCalls).to.have.length(1) - expect(endCalls[0]?.reason).to.eql("stdout.error") - }) + expect(noTaskDataEvents).to.have.length(1); + expect(endCalls).to.have.length(1); + expect(endCalls[0]?.reason).to.eql("stdout.error"); + }); it("should handle process ending scenario", function () { - mockContext.getCurrentTask = () => undefined - mockContext.isEnding = () => true - - streamHandler.processStdout("final output", mockContext) - streamHandler.processStderr("final error", mockContext) - - expect(taskDataEvents).to.have.length(0) - expect(noTaskDataEvents).to.have.length(0) - expect(endCalls).to.have.length(0) - }) - }) -}) + mockContext.getCurrentTask = () => undefined; + mockContext.isEnding = () => true; + + streamHandler.processStdout("final output", mockContext); + streamHandler.processStderr("final error", mockContext); + + expect(taskDataEvents).to.have.length(0); + expect(noTaskDataEvents).to.have.length(0); + expect(endCalls).to.have.length(0); + }); + }); +}); diff --git a/src/StreamHandler.ts b/src/StreamHandler.ts index 40e9fdd..7e84029 100644 --- a/src/StreamHandler.ts +++ b/src/StreamHandler.ts @@ -1,26 +1,26 @@ -import child_process from "node:child_process" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { Logger } from "./Logger" -import { map } from "./Object" -import { blank } from "./String" -import { Task } from "./Task" +import child_process from "node:child_process"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { Logger } from "./Logger"; +import { map } from "./Object"; +import { blank } from "./String"; +import { Task } from "./Task"; /** * Configuration for stream handling behavior */ export interface StreamHandlerOptions { - readonly logger: () => Logger + readonly logger: () => Logger; } /** * Interface for objects that can provide stream context */ export interface StreamContext { - readonly name: string - isEnding(): boolean - getCurrentTask(): Task | undefined - onError: (reason: string, error: Error) => void - end: (gracefully: boolean, reason: string) => void + readonly name: string; + isEnding(): boolean; + getCurrentTask(): Task | undefined; + onError: (reason: string, error: Error) => void; + end: (gracefully: boolean, reason: string) => void; } /** @@ -28,13 +28,13 @@ export interface StreamContext { * Manages stream event listeners, data routing, and error handling. */ export class StreamHandler { - readonly #logger: () => Logger + readonly #logger: () => Logger; constructor( options: StreamHandlerOptions, private readonly emitter: BatchClusterEmitter, ) { - this.#logger = options.logger + this.#logger = options.logger; } /** @@ -44,40 +44,40 @@ export class StreamHandler { proc: child_process.ChildProcess, context: StreamContext, ): void { - const stdin = proc.stdin - if (stdin == null) throw new Error("Given proc had no stdin") - stdin.on("error", (err) => context.onError("stdin.error", err)) + const stdin = proc.stdin; + if (stdin == null) throw new Error("Given proc had no stdin"); + stdin.on("error", (err) => context.onError("stdin.error", err)); - const stdout = proc.stdout - if (stdout == null) throw new Error("Given proc had no stdout") - stdout.on("error", (err) => context.onError("stdout.error", err)) - stdout.on("data", (data: string | Buffer) => this.#onStdout(data, context)) + const stdout = proc.stdout; + if (stdout == null) throw new Error("Given proc had no stdout"); + stdout.on("error", (err) => context.onError("stdout.error", err)); + stdout.on("data", (data: string | Buffer) => this.#onStdout(data, context)); map(proc.stderr, (stderr) => { - stderr.on("error", (err) => context.onError("stderr.error", err)) + stderr.on("error", (err) => context.onError("stderr.error", err)); stderr.on("data", (data: string | Buffer) => this.#onStderr(data, context), - ) - }) + ); + }); } /** * Handle stdout data from a child process */ #onStdout(data: string | Buffer, context: StreamContext): void { - if (data == null) return + if (data == null) return; - const task = context.getCurrentTask() + const task = context.getCurrentTask(); if (task != null && task.pending) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument - this.emitter.emit("taskData", data, task, context as any) - task.onStdout(data) + this.emitter.emit("taskData", data, task, context as any); + task.onStdout(data); } else if (context.isEnding()) { // don't care if we're already being shut down. } else if (!blank(data)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument - this.emitter.emit("noTaskData", data, null, context as any) - context.end(false, "stdout.error") + this.emitter.emit("noTaskData", data, null, context as any); + context.end(false, "stdout.error"); } } @@ -85,18 +85,18 @@ export class StreamHandler { * Handle stderr data from a child process */ #onStderr(data: string | Buffer, context: StreamContext): void { - if (blank(data)) return + if (blank(data)) return; - this.#logger().warn(context.name + ".onStderr(): " + String(data)) + this.#logger().warn(context.name + ".onStderr(): " + String(data)); - const task = context.getCurrentTask() + const task = context.getCurrentTask(); if (task != null && task.pending) { - task.onStderr(data) + task.onStderr(data); } else if (!context.isEnding()) { // If we're ending and there isn't a task, don't worry about it. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument - this.emitter.emit("noTaskData", null, data, context as any) - context.end(false, "stderr") + this.emitter.emit("noTaskData", null, data, context as any); + context.end(false, "stderr"); } } @@ -104,21 +104,21 @@ export class StreamHandler { * Process stdout data directly (for testing or manual processing) */ processStdout(data: string | Buffer, context: StreamContext): void { - this.#onStdout(data, context) + this.#onStdout(data, context); } /** * Process stderr data directly (for testing or manual processing) */ processStderr(data: string | Buffer, context: StreamContext): void { - this.#onStderr(data, context) + this.#onStderr(data, context); } /** * Check if data is considered blank/empty */ isBlankData(data: string | Buffer | null | undefined): boolean { - return blank(data) + return blank(data); } /** @@ -128,6 +128,6 @@ export class StreamHandler { return { handlerActive: true, emitterConnected: this.emitter != null, - } + }; } } diff --git a/src/String.ts b/src/String.ts index 6c9b2d1..63e4111 100644 --- a/src/String.ts +++ b/src/String.ts @@ -1,21 +1,21 @@ export function blank(s: unknown): boolean { - return s == null || toS(s).trim().length === 0 + return s == null || toS(s).trim().length === 0; } export function notBlank(s: unknown): boolean { - return !blank(s) + return !blank(s); } export function toNotBlank(s: unknown): string | undefined { - const result = toS(s).trim() - return result.length === 0 ? undefined : result + const result = toS(s).trim(); + return result.length === 0 ? undefined : result; } export function ensureSuffix(s: string, suffix: string): string { - return s.endsWith(suffix) ? s : s + suffix + return s.endsWith(suffix) ? s : s + suffix; } export function toS(s: unknown): string { /* eslint-disable-next-line @typescript-eslint/no-base-to-string */ - return s == null ? "" : s.toString() + return s == null ? "" : s.toString(); } diff --git a/src/Task.ts b/src/Task.ts index 388eed3..f104454 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -1,14 +1,14 @@ -import { delay } from "./Async" -import { Deferred } from "./Deferred" -import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions" -import { Parser } from "./Parser" +import { delay } from "./Async"; +import { Deferred } from "./Deferred"; +import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; +import { Parser } from "./Parser"; export type TaskOptions = Pick< InternalBatchProcessOptions, "streamFlushMillis" | "observer" | "passRE" | "failRE" | "logger" -> +>; -let _taskId = 1 +let _taskId = 1; /** * Tasks embody individual jobs given to the underlying child processes. Each @@ -16,14 +16,14 @@ let _taskId = 1 * result of the task. */ export class Task { - readonly taskId = _taskId++ - #opts?: TaskOptions - #startedAt?: number - #parsing = false - #settledAt?: number - readonly #d = new Deferred() - #stdout = "" - #stderr = "" + readonly taskId = _taskId++; + #opts?: TaskOptions; + #startedAt?: number; + #parsing = false; + #settledAt?: number; + readonly #d = new Deferred(); + #stdout = ""; + #stderr = ""; /** * @param {string} command is the value written to stdin to perform the given @@ -40,18 +40,18 @@ export class Task { this.#d.promise.then( () => this.#onSettle(), () => this.#onSettle(), - ) + ); } /** * @return the resolution or rejection of this task. */ get promise(): Promise { - return this.#d.promise + return this.#d.promise; } get pending(): boolean { - return this.#d.pending + return this.#d.pending; } get state(): string { @@ -59,18 +59,18 @@ export class Task { ? "pending" : this.#d.rejected ? "rejected" - : "resolved" + : "resolved"; } onStart(opts: TaskOptions) { - this.#opts = opts - this.#startedAt = Date.now() + this.#opts = opts; + this.#startedAt = Date.now(); } get runtimeMs() { return this.#startedAt == null ? undefined - : (this.#settledAt ?? Date.now()) - this.#startedAt + : (this.#settledAt ?? Date.now()) - this.#startedAt; } toString(): string { @@ -80,81 +80,81 @@ export class Task { this.command.replace(/\s+/gm, " ").slice(0, 80).trim() + ")#" + this.taskId - ) + ); } onStdout(buf: string | Buffer): void { - this.#stdout += buf.toString() - const passRE = this.#opts?.passRE + this.#stdout += buf.toString(); + const passRE = this.#opts?.passRE; if (passRE != null && passRE.exec(this.#stdout) != null) { // remove the pass token from stdout: - this.#stdout = this.#stdout.replace(passRE, "") - void this.#resolve(true) + this.#stdout = this.#stdout.replace(passRE, ""); + void this.#resolve(true); } else { - const failRE = this.#opts?.failRE + const failRE = this.#opts?.failRE; if (failRE != null && failRE.exec(this.#stdout) != null) { // remove the fail token from stdout: - this.#stdout = this.#stdout.replace(failRE, "") - void this.#resolve(false) + this.#stdout = this.#stdout.replace(failRE, ""); + void this.#resolve(false); } } } onStderr(buf: string | Buffer): void { - this.#stderr += buf.toString() - const failRE = this.#opts?.failRE + this.#stderr += buf.toString(); + const failRE = this.#opts?.failRE; if (failRE != null && failRE.exec(this.#stderr) != null) { // remove the fail token from stderr: - this.#stderr = this.#stderr.replace(failRE, "") - void this.#resolve(false) + this.#stderr = this.#stderr.replace(failRE, ""); + void this.#resolve(false); } } #onSettle() { - this.#settledAt ??= Date.now() + this.#settledAt ??= Date.now(); } /** * @return true if the wrapped promise was rejected */ reject(error: Error): boolean { - return this.#d.reject(error) + return this.#d.reject(error); } async #resolve(passed: boolean) { // fail always wins. - passed = !this.#d.rejected && passed + passed = !this.#d.rejected && passed; // wait for stderr and stdout to flush: - const flushMs = this.#opts?.streamFlushMillis ?? 0 + const flushMs = this.#opts?.streamFlushMillis ?? 0; if (flushMs > 0) { - await delay(flushMs) + await delay(flushMs); } // we're expecting this method may be called concurrently (if there are both // pass and fail tokens found in stderr and stdout), but we only want to run // this once, so - if (!this.pending || this.#parsing) return + if (!this.pending || this.#parsing) return; // this.#opts // ?.logger() // .trace("Task.#resolve()", { command: this.command, state: this.state }) // Prevent concurrent parsing: - this.#parsing = true + this.#parsing = true; try { - const parseResult = await this.parser(this.#stdout, this.#stderr, passed) + const parseResult = await this.parser(this.#stdout, this.#stderr, passed); if (this.#d.resolve(parseResult)) { // success } else { this.#opts?.observer.emit( "internalError", new Error(this.toString() + " ._resolved() more than once"), - ) + ); } } catch (error: unknown) { - this.reject(error instanceof Error ? error : new Error(String(error))) + this.reject(error instanceof Error ? error : new Error(String(error))); } } } diff --git a/src/TaskQueueManager.spec.ts b/src/TaskQueueManager.spec.ts index 2632760..664dff8 100644 --- a/src/TaskQueueManager.spec.ts +++ b/src/TaskQueueManager.spec.ts @@ -1,19 +1,19 @@ -import events from "node:events" -import { expect, parser } from "./_chai.spec" -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { BatchProcess } from "./BatchProcess" -import { logger } from "./Logger" -import { Task } from "./Task" -import { TaskQueueManager } from "./TaskQueueManager" +import events from "node:events"; +import { expect, parser } from "./_chai.spec"; +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { BatchProcess } from "./BatchProcess"; +import { logger } from "./Logger"; +import { Task } from "./Task"; +import { TaskQueueManager } from "./TaskQueueManager"; describe("TaskQueueManager", function () { - let queueManager: TaskQueueManager - let emitter: BatchClusterEmitter - let mockProcess: BatchProcess + let queueManager: TaskQueueManager; + let emitter: BatchClusterEmitter; + let mockProcess: BatchProcess; beforeEach(function () { - emitter = new events.EventEmitter() as BatchClusterEmitter - queueManager = new TaskQueueManager(logger, emitter) + emitter = new events.EventEmitter() as BatchClusterEmitter; + queueManager = new TaskQueueManager(logger, emitter); // Create a mock process that can execute tasks mockProcess = { @@ -21,243 +21,243 @@ describe("TaskQueueManager", function () { idle: true, pid: 12345, execTask: () => true, // Always succeed - } as unknown as BatchProcess - }) + } as unknown as BatchProcess; + }); describe("initial state", function () { it("should start with empty queue", function () { - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - expect(queueManager.pendingTasks).to.eql([]) - }) + expect(queueManager.pendingTaskCount).to.eql(0); + expect(queueManager.isEmpty).to.be.true; + expect(queueManager.pendingTasks).to.eql([]); + }); it("should return empty queue stats", function () { - const stats = queueManager.getQueueStats() - expect(stats.pendingTaskCount).to.eql(0) - expect(stats.isEmpty).to.be.true - }) - }) + const stats = queueManager.getQueueStats(); + expect(stats.pendingTaskCount).to.eql(0); + expect(stats.isEmpty).to.be.true; + }); + }); describe("task enqueuing", function () { it("should enqueue tasks when not ended", function () { - const task = new Task("test command", parser) - const promise = queueManager.enqueueTask(task, false) + const task = new Task("test command", parser); + const promise = queueManager.enqueueTask(task, false); - expect(queueManager.pendingTaskCount).to.eql(1) - expect(queueManager.isEmpty).to.be.false - expect(queueManager.pendingTasks).to.have.length(1) - expect(queueManager.pendingTasks[0]).to.eql(task) - expect(promise).to.equal(task.promise) - }) + expect(queueManager.pendingTaskCount).to.eql(1); + expect(queueManager.isEmpty).to.be.false; + expect(queueManager.pendingTasks).to.have.length(1); + expect(queueManager.pendingTasks[0]).to.eql(task); + expect(promise).to.equal(task.promise); + }); it("should reject tasks when ended", function () { - const task = new Task("test command", parser) - const promise = queueManager.enqueueTask(task, true) + const task = new Task("test command", parser); + const promise = queueManager.enqueueTask(task, true); - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - expect(promise).to.equal(task.promise) - expect(task.pending).to.be.false - }) + expect(queueManager.pendingTaskCount).to.eql(0); + expect(queueManager.isEmpty).to.be.true; + expect(promise).to.equal(task.promise); + expect(task.pending).to.be.false; + }); it("should handle multiple tasks", function () { - const task1 = new Task("command 1", parser) - const task2 = new Task("command 2", parser) - const task3 = new Task("command 3", parser) + const task1 = new Task("command 1", parser); + const task2 = new Task("command 2", parser); + const task3 = new Task("command 3", parser); - queueManager.enqueueTask(task1, false) - queueManager.enqueueTask(task2, false) - queueManager.enqueueTask(task3, false) + queueManager.enqueueTask(task1, false); + queueManager.enqueueTask(task2, false); + queueManager.enqueueTask(task3, false); - expect(queueManager.pendingTaskCount).to.eql(3) - expect(queueManager.pendingTasks).to.have.length(3) - }) - }) + expect(queueManager.pendingTaskCount).to.eql(3); + expect(queueManager.pendingTasks).to.have.length(3); + }); + }); describe("task assignment", function () { - let task: Task + let task: Task; beforeEach(function () { - task = new Task("test command", parser) - queueManager.enqueueTask(task, false) - }) + task = new Task("test command", parser); + queueManager.enqueueTask(task, false); + }); it("should assign task to ready process", function () { - const result = queueManager.tryAssignNextTask(mockProcess) + const result = queueManager.tryAssignNextTask(mockProcess); - expect(result).to.be.true - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - }) + expect(result).to.be.true; + expect(queueManager.pendingTaskCount).to.eql(0); + expect(queueManager.isEmpty).to.be.true; + }); it("should not assign task when no ready process", function () { - const result = queueManager.tryAssignNextTask(undefined) + const result = queueManager.tryAssignNextTask(undefined); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(1) - expect(queueManager.isEmpty).to.be.false - }) + expect(result).to.be.false; + expect(queueManager.pendingTaskCount).to.eql(1); + expect(queueManager.isEmpty).to.be.false; + }); it("should retry when process cannot execute task", function () { const failingProcess = { ...mockProcess, execTask: () => false, // Always fail - } as unknown as BatchProcess + } as unknown as BatchProcess; - const result = queueManager.tryAssignNextTask(failingProcess) + const result = queueManager.tryAssignNextTask(failingProcess); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(1) // Task should be re-queued - }) + expect(result).to.be.false; + expect(queueManager.pendingTaskCount).to.eql(1); // Task should be re-queued + }); it("should stop retrying after max retries", function () { const failingProcess = { ...mockProcess, execTask: () => false, - } as unknown as BatchProcess + } as unknown as BatchProcess; - const result = queueManager.tryAssignNextTask(failingProcess, 0) + const result = queueManager.tryAssignNextTask(failingProcess, 0); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(1) // Task remains when retries exhausted - }) + expect(result).to.be.false; + expect(queueManager.pendingTaskCount).to.eql(1); // Task remains when retries exhausted + }); it("should handle empty queue gracefully", function () { // Clear the queue first - queueManager.clearAllTasks() + queueManager.clearAllTasks(); - const result = queueManager.tryAssignNextTask(mockProcess) + const result = queueManager.tryAssignNextTask(mockProcess); - expect(result).to.be.false - expect(queueManager.pendingTaskCount).to.eql(0) - }) - }) + expect(result).to.be.false; + expect(queueManager.pendingTaskCount).to.eql(0); + }); + }); describe("queue processing", function () { beforeEach(function () { // Add multiple tasks for (let i = 0; i < 5; i++) { - const task = new Task(`command ${i}`, parser) - queueManager.enqueueTask(task, false) + const task = new Task(`command ${i}`, parser); + queueManager.enqueueTask(task, false); } - }) + }); it("should process all tasks when process is always ready", function () { - const findReadyProcess = () => mockProcess - const assignedCount = queueManager.processQueue(findReadyProcess) + const findReadyProcess = () => mockProcess; + const assignedCount = queueManager.processQueue(findReadyProcess); - expect(assignedCount).to.eql(5) - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - }) + expect(assignedCount).to.eql(5); + expect(queueManager.pendingTaskCount).to.eql(0); + expect(queueManager.isEmpty).to.be.true; + }); it("should stop processing when no ready process available", function () { - const findReadyProcess = () => undefined - const assignedCount = queueManager.processQueue(findReadyProcess) + const findReadyProcess = () => undefined; + const assignedCount = queueManager.processQueue(findReadyProcess); - expect(assignedCount).to.eql(0) - expect(queueManager.pendingTaskCount).to.eql(5) - expect(queueManager.isEmpty).to.be.false - }) + expect(assignedCount).to.eql(0); + expect(queueManager.pendingTaskCount).to.eql(5); + expect(queueManager.isEmpty).to.be.false; + }); it("should partially process queue when process becomes unavailable", function () { - let callCount = 0 + let callCount = 0; const findReadyProcess = () => { - callCount++ - return callCount <= 3 ? mockProcess : undefined - } + callCount++; + return callCount <= 3 ? mockProcess : undefined; + }; - const assignedCount = queueManager.processQueue(findReadyProcess) + const assignedCount = queueManager.processQueue(findReadyProcess); - expect(assignedCount).to.eql(3) - expect(queueManager.pendingTaskCount).to.eql(2) - }) + expect(assignedCount).to.eql(3); + expect(queueManager.pendingTaskCount).to.eql(2); + }); it("should handle process that fails to execute tasks", function () { const failingProcess = { ...mockProcess, execTask: () => false, - } as unknown as BatchProcess + } as unknown as BatchProcess; - const findReadyProcess = () => failingProcess - const assignedCount = queueManager.processQueue(findReadyProcess) + const findReadyProcess = () => failingProcess; + const assignedCount = queueManager.processQueue(findReadyProcess); - expect(assignedCount).to.eql(0) - expect(queueManager.pendingTaskCount).to.be.greaterThan(0) // Tasks remain queued - }) - }) + expect(assignedCount).to.eql(0); + expect(queueManager.pendingTaskCount).to.be.greaterThan(0); // Tasks remain queued + }); + }); describe("queue management", function () { beforeEach(function () { // Add some tasks for (let i = 0; i < 3; i++) { - const task = new Task(`command ${i}`, parser) - queueManager.enqueueTask(task, false) + const task = new Task(`command ${i}`, parser); + queueManager.enqueueTask(task, false); } - }) + }); it("should clear all tasks", function () { - expect(queueManager.pendingTaskCount).to.eql(3) + expect(queueManager.pendingTaskCount).to.eql(3); - queueManager.clearAllTasks() + queueManager.clearAllTasks(); - expect(queueManager.pendingTaskCount).to.eql(0) - expect(queueManager.isEmpty).to.be.true - expect(queueManager.pendingTasks).to.eql([]) - }) + expect(queueManager.pendingTaskCount).to.eql(0); + expect(queueManager.isEmpty).to.be.true; + expect(queueManager.pendingTasks).to.eql([]); + }); it("should provide accurate queue statistics", function () { - const stats = queueManager.getQueueStats() + const stats = queueManager.getQueueStats(); - expect(stats.pendingTaskCount).to.eql(3) - expect(stats.isEmpty).to.be.false - }) - }) + expect(stats.pendingTaskCount).to.eql(3); + expect(stats.isEmpty).to.be.false; + }); + }); describe("error handling", function () { it("should handle concurrent access gracefully", function () { // Add a task - const task = new Task("test", parser) - queueManager.enqueueTask(task, false) + const task = new Task("test", parser); + queueManager.enqueueTask(task, false); // First process gets the task - const result1 = queueManager.tryAssignNextTask(mockProcess) - expect(result1).to.be.true + const result1 = queueManager.tryAssignNextTask(mockProcess); + expect(result1).to.be.true; // Second attempt on empty queue should return false - const result2 = queueManager.tryAssignNextTask(mockProcess) - expect(result2).to.be.false - expect(queueManager.pendingTaskCount).to.eql(0) - }) - }) + const result2 = queueManager.tryAssignNextTask(mockProcess); + expect(result2).to.be.false; + expect(queueManager.pendingTaskCount).to.eql(0); + }); + }); describe("FIFO ordering", function () { it("should process tasks in first-in-first-out order", function () { - const executedTasks: Task[] = [] + const executedTasks: Task[] = []; const trackingProcess = { ...mockProcess, execTask: (task: Task) => { - executedTasks.push(task) - return true + executedTasks.push(task); + return true; }, - } as unknown as BatchProcess + } as unknown as BatchProcess; // Enqueue tasks with identifiable commands - const task1 = new Task("first", parser) - const task2 = new Task("second", parser) - const task3 = new Task("third", parser) + const task1 = new Task("first", parser); + const task2 = new Task("second", parser); + const task3 = new Task("third", parser); - queueManager.enqueueTask(task1, false) - queueManager.enqueueTask(task2, false) - queueManager.enqueueTask(task3, false) + queueManager.enqueueTask(task1, false); + queueManager.enqueueTask(task2, false); + queueManager.enqueueTask(task3, false); // Process all tasks - queueManager.processQueue(() => trackingProcess) - - expect(executedTasks).to.have.length(3) - expect(executedTasks[0]?.command).to.eql("first") - expect(executedTasks[1]?.command).to.eql("second") - expect(executedTasks[2]?.command).to.eql("third") - }) - }) -}) + queueManager.processQueue(() => trackingProcess); + + expect(executedTasks).to.have.length(3); + expect(executedTasks[0]?.command).to.eql("first"); + expect(executedTasks[1]?.command).to.eql("second"); + expect(executedTasks[2]?.command).to.eql("third"); + }); + }); +}); diff --git a/src/TaskQueueManager.ts b/src/TaskQueueManager.ts index 3412e96..1f203fc 100644 --- a/src/TaskQueueManager.ts +++ b/src/TaskQueueManager.ts @@ -1,21 +1,21 @@ -import { BatchClusterEmitter } from "./BatchClusterEmitter" -import { BatchProcess } from "./BatchProcess" -import { Logger } from "./Logger" -import { Task } from "./Task" +import { BatchClusterEmitter } from "./BatchClusterEmitter"; +import { BatchProcess } from "./BatchProcess"; +import { Logger } from "./Logger"; +import { Task } from "./Task"; /** * Manages task queuing, scheduling, and assignment to ready processes. * Handles the task lifecycle from enqueue to assignment. */ export class TaskQueueManager { - readonly #tasks: Task[] = [] - readonly #logger: () => Logger + readonly #tasks: Task[] = []; + readonly #logger: () => Logger; constructor( logger: () => Logger, private readonly emitter?: BatchClusterEmitter, ) { - this.#logger = logger + this.#logger = logger; } /** @@ -25,39 +25,39 @@ export class TaskQueueManager { if (ended) { task.reject( new Error("BatchCluster has ended, cannot enqueue " + task.command), - ) + ); } else { - this.#tasks.push(task as Task) + this.#tasks.push(task as Task); } - return task.promise + return task.promise; } /** * Simple enqueue method (alias for enqueueTask without ended check) */ enqueue(task: Task): void { - this.#tasks.push(task) + this.#tasks.push(task); } /** * Get the number of pending tasks in the queue */ get pendingTaskCount(): number { - return this.#tasks.length + return this.#tasks.length; } /** * Get all pending tasks (mostly for testing) */ get pendingTasks(): readonly Task[] { - return this.#tasks + return this.#tasks; } /** * Check if the queue is empty */ get isEmpty(): boolean { - return this.#tasks.length === 0 + return this.#tasks.length === 0; } /** @@ -69,28 +69,28 @@ export class TaskQueueManager { retries = 1, ): boolean { if (this.#tasks.length === 0 || retries < 0) { - return false + return false; } // no procs are idle and healthy :( if (readyProcess == null) { - return false + return false; } - const task = this.#tasks.shift() + const task = this.#tasks.shift(); if (task == null) { - this.emitter?.emit("internalError", new Error("unexpected null task")) - return false + this.emitter?.emit("internalError", new Error("unexpected null task")); + return false; } - const submitted = readyProcess.execTask(task) + const submitted = readyProcess.execTask(task); if (!submitted) { // This isn't an internal error: the proc may have needed to run a health // check. Let's reschedule the task and try again: - this.#tasks.push(task) + this.#tasks.push(task); // We don't want to return false here (it'll stop the assignment loop) unless // we actually can't submit the task: - return this.tryAssignNextTask(readyProcess, retries - 1) + return this.tryAssignNextTask(readyProcess, retries - 1); } this.#logger().trace( @@ -99,9 +99,9 @@ export class TaskQueueManager { child_pid: readyProcess.pid, task, }, - ) + ); - return submitted + return submitted; } /** @@ -109,24 +109,24 @@ export class TaskQueueManager { * Returns the number of tasks successfully assigned. */ processQueue(findReadyProcess: () => BatchProcess | undefined): number { - let assignedCount = 0 + let assignedCount = 0; while (this.#tasks.length > 0) { - const readyProcess = findReadyProcess() + const readyProcess = findReadyProcess(); if (!this.tryAssignNextTask(readyProcess)) { - break + break; } - assignedCount++ + assignedCount++; } - return assignedCount + return assignedCount; } /** * Clear all pending tasks (used during shutdown) */ clearAllTasks(): void { - this.#tasks.length = 0 + this.#tasks.length = 0; } /** @@ -136,6 +136,6 @@ export class TaskQueueManager { return { pendingTaskCount: this.#tasks.length, isEmpty: this.isEmpty, - } + }; } } diff --git a/src/Timeout.ts b/src/Timeout.ts index 94a16c9..ea80bb0 100644 --- a/src/Timeout.ts +++ b/src/Timeout.ts @@ -1,5 +1,5 @@ -import timers from "node:timers" -export const Timeout = Symbol("timeout") +import timers from "node:timers"; +export const Timeout = Symbol("timeout"); export async function thenOrTimeout( p: Promise, @@ -10,29 +10,29 @@ export async function thenOrTimeout( return timeoutMs <= 1 ? p : new Promise((resolve, reject) => { - let pending = true + let pending = true; const t = timers.setTimeout(() => { if (pending) { - pending = false - resolve(Timeout) + pending = false; + resolve(Timeout); } - }, timeoutMs) + }, timeoutMs); p.then( (result) => { if (pending) { - pending = false - clearTimeout(t) - resolve(result) + pending = false; + clearTimeout(t); + resolve(result); } }, (err: unknown) => { if (pending) { - pending = false - clearTimeout(t) - reject(err instanceof Error ? err : new Error(String(err))) + pending = false; + clearTimeout(t); + reject(err instanceof Error ? err : new Error(String(err))); } }, - ) - }) + ); + }); } diff --git a/src/WhyNotHealthy.ts b/src/WhyNotHealthy.ts index 4bdb103..53f0d70 100644 --- a/src/WhyNotHealthy.ts +++ b/src/WhyNotHealthy.ts @@ -20,6 +20,6 @@ export type WhyNotHealthy = | "tooMany" // < only sent by BatchCluster when maxProcs is reduced | "startError" | "unhealthy" - | "worn" + | "worn"; -export type WhyNotReady = WhyNotHealthy | "busy" +export type WhyNotReady = WhyNotHealthy | "busy"; diff --git a/src/_chai.spec.ts b/src/_chai.spec.ts index 724d883..5cf5914 100644 --- a/src/_chai.spec.ts +++ b/src/_chai.spec.ts @@ -1,25 +1,25 @@ /* eslint-disable @typescript-eslint/no-require-imports */ try { - require("source-map-support").install() + require("source-map-support").install(); } catch { // } -import { expect, use } from "chai" -import child_process from "node:child_process" -import path from "node:path" -import process from "node:process" -import { Log, logger, setLogger } from "./Logger" -import { Parser } from "./Parser" -import { pidExists } from "./Pids" -import { notBlank } from "./String" +import { expect, use } from "chai"; +import child_process from "node:child_process"; +import path from "node:path"; +import process from "node:process"; +import { Log, logger, setLogger } from "./Logger"; +import { Parser } from "./Parser"; +import { pidExists } from "./Pids"; +import { notBlank } from "./String"; -use(require("chai-as-promised")) -use(require("chai-string")) -use(require("chai-subset")) -use(require("chai-withintoleranceof")) +use(require("chai-as-promised")); +use(require("chai-string")); +use(require("chai-subset")); +use(require("chai-withintoleranceof")); -export { expect } from "chai" +export { expect } from "chai"; // Tests should be quiet unless LOG is set to "trace" or "debug" or "info" or... setLogger( @@ -37,20 +37,20 @@ setLogger( ), ), ), -) +); -export const parserErrors: string[] = [] +export const parserErrors: string[] = []; -export const unhandledRejections: Error[] = [] +export const unhandledRejections: Error[] = []; -beforeEach(() => (parserErrors.length = 0)) +beforeEach(() => (parserErrors.length = 0)); process.on("unhandledRejection", (reason: any) => { - console.error("unhandledRejection:", reason.stack ?? reason) - unhandledRejections.push(reason) -}) + console.error("unhandledRejection:", reason.stack ?? reason); + unhandledRejections.push(reason); +}); -afterEach(() => expect(unhandledRejections).to.eql([])) +afterEach(() => expect(unhandledRejections).to.eql([])); export const parser: Parser = ( stdout: string, @@ -58,32 +58,32 @@ export const parser: Parser = ( passed: boolean, ) => { if (stderr != null) { - parserErrors.push(stderr) + parserErrors.push(stderr); } if (!passed || notBlank(stderr)) { logger().debug("test parser: rejecting task", { stdout, stderr, passed, - }) + }); // process.stdout.write("!") - throw new Error(stderr) + throw new Error(stderr); } else { const str = stdout .split(/(\r?\n)+/) .filter((ea) => notBlank(ea) && !ea.startsWith("# ")) .join("\n") - .trim() - logger().debug("test parser: resolving task", str) + .trim(); + logger().debug("test parser: resolving task", str); // process.stdout.write(".") - return str + return str; } -} +}; export function times(n: number, f: (idx: number) => T): T[] { return Array(n) .fill(undefined) - .map((_, i) => f(i)) + .map((_, i) => f(i)); } // because @types/chai-withintoleranceof isn't a thing (yet) @@ -92,37 +92,37 @@ type WithinTolerance = ( expected: number, tol: number | number[], message?: string, -) => Chai.Assertion +) => Chai.Assertion; // eslint-disable-next-line @typescript-eslint/no-namespace declare namespace Chai { interface Assertion { - withinToleranceOf: WithinTolerance - withinTolOf: WithinTolerance + withinToleranceOf: WithinTolerance; + withinTolOf: WithinTolerance; } } -export const childProcs: child_process.ChildProcess[] = [] +export const childProcs: child_process.ChildProcess[] = []; export function testPids(): number[] { return childProcs .map((proc) => proc.pid) - .filter((ea) => ea != null) as number[] + .filter((ea) => ea != null) as number[]; } export function currentTestPids(): number[] { - return testPids().filter(pidExists) + return testPids().filter(pidExists); } export function sortNumeric(arr: number[]): number[] { - return arr.sort((a, b) => a - b) + return arr.sort((a, b) => a - b); } export function flatten(arr: (T | T[])[], result: T[] = []): T[] { arr.forEach((ea) => Array.isArray(ea) ? result.push(...ea) : result.push(ea), - ) - return result + ); + return result; } // Seeding the RNG deterministically _should_ give us repeatable @@ -132,27 +132,27 @@ export function flatten(arr: (T | T[])[], result: T[] = []): T[] { // to make sure different error pathways are exercised. YYYY-MM-$callcount // should do it. -const rngseedPrefix = new Date().toISOString().slice(0, 7) + "." -let rngseedCounter = 0 -let rngseedOverride: string | undefined +const rngseedPrefix = new Date().toISOString().slice(0, 7) + "."; +let rngseedCounter = 0; +let rngseedOverride: string | undefined; export function setRngseed(seed?: string) { - rngseedOverride = seed + rngseedOverride = seed; } function rngseed() { // We need a new rngseed for every execution, or all runs will either pass or // fail: - return rngseedOverride ?? rngseedPrefix + rngseedCounter++ + return rngseedOverride ?? rngseedPrefix + rngseedCounter++; } -let failrate: string +let failrate: string; export function setFailratePct(percent: number) { - failrate = (percent / 100).toFixed(2) + failrate = (percent / 100).toFixed(2); } -let unluckyfail: "1" | "0" +let unluckyfail: "1" | "0"; /** * Should EUNLUCKY be handled properly by the test script, and emit a "FAIL", or @@ -162,28 +162,28 @@ let unluckyfail: "1" | "0" * where all flaky errors require a timeout to recover. */ export function setUnluckyFail(b: boolean) { - unluckyfail = b ? "1" : "0" + unluckyfail = b ? "1" : "0"; } -let newline: "lf" | "crlf" +let newline: "lf" | "crlf"; export function setNewline(eol: "lf" | "crlf") { - newline = eol + newline = eol; } -let ignoreExit: "1" | "0" +let ignoreExit: "1" | "0"; export function setIgnoreExit(ignore: boolean) { - ignoreExit = ignore ? "1" : "0" + ignoreExit = ignore ? "1" : "0"; } beforeEach(() => { - setFailratePct(10) - setUnluckyFail(true) - setNewline("lf") - setIgnoreExit(false) - setRngseed() -}) + setFailratePct(10); + setUnluckyFail(true); + setNewline("lf"); + setIgnoreExit(false); + setRngseed(); +}); export const processFactory = () => { const proc = child_process.spawn( @@ -198,7 +198,7 @@ export const processFactory = () => { unluckyfail, }, }, - ) - childProcs.push(proc) - return proc -} + ); + childProcs.push(proc); + return proc; +}; diff --git a/src/test-helpers.ts b/src/test-helpers.ts index f256031..ab79908 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -1 +1 @@ -export const ErrorPrefix = "ERROR: " +export const ErrorPrefix = "ERROR: "; diff --git a/src/test.spec.ts b/src/test.spec.ts index 98b89e8..fcf613c 100644 --- a/src/test.spec.ts +++ b/src/test.spec.ts @@ -1,155 +1,159 @@ -import child_process from "node:child_process" -import { until } from "./Async" -import { Deferred } from "./Deferred" -import { kill, pidExists } from "./Pids" +import child_process from "node:child_process"; +import { until } from "./Async"; +import { Deferred } from "./Deferred"; +import { kill, pidExists } from "./Pids"; import { expect, processFactory, setFailratePct, setIgnoreExit, setRngseed, -} from "./_chai.spec" +} from "./_chai.spec"; describe("test.js", () => { class Harness { - readonly child: child_process.ChildProcess - public output = "" + readonly child: child_process.ChildProcess; + public output = ""; constructor() { - setFailratePct(0) - this.child = processFactory() + setFailratePct(0); + this.child = processFactory(); this.child.on("error", (err: any) => { - throw err - }) + throw err; + }); this.child.stdout!.on("data", (buff: any) => { - this.output += buff.toString() - }) + this.output += buff.toString(); + }); } async untilOutput(minLength = 0): Promise { - await until(() => this.output.length > minLength, 1000) - return + await until(() => this.output.length > minLength, 1000); + return; } async end(): Promise { - this.child.stdin!.end(null) - await until(() => this.notRunning(), 1000) + this.child.stdin!.end(null); + await until(() => this.notRunning(), 1000); if (await this.running()) { - console.error("Ack, I had to kill child pid " + this.child.pid) - kill(this.child.pid) + console.error("Ack, I had to kill child pid " + this.child.pid); + kill(this.child.pid); } - return + return; } running(): boolean { - return pidExists(this.child.pid) + return pidExists(this.child.pid); } notRunning(): boolean { - return !this.running() + return !this.running(); } async assertStdout(f: (output: string) => void) { // The OS may take a bit before the PID shows up in the process table: - const alive = await until(() => pidExists(this.child.pid), 2000) - expect(alive).to.eql(true) - const d = new Deferred() + const alive = await until(() => pidExists(this.child.pid), 2000); + expect(alive).to.eql(true); + const d = new Deferred(); this.child.on("exit", async () => { try { - f(this.output.trim()) - expect(await this.running()).to.eql(false) - d.resolve("on exit") + f(this.output.trim()); + expect(await this.running()).to.eql(false); + d.resolve("on exit"); } catch (err: any) { - d.reject(err) + d.reject(err); } - }) - return d + }); + return d; } } it("results in expected output", async () => { - const h = new Harness() + const h = new Harness(); const a = h.assertStdout((ea) => expect(ea).to.eql("HELLO\nPASS\nworld\nPASS\nFAIL\nv1.2.3\nPASS"), - ) - h.child.stdin!.end("upcase Hello\ndowncase World\ninvalid input\nversion\n") - return a - }) + ); + h.child.stdin!.end( + "upcase Hello\ndowncase World\ninvalid input\nversion\n", + ); + return a; + }); it("exits properly if ignoreExit is not set", async () => { - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\nexit\n") - await h.untilOutput(9) - expect(h.output).to.eql("FUZZY\nPASS\n") - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(false) - return - }) + const h = new Harness(); + h.child.stdin!.write("upcase fuzzy\nexit\n"); + await h.untilOutput(9); + expect(h.output).to.eql("FUZZY\nPASS\n"); + await until(() => h.notRunning(), 500); + expect(await h.running()).to.eql(false); + return; + }); it("kill(!force) with ignoreExit unset causes the process to end", async () => { - setIgnoreExit(false) - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\n") - await h.untilOutput() - kill(h.child.pid, true) - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(false) - return - }) + setIgnoreExit(false); + const h = new Harness(); + h.child.stdin!.write("upcase fuzzy\n"); + await h.untilOutput(); + kill(h.child.pid, true); + await until(() => h.notRunning(), 500); + expect(await h.running()).to.eql(false); + return; + }); it("kill(force) even with ignoreExit set causes the process to end", async () => { - setIgnoreExit(true) - const h = new Harness() - h.child.stdin!.write("upcase fuzzy\n") - await h.untilOutput() - kill(h.child.pid, true) - await until(() => h.notRunning(), 500) - expect(await h.running()).to.eql(false) - return - }) + setIgnoreExit(true); + const h = new Harness(); + h.child.stdin!.write("upcase fuzzy\n"); + await h.untilOutput(); + kill(h.child.pid, true); + await until(() => h.notRunning(), 500); + expect(await h.running()).to.eql(false); + return; + }); it("doesn't exit if ignoreExit is set", async () => { - setIgnoreExit(true) - const h = new Harness() - h.child.stdin!.write("upcase Boink\nexit\n") - await h.untilOutput("BOINK\nPASS\nignore".length) - expect(h.output).to.eql("BOINK\nPASS\nignoreExit is set\n") - expect(await h.running()).to.eql(true) - await h.end() - expect(await h.running()).to.eql(false) - return - }) + setIgnoreExit(true); + const h = new Harness(); + h.child.stdin!.write("upcase Boink\nexit\n"); + await h.untilOutput("BOINK\nPASS\nignore".length); + expect(h.output).to.eql("BOINK\nPASS\nignoreExit is set\n"); + expect(await h.running()).to.eql(true); + await h.end(); + expect(await h.running()).to.eql(false); + return; + }); it("returns a valid pid", async () => { - const h = new Harness() - expect(pidExists(h.child.pid)).to.eql(true) - await h.end() - return - }) + const h = new Harness(); + expect(pidExists(h.child.pid)).to.eql(true); + await h.end(); + return; + }); it("sleeps serially", () => { - const h = new Harness() - const start = Date.now() - const times = [200, 201, 202] + const h = new Harness(); + const start = Date.now(); + const times = [200, 201, 202]; const a = h .assertStdout((output) => { - const actualTimes: number[] = [] - const pids = new Set() + const actualTimes: number[] = []; + const pids = new Set(); output.split(/[\r\n]/).forEach((line) => { if (line.startsWith("{") && line.endsWith("}")) { - const json = JSON.parse(line) - actualTimes.push(json.slept) - pids.add(json.pid) + const json = JSON.parse(line); + actualTimes.push(json.slept); + pids.add(json.pid); } else { - expect(line).to.eql("PASS") + expect(line).to.eql("PASS"); } - }) - expect(pids.size).to.eql(1, "only one pid should have been used") - expect(actualTimes).to.eql(times) + }); + expect(pids.size).to.eql(1, "only one pid should have been used"); + expect(actualTimes).to.eql(times); }) - .then(() => expect(Date.now() - start).to.be.gte(603)) - h.child.stdin!.end(times.map((ea) => "sleep " + ea).join("\n") + "\nexit\n") - return a - }) + .then(() => expect(Date.now() - start).to.be.gte(603)); + h.child.stdin!.end( + times.map((ea) => "sleep " + ea).join("\n") + "\nexit\n", + ); + return a; + }); it("flakes out the first N responses", () => { - setFailratePct(0) - setRngseed("hello") - const h = new Harness() + setFailratePct(0); + setRngseed("hello"); + const h = new Harness(); // These random numbers are consistent because we have a consistent rngseed: const a = h.assertStdout((ea) => expect(ea).to.eql( @@ -162,8 +166,8 @@ describe("test.js", () => { "FAIL", ].join("\n"), ), - ) - h.child.stdin!.end("flaky .5\nflaky 0\nflaky 1\nexit\n") - return a - }) -}) + ); + h.child.stdin!.end("flaky .5\nflaky 0\nflaky 1\nexit\n"); + return a; + }); +}); diff --git a/src/test.ts b/src/test.ts index afee866..e2cd114 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import process from "node:process" -import { delay } from "./Async" -import { Mutex } from "./Mutex" +import process from "node:process"; +import { delay } from "./Async"; +import { Mutex } from "./Mutex"; /** * This is a script written to behave similarly to ExifTool or @@ -10,43 +10,43 @@ import { Mutex } from "./Mutex" * The complexity comes from introducing predictable flakiness. */ -const newline = process.env.newline === "crlf" ? "\r\n" : "\n" +const newline = process.env.newline === "crlf" ? "\r\n" : "\n"; async function write(s: string) { return new Promise((res, rej) => process.stdout.write(s + newline, (err) => err == null ? res() : rej(err), ), - ) + ); } -const ignoreExit = process.env.ignoreExit === "1" +const ignoreExit = process.env.ignoreExit === "1"; if (ignoreExit) { process.addListener("SIGINT", () => { - write("ignoring SIGINT") - }) + write("ignoring SIGINT"); + }); process.addListener("SIGTERM", () => { - write("ignoring SIGTERM") - }) + write("ignoring SIGTERM"); + }); } function toF(s: string | undefined) { - if (s == null) return - const f = parseFloat(s) - return isNaN(f) ? undefined : f + if (s == null) return; + const f = parseFloat(s); + return isNaN(f) ? undefined : f; } -const failrate = toF(process.env.failrate) ?? 0 +const failrate = toF(process.env.failrate) ?? 0; const rng = process.env.rngseed != null ? // eslint-disable-next-line @typescript-eslint/no-require-imports require("seedrandom")(process.env.rngseed) - : Math.random + : Math.random; async function onLine(line: string): Promise { // write(`# ${_p.pid} onLine(${line.trim()}) (newline = ${process.env.newline})`) - const r = rng() + const r = rng(); if (r < failrate) { // stderr isn't buffered, so this should be flushed immediately: console.error( @@ -56,25 +56,25 @@ async function onLine(line: string): Promise { failrate.toFixed(2) + ", seed: " + process.env.rngseed, - ) + ); if (process.env.unluckyfail === "1") { // Wait for a bit to ensure streams get merged thanks to streamFlushMillis: - await delay(5) - await write("FAIL") + await delay(5); + await write("FAIL"); } - return + return; } - line = line.trim() - const tokens = line.split(/\s+/) - const firstToken = tokens.shift() + line = line.trim(); + const tokens = line.split(/\s+/); + const firstToken = tokens.shift(); // support multi-line outputs: - const postToken = tokens.join(" ").split("
").join(newline) + const postToken = tokens.join(" ").split("
").join(newline); try { switch (firstToken) { case "flaky": { - const flakeRate = toF(tokens.shift()) ?? failrate + const flakeRate = toF(tokens.shift()) ?? failrate; write( "flaky response (" + (r < flakeRate ? "FAIL" : "PASS") + @@ -85,70 +85,70 @@ async function onLine(line: string): Promise { // Extra information is used for context: (tokens.length > 0 ? ", " + tokens.join(" ") : "") + ")", - ) + ); if (r < flakeRate) { - write("FAIL") + write("FAIL"); } else { - write("PASS") + write("PASS"); } - break + break; } case "upcase": { - write(postToken.toUpperCase()) - write("PASS") - break + write(postToken.toUpperCase()); + write("PASS"); + break; } case "downcase": { - write(postToken.toLowerCase()) - write("PASS") - break + write(postToken.toLowerCase()); + write("PASS"); + break; } case "sleep": { - const millis = parseInt(tokens[0] ?? "100") - await delay(millis) - write(JSON.stringify({ slept: millis, pid: process.pid })) - write("PASS") - break + const millis = parseInt(tokens[0] ?? "100"); + await delay(millis); + write(JSON.stringify({ slept: millis, pid: process.pid })); + write("PASS"); + break; } case "version": { - write("v1.2.3") - write("PASS") - break + write("v1.2.3"); + write("PASS"); + break; } case "exit": { if (ignoreExit) { - write("ignoreExit is set") + write("ignoreExit is set"); } else { - process.exit(0) + process.exit(0); } - break + break; } case "stderr": { // force stdout to be emitted before stderr, and exercise stream // debouncing: - write("PASS") - await delay(1) - console.error("Error: " + postToken) - break + write("PASS"); + await delay(1); + console.error("Error: " + postToken); + break; } default: { - console.error("invalid or missing command for input", line) - write("FAIL") + console.error("invalid or missing command for input", line); + write("FAIL"); } } } catch (err) { - console.error("Error: " + err) - write("FAIL") + console.error("Error: " + err); + write("FAIL"); } - return + return; } -const m = new Mutex() +const m = new Mutex(); process.stdin // eslint-disable-next-line @typescript-eslint/no-require-imports .pipe(require("split2")()) - .on("data", (ea: string) => m.serial(() => onLine(ea))) + .on("data", (ea: string) => m.serial(() => onLine(ea))); diff --git a/types/chai-withintoleranceof/index.d.ts b/types/chai-withintoleranceof/index.d.ts index 73882d6..6ba9166 100644 --- a/types/chai-withintoleranceof/index.d.ts +++ b/types/chai-withintoleranceof/index.d.ts @@ -6,7 +6,7 @@ /// interface WithinTolerance { - (expected: number, tol: number | number[], message?: string): Chai.Assertion + (expected: number, tol: number | number[], message?: string): Chai.Assertion; } declare namespace Chai { @@ -14,7 +14,7 @@ declare namespace Chai { extends LanguageChains, NumericComparison, TypeComparison { - withinToleranceOf: WithinTolerance - withinTolOf: WithinTolerance + withinToleranceOf: WithinTolerance; + withinTolOf: WithinTolerance; } } From 0204bd80c61d008a4d711495533ffadf50b9d07d Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 11:18:36 -0700 Subject: [PATCH 30/60] fix(changelog): drop official support for Node v23, which is EOL --- .github/workflows/node.js.yml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 0c4c751..4f638f9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -30,7 +30,7 @@ jobs: matrix: os: [ubuntu-latest, macos-14, windows-latest] # See https://github.com/nodejs/release#release-schedule - node-version: [20.x, 22.x, 23.x, 24.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b1087..7ab3f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ See [Semver](http://semver.org/). - ๐Ÿ’” Deleted the standalone `pids()` function and associated code (including the ProcpsChecker). This function was exported but only used internally by tests. This fixes the [issue #58](https://github.com/photostructure/batch-cluster.js/issues/58) (by deleting the unused code! _the best kind of bugfix_). Thanks for the report, [Zaczero](https://github.com/Zaczero)! +- ๐Ÿ’” Dropped official support for [Node v23, which is EOL](https://nodejs.org/en/about/previous-releases). + - ๐Ÿ“ฆ Simplified `prettier` config to accept all defaults -- this added semicolons to every file. ## v14.0.0 From 8db8503118b0d09a9ab70659dfdbbc77bc8b88e1 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 14:32:29 -0700 Subject: [PATCH 31/60] feat: add release script and GitHub Actions workflow for automated releases - Added "release" script to package.json to facilitate versioning and publishing. - Created a new GitHub Actions workflow for handling releases, including version input options and SSH signing setup. --- .claude/settings.local.json | 5 +- .github/workflows/release.yml | 50 + package-lock.json | 2402 +++++++++++++++++++++++++++++++-- package.json | 2 + 4 files changed, 2340 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 75cb4a9..c1240a9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -31,9 +31,10 @@ "Bash(for i in {1..5})", "Bash(do echo \"Run $i:\")", "Bash(break)", - "Bash(done)" + "Bash(done)", + "WebSearch" ], "deny": [] }, "enableAllProjectMcpServers": false -} +} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3515c45 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: "Version type (auto-detects from package.json if not specified)" + required: false + type: choice + options: + - "" + - patch + - minor + - major + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 # Need full history for release-it + + # setup-node configures auth with registry-url and NODE_AUTH_TOKEN + # See: https://github.com/actions/setup-node#usage + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 20 + cache: "npm" + registry-url: "https://registry.npmjs.org/" + + - name: Set up SSH signing + uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v1.0.0 + with: + ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} + git-user-name: ${{ secrets.GIT_USER_NAME }} + git-user-email: ${{ secrets.GIT_USER_EMAIL }} + + - name: Install dependencies + run: npm ci + + - name: Release + run: npm run release -- --ci ${{ github.event.inputs.version }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fda3f5e..47b4219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "npm-run-all": "4.1.5", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.2.0", + "release-it": "^19.0.4", "rimraf": "^5.0.10", "seedrandom": "^3.0.5", "serve": "^14.2.4", @@ -101,9 +102,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -116,9 +117,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -126,9 +127,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -199,13 +200,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -292,6 +293,406 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/checkbox": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.1.tgz", + "integrity": "sha512-bevKGO6kX1eM/N+pdh9leS5L7TBF4ICrzi9a+cbWkrxeAeIcwlo/7OfWGCDERdRCI2/Q6tjltX4bt07ALHDwFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.15.tgz", + "integrity": "sha512-SwHMGa8Z47LawQN0rog0sT+6JpiL0B7eW9p1Bb7iCeKDGTI5Ez25TSc2l8kw52VV7hA4sX/C78CGkMrKXfuspA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.17.tgz", + "integrity": "sha512-r6bQLsyPSzbWrZZ9ufoWL+CztkSatnJ6uSxqd6N+o41EZC51sQeWOzI6s5jLb+xxTWxl7PlUppqm8/sow241gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/external-editor": "^1.0.1", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", + "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.3.tgz", + "integrity": "sha512-iHYp+JCaCRktM/ESZdpHI51yqsDgXu+dMs4semzETftOaF8u5hwlqnbIsuIR/LrWZl8Pm1/gzteK9I7MAq5HTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.1", + "@inquirer/confirm": "^5.1.15", + "@inquirer/editor": "^4.2.17", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -321,9 +722,9 @@ } }, "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==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, @@ -376,6 +777,230 @@ "node": ">= 8" } }, + "node_modules/@nodeutils/defaults-deep": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodeutils/defaults-deep/-/defaults-deep-1.1.0.tgz", + "integrity": "sha512-gG44cwQovaOFdSR02jR9IhVRpnDP64VN6JdjYJTfNz4J4fWn7TQnmrf22nSjRqlwlxPcW8PL/L3KbJg3tdwvpg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash": "^4.15.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", + "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", + "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", + "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^6.1.4", + "@octokit/plugin-paginate-rest": "^11.4.2", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.3.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@phun-ky/typeof": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-1.2.8.tgz", + "integrity": "sha512-7J6ca1tK0duM2BgVB+CuFMh3idlIVASOP2QvOCbNWDc6JnvjtKa9nufPoJQQ4xrwBonwgT1TIhRRcEtzdVgWsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.9.0 || >=22.0.0", + "npm": ">=10.8.2" + }, + "funding": { + "url": "https://github.com/phun-ky/typeof?sponsor=1" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -463,6 +1088,13 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -529,9 +1161,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -576,6 +1208,13 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/parse-path": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", + "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", @@ -875,17 +1514,40 @@ "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/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/accepts/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/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -918,6 +1580,16 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -990,10 +1662,26 @@ "node": ">=8" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -1186,6 +1874,19 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -1196,6 +1897,16 @@ "node": ">= 0.4" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1219,6 +1930,23 @@ "dev": true, "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/boxen": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", @@ -1243,9 +1971,9 @@ } }, "node_modules/boxen/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", "dev": true, "license": "MIT", "engines": { @@ -1255,10 +1983,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/boxen/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1293,6 +2034,22 @@ "dev": true, "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -1303,6 +2060,35 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1422,6 +2208,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==", + "deprecated": "functionality of this lib is built-in to chai now. see more details here: https://github.com/debitoor/chai-subset/pull/85", "dev": true, "license": "MIT", "engines": { @@ -1478,6 +2265,13 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, + "node_modules/chardet": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -1507,6 +2301,32 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -1520,6 +2340,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/clipboardy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", @@ -1699,6 +2558,23 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -1731,6 +2607,16 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -1846,6 +2732,36 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1864,6 +2780,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -1882,6 +2811,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -1905,6 +2863,19 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2129,21 +3100,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2154,9 +3147,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2285,9 +3278,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2314,29 +3307,16 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/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/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2345,6 +3325,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2391,6 +3385,19 @@ "node": ">=0.10.0" } }, + "node_modules/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2409,10 +3416,26 @@ "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/execa/node_modules/signal-exit": { @@ -2422,6 +3445,30 @@ "dev": true, "license": "ISC" }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2483,6 +3530,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2641,6 +3706,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -2721,6 +3799,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-up": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", + "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^9.2.0" + } + }, + "node_modules/git-url-parse": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", + "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-up": "^8.1.0" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2756,9 +3888,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2949,6 +4081,34 @@ "dev": true, "license": "ISC" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2959,6 +4119,19 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3003,6 +4176,33 @@ "dev": true, "license": "ISC" }, + "node_modules/inquirer": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.7.0.tgz", + "integrity": "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/prompts": "^7.6.0", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "mute-stream": "^2.0.0", + "run-async": "^4.0.4", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3018,6 +4218,16 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3161,16 +4371,16 @@ } }, "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3244,6 +4454,38 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3368,6 +4610,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ssh": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3504,6 +4756,22 @@ "node": ">=8" } }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -3518,6 +4786,23 @@ "dev": true, "license": "ISC" }, + "node_modules/issue-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", + "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -3534,6 +4819,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3654,6 +4949,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3661,6 +4991,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3702,6 +5039,19 @@ "dev": true, "license": "MIT" }, + "node_modules/macos-release": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.4.0.tgz", + "integrity": "sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -3784,6 +5134,19 @@ "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", @@ -3795,28 +5158,18 @@ } }, "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==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.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", @@ -3827,6 +5180,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3897,9 +5263,9 @@ } }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3945,6 +5311,16 @@ "dev": true, "license": "MIT" }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3962,6 +5338,45 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/new-github-release-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-2.0.0.tgz", + "integrity": "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^2.5.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/new-github-release-url/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -3969,6 +5384,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -4197,6 +5619,26 @@ "node": ">=8" } }, + "node_modules/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4294,6 +5736,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/on-headers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", @@ -4305,16 +5754,35 @@ } }, "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4338,6 +5806,128 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-name": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.1.0.tgz", + "integrity": "sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "macos-release": "^3.3.0", + "windows-release": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -4388,6 +5978,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4422,6 +6046,30 @@ "node": ">=4" } }, + "node_modules/parse-path": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", + "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", + "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-path": "^7.0.0", + "parse-path": "^7.0.0" + }, + "engines": { + "node": ">=14.13.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4493,6 +6141,13 @@ "node": ">=4" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -4503,6 +6158,13 @@ "node": "*" } }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4511,13 +6173,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -4546,6 +6208,18 @@ "node": ">=4" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -4599,6 +6273,50 @@ } } }, + "node_modules/protocols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4686,6 +6404,17 @@ "node": ">=0.10.0" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -4780,7 +6509,68 @@ "rc": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/release-it": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.0.4.tgz", + "integrity": "sha512-W9A26FW+l1wy5fDg9BeAknZ19wV+UvHUDOC4D355yIOZF/nHBOIhjDwutKd4pikkjvL7CpKeF+4zLxVP9kmVEw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/webpro" + } + ], + "license": "MIT", + "dependencies": { + "@nodeutils/defaults-deep": "1.1.0", + "@octokit/rest": "21.1.1", + "@phun-ky/typeof": "1.2.8", + "async-retry": "1.3.3", + "c12": "3.1.0", + "ci-info": "^4.3.0", + "eta": "3.5.0", + "git-url-parse": "16.1.0", + "inquirer": "12.7.0", + "issue-parser": "7.0.1", + "lodash.merge": "4.6.2", + "mime-types": "3.0.1", + "new-github-release-url": "2.0.0", + "open": "10.2.0", + "ora": "8.2.0", + "os-name": "6.1.0", + "proxy-agent": "6.5.0", + "semver": "7.7.2", + "tinyglobby": "0.2.14", + "undici": "6.21.3", + "url-join": "5.0.0", + "wildcard-match": "5.1.4", + "yargs-parser": "21.1.1" + }, + "bin": { + "release-it": "bin/release-it.js" + }, + "engines": { + "node": "^20.12.0 || >=22.0.0" + } + }, + "node_modules/release-it/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/require-directory": { @@ -4834,6 +6624,33 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4861,6 +6678,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4885,6 +6725,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -4961,6 +6811,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -5264,6 +7121,47 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5315,9 +7213,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, "license": "CC0-1.0" }, @@ -5331,6 +7229,19 @@ "node": ">= 10.x" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5586,6 +7497,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5686,6 +7621,13 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5710,13 +7652,13 @@ } }, "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5825,9 +7767,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5914,6 +7856,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -5921,6 +7873,13 @@ "dev": true, "license": "MIT" }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, "node_modules/update-check": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", @@ -5942,6 +7901,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6091,6 +8060,160 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wildcard-match": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", + "dev": true, + "license": "ISC" + }, + "node_modules/windows-release": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz", + "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/windows-release/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/windows-release/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6102,9 +8225,9 @@ } }, "node_modules/workerpool": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", - "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", "dev": true, "license": "Apache-2.0" }, @@ -6203,6 +8326,38 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6214,9 +8369,9 @@ } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { @@ -6351,6 +8506,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 951b521..35a8877 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "update:deps": "ncu -u --install always", "install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest", "update:actions": "pinact run -u", + "release": "release-it", "precommit": "npm i && run-s update docs:build test" }, "release-it": { @@ -74,6 +75,7 @@ "npm-run-all": "4.1.5", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.2.0", + "release-it": "^19.0.4", "rimraf": "^5.0.10", "seedrandom": "^3.0.5", "serve": "^14.2.4", From e7248ce8f45dc8fb23fe3f8e4ebb17f94f7b16c8 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 14:48:27 -0700 Subject: [PATCH 32/60] fix(pidExists): handle Windows-specific error codes for process existence check --- src/Pids.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Pids.ts b/src/Pids.ts index 65339c5..865602c 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -1,3 +1,5 @@ +import { isWin } from "./Platform"; + /** * @param {number} pid process id. Required. * @returns boolean true if the given process id is in the local process @@ -15,6 +17,13 @@ export function pidExists(pid: number | undefined): boolean { // exist. EPERM means it _does_ exist! if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true; + // On Windows, some error codes might indicate the process is terminating + // but hasn't fully exited yet. Treat these as "not existing" to avoid + // race conditions during shutdown. + if (isWin && (err as NodeJS.ErrnoException)?.code === "EINVAL") { + return false; + } + // failed to get priority--assume the pid is gone. return false; } From 888d0e860122820d981a8a14215ad8c9117483f3 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 14:48:31 -0700 Subject: [PATCH 33/60] fix(ProcessTerminator): adjust timeout for Windows process cleanup --- src/ProcessTerminator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ProcessTerminator.ts b/src/ProcessTerminator.ts index a457b49..67d6cc7 100644 --- a/src/ProcessTerminator.ts +++ b/src/ProcessTerminator.ts @@ -3,6 +3,7 @@ import { until } from "./Async"; import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; import { Logger } from "./Logger"; import { kill } from "./Pids"; +import { isWin } from "./Platform"; import { destroy } from "./Stream"; import { ensureSuffix } from "./String"; import { Task } from "./Task"; @@ -180,6 +181,8 @@ export class ProcessTerminator { timeout: number, isRunning: () => boolean, ): Promise { - await until(() => !isRunning(), timeout); + // Windows processes can take longer to clean up after being killed + const effectiveTimeout = isWin ? timeout * 3 : timeout; + await until(() => !isRunning(), effectiveTimeout); } } From 27d5a1afff2f516aedc59149a3bcbf947a6eefaf Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 22:06:30 -0700 Subject: [PATCH 34/60] chore(health): rename maybeRunHealthcheck to maybeRunHealthCheck --- src/BatchProcess.ts | 2 ++ src/ProcessHealthMonitor.spec.ts | 14 +++++++------- src/ProcessHealthMonitor.ts | 2 +- src/ProcessPoolManager.ts | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/BatchProcess.ts b/src/BatchProcess.ts index e8f8e6f..15b581e 100644 --- a/src/BatchProcess.ts +++ b/src/BatchProcess.ts @@ -236,6 +236,8 @@ export class BatchProcess { maybeRunHealthcheck(): Task | undefined { return this.#healthMonitor.maybeRunHealthcheck(this); + maybeRunHealthCheck(): Task | undefined { + return this.#healthMonitor.maybeRunHealthCheck(this); } // This must not be async, or new instances aren't started as busy (until the diff --git a/src/ProcessHealthMonitor.spec.ts b/src/ProcessHealthMonitor.spec.ts index 8a2ed75..20a9bab 100644 --- a/src/ProcessHealthMonitor.spec.ts +++ b/src/ProcessHealthMonitor.spec.ts @@ -274,21 +274,21 @@ describe("ProcessHealthMonitor", function () { }); const noHealthCheckMonitor = new ProcessHealthMonitor(options, emitter); - const result = noHealthCheckMonitor.maybeRunHealthcheck(mockBatchProcess); + 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); + 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); + const result = healthMonitor.maybeRunHealthCheck(mockBatchProcess); expect(result).to.not.be.undefined; expect(result?.command).to.eql("healthcheck"); }); @@ -300,7 +300,7 @@ describe("ProcessHealthMonitor", function () { state.lastHealthCheck = Date.now() - 2000; // 2 seconds ago } - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + const result = healthMonitor.maybeRunHealthCheck(mockBatchProcess); expect(result).to.not.be.undefined; expect(result?.command).to.eql("healthcheck"); }); @@ -312,7 +312,7 @@ describe("ProcessHealthMonitor", function () { state.lastHealthCheck = Date.now(); } - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + const result = healthMonitor.maybeRunHealthCheck(mockBatchProcess); expect(result).to.be.undefined; }); @@ -324,7 +324,7 @@ describe("ProcessHealthMonitor", function () { healthMonitor.recordJobFailure(mockProcess.pid); - const result = healthMonitor.maybeRunHealthcheck(failingProcess); + const result = healthMonitor.maybeRunHealthCheck(failingProcess); expect(result).to.be.undefined; }); }); @@ -380,7 +380,7 @@ describe("ProcessHealthMonitor", function () { }; // Don't initialize the process - const result = healthMonitor.maybeRunHealthcheck(mockBatchProcess); + const result = healthMonitor.maybeRunHealthCheck(mockBatchProcess); expect(result).to.be.undefined; }); }); diff --git a/src/ProcessHealthMonitor.ts b/src/ProcessHealthMonitor.ts index c6515c9..fa64b99 100644 --- a/src/ProcessHealthMonitor.ts +++ b/src/ProcessHealthMonitor.ts @@ -133,7 +133,7 @@ export class ProcessHealthMonitor { /** * Run a health check on a process if needed */ - maybeRunHealthcheck( + maybeRunHealthCheck( process: HealthCheckable & { execTask: (task: Task) => boolean }, ): Task | undefined { const hcc = this.options.healthCheckCommand; diff --git a/src/ProcessPoolManager.ts b/src/ProcessPoolManager.ts index 97d7034..f8f9149 100644 --- a/src/ProcessPoolManager.ts +++ b/src/ProcessPoolManager.ts @@ -167,7 +167,7 @@ export class ProcessPoolManager { endPromises.push(proc.end(true, why)); return false; } - proc.maybeRunHealthcheck(); + proc.maybeRunHealthCheck(); } return true; }); From 63355a33ba89eb00639353ab93be7bc08c8de112 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 22:07:58 -0700 Subject: [PATCH 35/60] feat(Pids): enhance pidExists function with optional kill function parameter. Add tests. --- src/Pids.spec.ts | 199 ++++++++++++++++++++++++++++++++++++++++++++++ src/Pids.ts | 33 +++++--- src/_chai.spec.ts | 2 +- 3 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 src/Pids.spec.ts diff --git a/src/Pids.spec.ts b/src/Pids.spec.ts new file mode 100644 index 0000000..4acf579 --- /dev/null +++ b/src/Pids.spec.ts @@ -0,0 +1,199 @@ +import child_process from "node:child_process"; +import process from "node:process"; +import { expect } from "./_chai.spec"; +import { kill, pidExists } from "./Pids"; +import { isWin } from "./Platform"; + +describe("Pids", function () { + describe("pidExists", function () { + it("should return true for current process", function () { + expect(pidExists(process.pid)).to.be.true; + }); + + it("should return false for invalid PIDs", function () { + expect(pidExists(0)).to.be.false; + expect(pidExists(-1)).to.be.false; + expect(pidExists(-999)).to.be.false; + }); + + it("should return false for null and undefined", function () { + expect(pidExists(null as any)).to.be.false; + expect(pidExists(undefined)).to.be.false; + }); + + it("should return false for non-finite numbers", function () { + expect(pidExists(NaN)).to.be.false; + expect(pidExists(Infinity)).to.be.false; + expect(pidExists(-Infinity)).to.be.false; + }); + + it("should return false for very large non-existent PID", function () { + // Use a PID that's extremely unlikely to exist + expect(pidExists(999999999)).to.be.false; + }); + + it("should handle child process PIDs correctly", function () { + const child = child_process.spawn("node", [ + "-e", + "setTimeout(() => {}, 100)", + ]); + + if (child.pid != null) { + expect(pidExists(child.pid)).to.be.true; + + child.kill(); + + // Give process time to terminate + return new Promise((resolve) => { + child.on("exit", () => { + // Process should no longer exist after termination + setTimeout(() => { + expect(pidExists(child.pid!)).to.be.false; + resolve(); + }, 50); + }); + }); + } else { + // If no PID, skip this test + return Promise.resolve(); + } + }); + + if (isWin) { + it("should handle Windows-specific error codes", function () { + // Create a process that terminates quickly to potentially trigger Windows-specific errors + const child = child_process.spawn("cmd", ["/c", "echo test"]); + + if (child.pid != null) { + const originalPid = child.pid; + + return new Promise((resolve) => { + child.on("exit", () => { + // On Windows, attempting to check a recently terminated process + // may throw EINVAL or EACCES instead of ESRCH + setTimeout(() => { + // This should return false regardless of the specific error code + expect(pidExists(originalPid)).to.be.false; + resolve(); + }, 100); + }); + }); + } else { + // If no PID, skip this test + return Promise.resolve(); + } + }); + } + + it("should handle error conditions gracefully", function () { + // Test EPERM error (should return true - process exists but no permission) + const mockKillEPERM = () => { + const err = new Error( + "Operation not permitted", + ) as NodeJS.ErrnoException; + err.code = "EPERM"; + throw err; + }; + expect(pidExists(12345, mockKillEPERM)).to.be.true; + + // Test ESRCH error (should return false - no such process) + const mockKillESRCH = () => { + const err = new Error("No such process") as NodeJS.ErrnoException; + err.code = "ESRCH"; + throw err; + }; + expect(pidExists(12345, mockKillESRCH)).to.be.false; + + if (isWin) { + // Test Windows-specific EINVAL error (should return false) + const mockKillEINVAL = () => { + const err = new Error("Invalid argument") as NodeJS.ErrnoException; + err.code = "EINVAL"; + throw err; + }; + expect(pidExists(12345, mockKillEINVAL)).to.be.false; + + // Test Windows-specific EACCES error (should return false) + const mockKillEACCES = () => { + const err = new Error("Permission denied") as NodeJS.ErrnoException; + err.code = "EACCES"; + throw err; + }; + expect(pidExists(12345, mockKillEACCES)).to.be.false; + } + + // Test unknown error code (should return false) + const mockKillUnknown = () => { + const err = new Error("Unknown error") as NodeJS.ErrnoException; + err.code = "EUNKNOWN"; + throw err; + }; + expect(pidExists(12345, mockKillUnknown)).to.be.false; + }); + }); + + describe("kill", function () { + it("should return false for invalid PIDs", function () { + expect(kill(0)).to.be.false; + expect(kill(-1)).to.be.false; + expect(kill(null as any)).to.be.false; + expect(kill(undefined)).to.be.false; + expect(kill(NaN)).to.be.false; + expect(kill(Infinity)).to.be.false; + }); + + it("should return false for non-existent PID", function () { + expect(kill(999999999)).to.be.false; + }); + + it("should handle ESRCH error gracefully", function () { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalKill = process.kill; + + const mockKill = () => { + const err = new Error("No such process - ESRCH"); + throw err; + }; + process.kill = mockKill; + + expect(kill(12345)).to.be.false; + + process.kill = originalKill; + }); + + it("should re-throw non-ESRCH errors", function () { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalKill = process.kill; + + const mockKill = () => { + const err = new Error("Operation not permitted"); + throw err; + }; + process.kill = mockKill; + + expect(() => kill(12345)).to.throw("Operation not permitted"); + + process.kill = originalKill; + }); + + it("should use SIGKILL when force is true", function () { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalKill = process.kill; + let capturedSignal: string | number | undefined; + + const mockKill = (_pid: number, signal?: string | number): true => { + capturedSignal = signal; + return true; + }; + process.kill = mockKill; + + kill(12345, true); + expect(capturedSignal).to.equal("SIGKILL"); + + kill(12345, false); + expect(capturedSignal).to.be.undefined; + + process.kill = originalKill; + }); + }); +}); diff --git a/src/Pids.ts b/src/Pids.ts index 865602c..d700f3b 100644 --- a/src/Pids.ts +++ b/src/Pids.ts @@ -2,29 +2,38 @@ import { isWin } from "./Platform"; /** * @param {number} pid process id. Required. + * @param {Function} killFn optional kill function, defaults to process.kill * @returns boolean true if the given process id is in the local process * table. The PID may be paused or a zombie, though. */ -export function pidExists(pid: number | undefined): boolean { +export function pidExists( + pid: number | undefined, + killFn?: (pid: number, signal?: string | number) => boolean, +): boolean { if (pid == null || !isFinite(pid) || pid <= 0) return false; try { // signal 0 can be used to test for the existence of a process // see https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processkillpid-signal - return process.kill(pid, 0); + return (killFn ?? process.kill)(pid, 0); } catch (err: unknown) { - // We're expecting err.code to be either "EPERM" (if we don't have - // permission to send `pid` and message), or "ESRCH" if that pid doesn't - // exist. EPERM means it _does_ exist! - if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true; + const errorCode = (err as NodeJS.ErrnoException)?.code; - // On Windows, some error codes might indicate the process is terminating - // but hasn't fully exited yet. Treat these as "not existing" to avoid - // race conditions during shutdown. - if (isWin && (err as NodeJS.ErrnoException)?.code === "EINVAL") { - return false; + // EPERM means we don't have permission to signal the process, but it exists + if (errorCode === "EPERM") return true; + + // ESRCH means "no such process" - the process doesn't exist or has terminated + if (errorCode === "ESRCH") return false; + + // On Windows, additional error codes can indicate process termination issues + if (isWin) { + // EINVAL: Invalid signal argument (process may be terminating) + // EACCES: Access denied (process may be in terminating state) + if (errorCode === "EINVAL" || errorCode === "EACCES") { + return false; + } } - // failed to get priority--assume the pid is gone. + // For any other error, assume the pid is gone return false; } } diff --git a/src/_chai.spec.ts b/src/_chai.spec.ts index 5cf5914..02ac698 100644 --- a/src/_chai.spec.ts +++ b/src/_chai.spec.ts @@ -111,7 +111,7 @@ export function testPids(): number[] { } export function currentTestPids(): number[] { - return testPids().filter(pidExists); + return testPids().filter((pid) => pidExists(pid)); } export function sortNumeric(arr: number[]): number[] { From b024e6eb0587af5064190c221012e1978a2dbbc8 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 22:08:34 -0700 Subject: [PATCH 36/60] fix(BatchCluster): change checkShutdown to synchronous function and remove unnecessary await for currentTestPids --- src/BatchCluster.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index 98c9f00..a1de9f5 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -122,14 +122,14 @@ describe("BatchCluster", function () { // processes to exit: expect(bc.ended).to.eql(true); - async function checkShutdown() { + function checkShutdown() { // const isIdle = bc.isIdle // If bc has been told to shut down, it won't ever finish any pending commands. // const pendingCommands = bc.pendingTasks.map((ea) => ea.command) const runningCommands = bc.currentTasks.map((ea) => ea.command); const busyProcCount = bc.busyProcCount; const pids = bc.pids(); - const livingPids = await currentTestPids(); + const livingPids = currentTestPids(); const done = runningCommands.length === 0 && From 32be80daeaaab6f023edf49ac6892d3efa3e8f03 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 22:08:44 -0700 Subject: [PATCH 37/60] fix(BatchProcess): correct spelling of lastJobFinishedAt and update process exit handling to use Deferred --- src/BatchProcess.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/BatchProcess.ts b/src/BatchProcess.ts index 15b581e..0c35c30 100644 --- a/src/BatchProcess.ts +++ b/src/BatchProcess.ts @@ -27,12 +27,13 @@ export class BatchProcess { readonly #terminator: ProcessTerminator; readonly #healthMonitor: ProcessHealthMonitor; readonly #streamHandler: StreamHandler; - #lastJobFinshedAt = Date.now(); + #lastJobFinishedAt = Date.now(); // Only set to true when `proc.pid` is no longer in the process table. #starting = true; - #exited = false; + // Deferred that resolves when the process exits (via OS events) + #processExitDeferred = new Deferred(); // override for .whyNotHealthy() #whyNotHealthy?: WhyNotHealthy; @@ -101,12 +102,15 @@ export class BatchProcess { this.proc.on("error", (err) => this.#onError("proc.error", err)); this.proc.on("close", () => { + this.#processExitDeferred.resolve(); void this.end(false, "proc.close"); }); this.proc.on("exit", () => { + this.#processExitDeferred.resolve(); void this.end(false, "proc.exit"); }); this.proc.on("disconnect", () => { + this.#processExitDeferred.resolve(); void this.end(false, "proc.disconnect"); }); @@ -161,13 +165,12 @@ export class BatchProcess { } /** - * @return true if the child process has exited and is no longer in the - * process table. Note that this may be erroneously false if the process table - * hasn't been checked. Call {@link BatchProcess#running()} for an authoritative (but - * expensive!) answer. + * @return true if the child process has exited (based on OS events). + * This is now authoritative and inexpensive since it's driven by OS events + * rather than polling. */ get exited(): boolean { - return this.#exited; + return this.#processExitDeferred.settled; } /** @@ -212,18 +215,22 @@ export class BatchProcess { } get idleMs(): number { - return this.idle ? Date.now() - this.#lastJobFinshedAt : -1; + return this.idle ? Date.now() - this.#lastJobFinishedAt : -1; } /** - * @return true if the child process is in the process table + * @return true if the child process is running. + * Now event-driven first with polling fallback. */ running(): boolean { - if (this.#exited) return false; + // If we've been notified via OS events that process exited, trust that immediately + if (this.exited) return false; + // Only poll as fallback if we haven't been notified yet + // This handles edge cases where events might not fire reliably const alive = pidExists(this.pid); if (!alive) { - this.#exited = true; + this.#processExitDeferred.resolve(); // once a PID leaves the process table, it's gone for good. void this.end(false, "proc.exit"); } @@ -234,8 +241,6 @@ export class BatchProcess { return !this.running(); } - maybeRunHealthcheck(): Task | undefined { - return this.#healthMonitor.maybeRunHealthcheck(this); maybeRunHealthCheck(): Task | undefined { return this.#healthMonitor.maybeRunHealthCheck(this); } @@ -354,7 +359,7 @@ export class BatchProcess { lastTask, this.startupTaskId, gracefully, - this.#exited, + this.exited, () => this.running(), ); @@ -429,6 +434,6 @@ export class BatchProcess { map(this.#currentTaskTimeout, (ea) => clearTimeout(ea)); this.#currentTaskTimeout = undefined; this.#currentTask = undefined; - this.#lastJobFinshedAt = Date.now(); + this.#lastJobFinishedAt = Date.now(); } } From c0eb184c5e0c519b6dfbe4cf12c182d0d87803e4 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 22:08:58 -0700 Subject: [PATCH 38/60] fix(ProcessTerminator): remove Windows-specific timeout adjustment in awaitNotRunning function --- src/ProcessTerminator.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ProcessTerminator.ts b/src/ProcessTerminator.ts index 67d6cc7..a457b49 100644 --- a/src/ProcessTerminator.ts +++ b/src/ProcessTerminator.ts @@ -3,7 +3,6 @@ import { until } from "./Async"; import { InternalBatchProcessOptions } from "./InternalBatchProcessOptions"; import { Logger } from "./Logger"; import { kill } from "./Pids"; -import { isWin } from "./Platform"; import { destroy } from "./Stream"; import { ensureSuffix } from "./String"; import { Task } from "./Task"; @@ -181,8 +180,6 @@ export class ProcessTerminator { timeout: number, isRunning: () => boolean, ): Promise { - // Windows processes can take longer to clean up after being killed - const effectiveTimeout = isWin ? timeout * 3 : timeout; - await until(() => !isRunning(), effectiveTimeout); + await until(() => !isRunning(), timeout); } } From 9c0d213e8ade2d0f10fa613115a94b82dd9cdbdf Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 21 Aug 2025 22:09:12 -0700 Subject: [PATCH 39/60] chore(settings): add 'Millis' and 'photostructure' to cSpell.words --- .vscode/settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 81106d7..26968c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,12 @@ "debouncing", "rngseed" ], - "cSpell.words": ["Pids", "Procs", "sinonjs", "zombification"] + "cSpell.words": [ + "Millis", + "photostructure", + "Pids", + "Procs", + "sinonjs", + "zombification" + ] } From ee585dfddac19bc2894bb1cebf18551fc80d1ba1 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 22 Aug 2025 13:41:13 -0700 Subject: [PATCH 40/60] chore(release): OIDC --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3515c45..e953098 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,9 @@ on: - minor - major +permissions: + id-token: write # Required for OIDC + jobs: release: runs-on: ubuntu-latest @@ -47,4 +50,4 @@ jobs: run: npm run release -- --ci ${{ github.event.inputs.version }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 19498c4954a103301e4fcabc0df070f911d5a7c2 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 22 Aug 2025 13:43:32 -0700 Subject: [PATCH 41/60] fix(settings): add WebFetch permission for npmjs.com documentation --- .claude/settings.local.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c1240a9..d39b8e8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -32,7 +32,8 @@ "Bash(do echo \"Run $i:\")", "Bash(break)", "Bash(done)", - "WebSearch" + "WebSearch", + "WebFetch(domain:docs.npmjs.com)" ], "deny": [] }, From 9bfe91437765285c4159a0d84e5a23f027b48479 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 22 Aug 2025 13:48:59 -0700 Subject: [PATCH 42/60] fix(release): migrate to OIDC/remove NODE_AUTH_TOKEN --- .github/workflows/release.yml | 1 - package.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e953098..af89e1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,5 +49,4 @@ jobs: - name: Release run: npm run release -- --ci ${{ github.event.inputs.version }} env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index 35a8877..c13fb7e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,9 @@ }, "github": { "release": true + }, + "npm": { + "publish": true } }, "author": "Matthew McEachen ", From c7ac74249f628d83fba7612239715f3c99087acc Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 22 Aug 2025 13:53:19 -0700 Subject: [PATCH 43/60] fix(release): consolidate permissions and add npm update/check steps --- .github/workflows/release.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af89e1d..f329b13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,13 +15,12 @@ on: permissions: id-token: write # Required for OIDC + contents: write + packages: write jobs: release: runs-on: ubuntu-latest - permissions: - contents: write - packages: write steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -43,6 +42,15 @@ jobs: git-user-name: ${{ secrets.GIT_USER_NAME }} git-user-email: ${{ secrets.GIT_USER_EMAIL }} + - name: Update npm to latest version + run: npm install -g npm@latest + + - name: Check npm configuration + run: | + npm version + npm config list + npm whoami 2>/dev/null || echo "Not logged in (expected with OIDC)" + - name: Install dependencies run: npm ci From 6e40c861c0ebf52c39aa54817f49952f5ed9ca9f Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 22 Aug 2025 14:02:24 -0700 Subject: [PATCH 44/60] fix(release): debug OIDC release --- .claude/settings.local.json | 3 +-- .github/workflows/release.yml | 14 +++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d39b8e8..c1240a9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -32,8 +32,7 @@ "Bash(do echo \"Run $i:\")", "Bash(break)", "Bash(done)", - "WebSearch", - "WebFetch(domain:docs.npmjs.com)" + "WebSearch" ], "deny": [] }, diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f329b13..3fe47b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,7 @@ on: permissions: id-token: write # Required for OIDC - contents: write - packages: write + contents: write # Required for release-it to create tags/commits jobs: release: @@ -27,13 +26,12 @@ jobs: with: fetch-depth: 0 # Need full history for release-it - # setup-node configures auth with registry-url and NODE_AUTH_TOKEN - # See: https://github.com/actions/setup-node#usage + # setup-node with registry-url is required for OIDC trusted publishing - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: "npm" - registry-url: "https://registry.npmjs.org/" + registry-url: "https://registry.npmjs.org" - name: Set up SSH signing uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v1.0.0 @@ -50,10 +48,16 @@ jobs: npm version npm config list npm whoami 2>/dev/null || echo "Not logged in (expected with OIDC)" + echo "NODE_AUTH_TOKEN set: ${NODE_AUTH_TOKEN:+yes}" + echo "NPM_CONFIG_USERCONFIG: $NPM_CONFIG_USERCONFIG" - name: Install dependencies run: npm ci + - name: Test npm publish (dry run) + run: npm publish --dry-run + continue-on-error: true + - name: Release run: npm run release -- --ci ${{ github.event.inputs.version }} env: From be620e4653037bb046c37c36f326f1764c3855b4 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 10:19:43 -0700 Subject: [PATCH 45/60] fix(release): update release workflow and package.json for OIDC trusted publishing --- .claude/settings.local.json | 2 +- .github/workflows/release.yml | 18 +++--------------- package.json | 4 +++- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c1240a9..6717548 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -37,4 +37,4 @@ "deny": [] }, "enableAllProjectMcpServers": false -} \ No newline at end of file +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fe47b4..2864a4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,25 +40,13 @@ jobs: git-user-name: ${{ secrets.GIT_USER_NAME }} git-user-email: ${{ secrets.GIT_USER_EMAIL }} - - name: Update npm to latest version - run: npm install -g npm@latest - - - name: Check npm configuration - run: | - npm version - npm config list - npm whoami 2>/dev/null || echo "Not logged in (expected with OIDC)" - echo "NODE_AUTH_TOKEN set: ${NODE_AUTH_TOKEN:+yes}" - echo "NPM_CONFIG_USERCONFIG: $NPM_CONFIG_USERCONFIG" - - name: Install dependencies run: npm ci - - name: Test npm publish (dry run) - run: npm publish --dry-run - continue-on-error: true + - name: Run tests + run: npm test - - name: Release + - name: Release with release-it run: npm run release -- --ci ${{ github.event.inputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index c13fb7e..e98f082 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,11 @@ "release": true }, "npm": { - "publish": true + "publish": true, + "skipChecks": true } }, + "# release-it.npm.skipChecks": "Required for OIDC Trusted Publishing - bypasses npm auth checks since OIDC handles authentication automatically. See: https://github.com/release-it/release-it/issues/1244 and https://docs.npmjs.com/trusted-publishers#supported-cicd-providers", "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { From ef0542cf5e907ec6428c5637a3a3b9d02da037d7 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 10:21:33 -0700 Subject: [PATCH 46/60] chore(deps): `npm run update` --- .github/workflows/node.js.yml | 2 +- .github/workflows/release.yml | 4 +- package-lock.json | 260 +++++++++++++++++----------------- package.json | 6 +- 4 files changed, 136 insertions(+), 136 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 4f638f9..6f550d1 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -61,7 +61,7 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Setup SSH Bot - uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v1.0.0 + uses: photostructure/git-ssh-signing-action@a770c2ff3aea31d9df9f2974ac9d672f2bfe62f3 # v1.1.0 with: ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} git-user-name: ${{ secrets.GIT_USER_NAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2864a4c..335a1b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,14 +27,14 @@ jobs: fetch-depth: 0 # Need full history for release-it # setup-node with registry-url is required for OIDC trusted publishing - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: 20 cache: "npm" registry-url: "https://registry.npmjs.org" - name: Set up SSH signing - uses: photostructure/git-ssh-signing-action@7a06ef30090b6755c6c9a4295e8afd95bf264bc1 # v1.0.0 + uses: photostructure/git-ssh-signing-action@a770c2ff3aea31d9df9f2974ac9d672f2bfe62f3 # v1.1.0 with: ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} git-user-name: ${{ secrets.GIT_USER_NAME }} diff --git a/package-lock.json b/package-lock.json index 47b4219..8f8f79a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "14.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.34.0", "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7", @@ -38,9 +38,9 @@ "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", - "typedoc": "^0.28.10", + "typedoc": "^0.28.11", "typescript": "~5.9.2", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.41.0" }, "engines": { "node": ">=20" @@ -177,9 +177,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -294,13 +294,13 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.1.tgz", - "integrity": "sha512-bevKGO6kX1eM/N+pdh9leS5L7TBF4ICrzi9a+cbWkrxeAeIcwlo/7OfWGCDERdRCI2/Q6tjltX4bt07ALHDwFw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.2.tgz", + "integrity": "sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", @@ -319,13 +319,13 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.15.tgz", - "integrity": "sha512-SwHMGa8Z47LawQN0rog0sT+6JpiL0B7eW9p1Bb7iCeKDGTI5Ez25TSc2l8kw52VV7hA4sX/C78CGkMrKXfuspA==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.16.tgz", + "integrity": "sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8" }, "engines": { @@ -341,9 +341,9 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.0.tgz", + "integrity": "sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==", "dev": true, "license": "MIT", "dependencies": { @@ -429,13 +429,13 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.17", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.17.tgz", - "integrity": "sha512-r6bQLsyPSzbWrZZ9ufoWL+CztkSatnJ6uSxqd6N+o41EZC51sQeWOzI6s5jLb+xxTWxl7PlUppqm8/sow241gg==", + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.18.tgz", + "integrity": "sha512-yeQN3AXjCm7+Hmq5L6Dm2wEDeBRdAZuyZ4I7tWSSanbxDzqM0KqzoDbKM7p4ebllAYdoQuPJS6N71/3L281i6w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/external-editor": "^1.0.1", "@inquirer/type": "^3.0.8" }, @@ -452,13 +452,13 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", - "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.18.tgz", + "integrity": "sha512-xUjteYtavH7HwDMzq4Cn2X4Qsh5NozoDHCJTdoXg9HfZ4w3R6mxV1B9tL7DGJX2eq/zqtsFjhm0/RJIMGlh3ag==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, @@ -507,13 +507,13 @@ } }, "node_modules/@inquirer/input": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", - "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.2.tgz", + "integrity": "sha512-hqOvBZj/MhQCpHUuD3MVq18SSoDNHy7wEnQ8mtvs71K8OPZVXJinOzcvQna33dNYLYE4LkA9BlhAhK6MJcsVbw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8" }, "engines": { @@ -529,13 +529,13 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", - "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.18.tgz", + "integrity": "sha512-7exgBm52WXZRczsydCVftozFTrrwbG5ySE0GqUd2zLNSBXyIucs2Wnm7ZKLe/aUu6NUg9dg7Q80QIHCdZJiY4A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8" }, "engines": { @@ -551,13 +551,13 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", - "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.18.tgz", + "integrity": "sha512-zXvzAGxPQTNk/SbT3carAD4Iqi6A2JS2qtcqQjsL22uvD+JfQzUrDEtPjLL7PLn8zlSNyPdY02IiQjzoL9TStA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" }, @@ -574,22 +574,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.3.tgz", - "integrity": "sha512-iHYp+JCaCRktM/ESZdpHI51yqsDgXu+dMs4semzETftOaF8u5hwlqnbIsuIR/LrWZl8Pm1/gzteK9I7MAq5HTA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.4.tgz", + "integrity": "sha512-MuxVZ1en1g5oGamXV3DWP89GEkdD54alcfhHd7InUW5BifAdKQEK9SLFa/5hlWbvuhMPlobF0WAx7Okq988Jxg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.2.1", - "@inquirer/confirm": "^5.1.15", - "@inquirer/editor": "^4.2.17", - "@inquirer/expand": "^4.0.17", - "@inquirer/input": "^4.2.1", - "@inquirer/number": "^3.0.17", - "@inquirer/password": "^4.0.17", - "@inquirer/rawlist": "^4.1.5", - "@inquirer/search": "^3.1.0", - "@inquirer/select": "^4.3.1" + "@inquirer/checkbox": "^4.2.2", + "@inquirer/confirm": "^5.1.16", + "@inquirer/editor": "^4.2.18", + "@inquirer/expand": "^4.0.18", + "@inquirer/input": "^4.2.2", + "@inquirer/number": "^3.0.18", + "@inquirer/password": "^4.0.18", + "@inquirer/rawlist": "^4.1.6", + "@inquirer/search": "^3.1.1", + "@inquirer/select": "^4.3.2" }, "engines": { "node": ">=18" @@ -604,13 +604,13 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", - "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.6.tgz", + "integrity": "sha512-KOZqa3QNr3f0pMnufzL7K+nweFFCCBs6LCXZzXDrVGTyssjLeudn5ySktZYv1XiSqobyHRYYK0c6QsOxJEhXKA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, @@ -627,13 +627,13 @@ } }, "node_modules/@inquirer/search": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", - "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.1.tgz", + "integrity": "sha512-TkMUY+A2p2EYVY3GCTItYGvqT6LiLzHBnqsU1rJbrpXUijFfM6zvUx0R4civofVwFCmJZcKqOVwwWAjplKkhxA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" @@ -651,13 +651,13 @@ } }, "node_modules/@inquirer/select": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", - "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.2.tgz", + "integrity": "sha512-nwous24r31M+WyDEHV+qckXkepvihxhnyIaod2MG7eCE6G0Zm/HUF6jgN8GXgf4U7AU6SLseKdanY195cwvU6w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", @@ -1230,17 +1230,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1254,7 +1254,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1270,16 +1270,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "engines": { @@ -1295,14 +1295,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "engines": { @@ -1317,14 +1317,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1335,9 +1335,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", "dev": true, "license": "MIT", "engines": { @@ -1352,15 +1352,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1377,9 +1377,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", "dev": true, "license": "MIT", "engines": { @@ -1391,16 +1391,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1459,16 +1459,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1483,13 +1483,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3123,9 +3123,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -3135,7 +3135,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7743,9 +7743,9 @@ } }, "node_modules/typedoc": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.10.tgz", - "integrity": "sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==", + "version": "0.28.11", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.11.tgz", + "integrity": "sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7807,16 +7807,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", - "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.41.0.tgz", + "integrity": "sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0" + "@typescript-eslint/eslint-plugin": "8.41.0", + "@typescript-eslint/parser": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index e98f082..55f27bd 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.34.0", "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7", @@ -87,8 +87,8 @@ "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", - "typedoc": "^0.28.10", + "typedoc": "^0.28.11", "typescript": "~5.9.2", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.41.0" } } From 7737d202119e6abef8e3b7f6b1bd24bdd5310b55 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 11:51:16 -0700 Subject: [PATCH 47/60] refactor(release): consolidate release workflow into node.js.yml and remove redundant release.yml --- .github/workflows/node.js.yml | 58 +++++++++++++++-------------------- .github/workflows/release.yml | 52 ------------------------------- 2 files changed, 24 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 6f550d1..6e71972 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -9,6 +9,16 @@ on: pull_request: branches: [main] workflow_dispatch: + inputs: + version: + description: "Version type (auto-detects from package.json if not specified)" + required: false + type: choice + options: + - "" + - patch + - minor + - major jobs: lint: @@ -46,21 +56,21 @@ jobs: needs: [lint, build] if: ${{ github.event_name == 'workflow_dispatch' }} permissions: - contents: write - packages: write + id-token: write # Required for OIDC + contents: write # Required for release-it to create tags/commits steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - # Fetch all history for proper versioning - fetch-depth: 0 + fetch-depth: 0 # Need full history for release-it - - name: Use Node.js 20 - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + # setup-node with registry-url is required for OIDC trusted publishing + - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: - node-version: "20" + node-version: 20 + cache: "npm" registry-url: "https://registry.npmjs.org" - - name: Setup SSH Bot + - name: Set up SSH signing uses: photostructure/git-ssh-signing-action@a770c2ff3aea31d9df9f2974ac9d672f2bfe62f3 # v1.1.0 with: ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} @@ -70,30 +80,10 @@ jobs: - name: Install dependencies run: npm ci - - name: Build and test - run: npm test - - - name: Create release - run: | - # Bump version and create signed commit and tag - npm version patch -m "release: %s" - - # Push the version commit and tag - git push --follow-tags - - - name: Publish to npm - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Create GitHub release - run: | - # Get the version from package.json - VERSION=$(node -p "require('./package.json').version") - - # Create GitHub release - gh release create "v${VERSION}" \ - --title "Release v${VERSION}" \ - --generate-notes + # Note: Tests are run by release-it's before:init hook via npm run lint -> pretest + # This avoids running the full test matrix (9+ OS/Node combinations) in the release workflow + # The pretest script (clean + lint + compile) is sufficient for release validation + - name: Release with release-it + run: npm run release -- --ci ${{ github.event.inputs.version }} env: - GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 335a1b8..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - version: - description: "Version type (auto-detects from package.json if not specified)" - required: false - type: choice - options: - - "" - - patch - - minor - - major - -permissions: - id-token: write # Required for OIDC - contents: write # Required for release-it to create tags/commits - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 # Need full history for release-it - - # setup-node with registry-url is required for OIDC trusted publishing - - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 - with: - node-version: 20 - cache: "npm" - registry-url: "https://registry.npmjs.org" - - - name: Set up SSH signing - uses: photostructure/git-ssh-signing-action@a770c2ff3aea31d9df9f2974ac9d672f2bfe62f3 # v1.1.0 - with: - ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }} - git-user-name: ${{ secrets.GIT_USER_NAME }} - git-user-email: ${{ secrets.GIT_USER_EMAIL }} - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test - - - name: Release with release-it - run: npm run release -- --ci ${{ github.event.inputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From bbfbfc552519dd9b5f2168242503a224d2a37510 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 11:57:43 -0700 Subject: [PATCH 48/60] chore(workflow): rename node.js.yml to build.yml --- .github/workflows/{node.js.yml => build.yml} | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{node.js.yml => build.yml} (99%) diff --git a/.github/workflows/node.js.yml b/.github/workflows/build.yml similarity index 99% rename from .github/workflows/node.js.yml rename to .github/workflows/build.yml index 6e71972..a90f5f7 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ # This is used by the build badge: -name: CI tests +name: Build & Release env: CI: 1 diff --git a/README.md b/README.md index ef38df5..e7a6e8a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Efficient, concurrent work via batch-mode command-line tools from within Node.js.** [![npm version](https://img.shields.io/npm/v/batch-cluster.svg)](https://www.npmjs.com/package/batch-cluster) -[![Build status](https://github.com/photostructure/batch-cluster.js/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/photostructure/batch-cluster.js/actions/workflows/node.js.yml) +[![Build status](https://github.com/photostructure/batch-cluster.js/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/photostructure/batch-cluster.js/actions/workflows/build.yml) [![GitHub issues](https://img.shields.io/github/issues/photostructure/batch-cluster.js.svg)](https://github.com/photostructure/batch-cluster.js/issues) [![CodeQL](https://github.com/photostructure/batch-cluster.js/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/photostructure/batch-cluster.js/actions/workflows/codeql-analysis.yml) [![Known Vulnerabilities](https://snyk.io/test/github/photostructure/batch-cluster.js/badge.svg?targetFile=package.json)](https://snyk.io/test/github/photostructure/batch-cluster.js?targetFile=package.json) From f8ceca31d0116153cfe29d16fd7b78f1bd9b7194 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 13:17:41 -0700 Subject: [PATCH 49/60] fix(tests): improve shutdown timeout handling for CI environments --- src/BatchCluster.spec.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index a1de9f5..dddb3c0 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -48,7 +48,8 @@ function arrayEqualish(a: T[], b: T[], maxAcceptableDiffs: number) { describe("BatchCluster", function () { const ErrorPrefix = "ERROR: "; - const ShutdownTimeoutMs = 12 * secondMs; + // Windows CI can be extremely slow to shut down processes, so we need a longer timeout + const ShutdownTimeoutMs = (isWin && isCI ? 30 : 12) * secondMs; function runTasks( bc: BatchCluster, @@ -117,6 +118,7 @@ describe("BatchCluster", function () { async function shutdown(bc: BatchCluster) { if (bc == null) return; // we skipped the spec + const shutdownStartTime = Date.now(); const endPromise = bc.end(true); // "ended" should be true immediately, but it may still be waiting for child // processes to exit: @@ -137,17 +139,21 @@ describe("BatchCluster", function () { pids.length === 0 && livingPids.length === 0; - if (!done) - console.log("shutdown(): waiting for end", { + if (!done) { + const elapsed = Date.now() - shutdownStartTime; + console.log(`shutdown(): waiting for end (${elapsed}ms elapsed)`, { runningCommands, busyProcCount, pids, livingPids, + platform: process.platform, + isCI, }); + } return done; } - // Mac CI can be extremely slow to shut down: + // CI environments (especially Windows and Mac) can be extremely slow to shut down: const endOrTimeout = await thenOrTimeout( endPromise.promise.then(() => true), ShutdownTimeoutMs, @@ -156,6 +162,16 @@ describe("BatchCluster", function () { until(checkShutdown, ShutdownTimeoutMs, 1000), ShutdownTimeoutMs, ); + + if (isCI && (endOrTimeout !== true || shutdownOrTimeout !== true)) { + console.log(`Shutdown timeout on CI after ${Date.now() - shutdownStartTime}ms`, { + endOrTimeout, + shutdownOrTimeout, + platform: process.platform, + ShutdownTimeoutMs, + }); + } + expect(endOrTimeout).to.eql(true, ".end() failed"); expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed"); From cc281fda482a2abde02c880e65c5aba0267be6db Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 14:33:35 -0700 Subject: [PATCH 50/60] fix(package): update repository URL format and enhance formatting scripts --- package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 55f27bd..ea599fd 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "types": "dist/BatchCluster.d.ts", "repository": { "type": "git", - "url": "https://github.com/photostructure/batch-cluster.js.git" + "url": "git+https://github.com/photostructure/batch-cluster.js.git" }, "engines": { "node": ">=20" @@ -18,7 +18,9 @@ "scripts": { "ci": "npm ci", "clean": "rimraf dist", - "fmt": "prettier --write .", + "fmt": "run-p fmt:*", + "fmt:pkg": "npm pkg fix", + "fmt:prettier": "prettier --write .", "lint": "eslint src", "compile": "tsc", "watch": "rimraf dist & tsc --watch", @@ -91,4 +93,4 @@ "typescript": "~5.9.2", "typescript-eslint": "^8.41.0" } -} +} \ No newline at end of file From a318216cc5674191b2d23b63bf2e9359c14ca352 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 14:33:46 -0700 Subject: [PATCH 51/60] chore(fmt): prettier --- src/BatchCluster.spec.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/BatchCluster.spec.ts b/src/BatchCluster.spec.ts index dddb3c0..24802e9 100644 --- a/src/BatchCluster.spec.ts +++ b/src/BatchCluster.spec.ts @@ -162,16 +162,19 @@ describe("BatchCluster", function () { until(checkShutdown, ShutdownTimeoutMs, 1000), ShutdownTimeoutMs, ); - + if (isCI && (endOrTimeout !== true || shutdownOrTimeout !== true)) { - console.log(`Shutdown timeout on CI after ${Date.now() - shutdownStartTime}ms`, { - endOrTimeout, - shutdownOrTimeout, - platform: process.platform, - ShutdownTimeoutMs, - }); + console.log( + `Shutdown timeout on CI after ${Date.now() - shutdownStartTime}ms`, + { + endOrTimeout, + shutdownOrTimeout, + platform: process.platform, + ShutdownTimeoutMs, + }, + ); } - + expect(endOrTimeout).to.eql(true, ".end() failed"); expect(shutdownOrTimeout).to.eql(true, ".checkShutdown() failed"); From 55d79131b0587f7b5017ce6f665c4f2bff25b88f Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 14:39:56 -0700 Subject: [PATCH 52/60] fix(package): add publishConfig for public access and provenance --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ea599fd..3ad833d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "type": "git", "url": "git+https://github.com/photostructure/batch-cluster.js.git" }, + "publishConfig": { + "access": "public", + "provenance": true + }, "engines": { "node": ">=20" }, @@ -93,4 +97,4 @@ "typescript": "~5.9.2", "typescript-eslint": "^8.41.0" } -} \ No newline at end of file +} From 37ec2f77ec4b05decbd1b1965898d8b79b0212d0 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 14:53:19 -0700 Subject: [PATCH 53/60] fix(workflow): add step to update npm to the latest version --- .github/workflows/build.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a90f5f7..c726ce1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,6 +77,15 @@ jobs: git-user-name: ${{ secrets.GIT_USER_NAME }} git-user-email: ${{ secrets.GIT_USER_EMAIL }} + - name: Update npm to latest + run: | + echo "Current npm version:" + npm --version + echo "Updating npm to latest..." + npm install -g npm@latest + echo "New npm version:" + npm --version + - name: Install dependencies run: npm ci From 093520f2b27c33c91b3804957db07d0d7748c247 Mon Sep 17 00:00:00 2001 From: photostructure-bot Date: Mon, 25 Aug 2025 22:11:30 +0000 Subject: [PATCH 54/60] Release 15.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f8f79a..0e2b37d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "batch-cluster", - "version": "14.0.0", + "version": "15.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "batch-cluster", - "version": "14.0.0", + "version": "15.0.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.34.0", diff --git a/package.json b/package.json index 3ad833d..b2f8a63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "batch-cluster", - "version": "14.0.0", + "version": "15.0.0", "description": "Manage a cluster of child processes", "main": "dist/BatchCluster.js", "homepage": "https://photostructure.github.io/batch-cluster.js/", From 253d3a53ecd44c1d37d685c212f5dde2cf217188 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 17:17:07 -0700 Subject: [PATCH 55/60] fix(workflow): update pre-init hooks to use npm ci, clean, and compile --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b2f8a63..13caaf6 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,9 @@ }, "hooks": { "before:init": [ - "npm install", - "npm run lint" + "npm ci", + "npm run clean", + "npm run compile" ] }, "github": { From 98b15614ba20fb2728eb5e51db8b1b82da53256f Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Mon, 25 Aug 2025 17:20:22 -0700 Subject: [PATCH 56/60] chore(changelog): add entry for v15.0.1 to clarify release issues --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab3f04..bc20ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ See [Semver](http://semver.org/). - ๐Ÿž Backwards-compatible bug fixes - ๐Ÿ“ฆ Minor packaging changes +- +## v15.0.1 + +"This time, with feeling" + +- ๐Ÿ“ฆ v15.0.0 automated the release to use OIDC ๐Ÿ‘, but the `compile` prerequisite was missed ๐Ÿคฆ, so v15.0.0 has _no code in it_ ๐Ÿชน. ## v15.0.0 From 5cfc0f44897e21c356c371b1c106d2934e003747 Mon Sep 17 00:00:00 2001 From: photostructure-bot Date: Tue, 26 Aug 2025 00:38:36 +0000 Subject: [PATCH 57/60] Release 15.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e2b37d..3ba1d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "batch-cluster", - "version": "15.0.0", + "version": "15.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "batch-cluster", - "version": "15.0.0", + "version": "15.0.1", "license": "MIT", "devDependencies": { "@eslint/js": "^9.34.0", diff --git a/package.json b/package.json index 13caaf6..5a313e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "batch-cluster", - "version": "15.0.0", + "version": "15.0.1", "description": "Manage a cluster of child processes", "main": "dist/BatchCluster.js", "homepage": "https://photostructure.github.io/batch-cluster.js/", From 5d3e194ed623aeee2cc0a566a571e6afd5308f51 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Wed, 27 Aug 2025 11:14:48 -0700 Subject: [PATCH 58/60] fix(workflow): update macOS version and node version format in build matrix (should be no-op) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c726ce1..6a1b4ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,9 +38,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-14, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] # See https://github.com/nodejs/release#release-schedule - node-version: [20.x, 22.x, 24.x] + node-version: [20, 22, 24] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 From 5b151ffa1bc72621cabe3b3c71ef6a7fbf2548ba Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 18 Sep 2025 13:38:54 -0700 Subject: [PATCH 59/60] fix(workflow): update setup-node action to v5.0.0 in build, codeql-analysis, and docs workflows --- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/docs.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a1b4ee..784a8e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: "20" - run: npm ci @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -64,7 +64,7 @@ jobs: fetch-depth: 0 # Need full history for release-it # setup-node with registry-url is required for OIDC trusted publishing - - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: 20 cache: "npm" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f1224cf..952dcd4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 + uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1a73b80..a3716c4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Node.js - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - name: Install dependencies run: npm ci From becf4ba58d623d27d678c3d4eaf9a347e3e379fd Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Thu, 18 Sep 2025 16:01:18 -0700 Subject: [PATCH 60/60] chore(dependencies): update npm-check-updates and ESLint dependencies; refine Dependabot and ncu config to include cooldown --- .github/dependabot.yml | 19 ++---- .ncurc.json | 4 +- package-lock.json | 137 +++++++++++++++++------------------------ package.json | 12 ++-- 4 files changed, 72 insertions(+), 100 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c0cf0be..048017e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,4 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference version: 2 updates: @@ -10,19 +7,13 @@ updates: directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 0 - ignore: - - dependency-name: "*" - update-types: - ["version-update:semver-minor", "version-update:semver-patch"] + cooldown: + default-days: 8 # Maintain dependencies for npm - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 0 - ignore: - - dependency-name: "*" - update-types: - ["version-update:semver-minor", "version-update:semver-patch"] + cooldown: + default-days: 8 diff --git a/.ncurc.json b/.ncurc.json index 3de3741..ae639fb 100644 --- a/.ncurc.json +++ b/.ncurc.json @@ -1,4 +1,6 @@ { + "$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json", + "cooldown": 4, "reject": [ "@types/chai", "@types/chai-as-promised", @@ -9,4 +11,4 @@ "rimraf", "rimraf why: https://github.com/isaacs/rimraf/issues/316 (!!)" ] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3ba1d14..2e41583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "15.0.1", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.34.0", + "@eslint/js": "^9.35.0", "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7", @@ -25,16 +25,16 @@ "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", "eslint-plugin-import": "^2.32.0", - "globals": "^16.3.0", - "mocha": "^11.7.1", - "npm-check-updates": "^18.0.2", + "globals": "^16.4.0", + "mocha": "^11.7.2", + "npm-check-updates": "^18.2.1", "npm-run-all": "4.1.5", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.2.0", "release-it": "^19.0.4", "rimraf": "^5.0.10", "seedrandom": "^3.0.5", - "serve": "^14.2.4", + "serve": "^14.2.5", "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2", @@ -177,9 +177,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -1507,43 +1507,6 @@ "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/accepts/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/accepts/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/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2509,24 +2472,34 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2544,13 +2517,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3307,6 +3273,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3914,9 +3893,9 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -5227,9 +5206,9 @@ } }, "node_modules/mocha": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", - "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.2.tgz", + "integrity": "sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5329,9 +5308,9 @@ "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==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "license": "MIT", "engines": { @@ -5415,9 +5394,9 @@ } }, "node_modules/npm-check-updates": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.2.tgz", - "integrity": "sha512-9uVFZUCg5oDOcbzdsrJ4BEvq2gikd23tXuF5mqpl4mxVl051lzB00Xmd7ZVjVWY3XNUF3BQKWlN/qmyD8/bwrA==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.2.1.tgz", + "integrity": "sha512-g1VjhAtGMSFFmmN5fT77aF9Eg9dZ6WG9WAqOv7RmWL2ANfeBZGgi6MxYwcNxwSIp5t7Nky0oNFEwHcG6EHQFKw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5744,9 +5723,9 @@ "license": "MIT" }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { @@ -6846,9 +6825,9 @@ } }, "node_modules/serve": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", - "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", "dev": true, "license": "MIT", "dependencies": { @@ -6859,7 +6838,7 @@ "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", - "compression": "1.7.4", + "compression": "1.8.1", "is-port-reachable": "4.0.0", "serve-handler": "6.1.6", "update-check": "1.5.4" diff --git a/package.json b/package.json index 5a313e0..85b9dbb 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "docs:build": "typedoc", "docs:serve": "cp .serve.json build/docs/serve.json && touch build/docs/.nojekyll && serve build/docs", "update": "run-p update:*", - "update:deps": "ncu -u --install always", + "update:deps": "npm-check-updates --upgrade --install always", "install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest", "update:actions": "pinact run -u", "release": "release-it", @@ -65,7 +65,7 @@ "author": "Matthew McEachen ", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.34.0", + "@eslint/js": "^9.35.0", "@sinonjs/fake-timers": "^14.0.0", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7", @@ -81,16 +81,16 @@ "chai-withintoleranceof": "^1.0.1", "eslint": "^9.27.0", "eslint-plugin-import": "^2.32.0", - "globals": "^16.3.0", - "mocha": "^11.7.1", - "npm-check-updates": "^18.0.2", + "globals": "^16.4.0", + "mocha": "^11.7.2", + "npm-check-updates": "^18.2.1", "npm-run-all": "4.1.5", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.2.0", "release-it": "^19.0.4", "rimraf": "^5.0.10", "seedrandom": "^3.0.5", - "serve": "^14.2.4", + "serve": "^14.2.5", "source-map-support": "^0.5.21", "split2": "^4.2.0", "ts-node": "^10.9.2",