diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 9430108979245..2ffb48325c816 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -274,6 +274,7 @@ graph LR; encoding-->iconv-lite; fdir-->picomatch; fs-minipass-->minipass; + gar-promise-retry-->retry; glob-->minimatch; glob-->minipass; glob-->path-scurry; @@ -312,6 +313,7 @@ graph LR; libnpmexec-->bin-links; libnpmexec-->chalk; libnpmexec-->ci-info; + libnpmexec-->gar-promise-retry["@gar/promise-retry"]; libnpmexec-->just-extend; libnpmexec-->just-safe-set; libnpmexec-->npm-package-arg; @@ -323,7 +325,6 @@ graph LR; libnpmexec-->npmcli-template-oss["@npmcli/template-oss"]; libnpmexec-->pacote; libnpmexec-->proc-log; - libnpmexec-->promise-retry; libnpmexec-->read; libnpmexec-->semver; libnpmexec-->signal-exit; diff --git a/node_modules/.gitignore b/node_modules/.gitignore index 8971f57cdafcb..68a75cf4c7b39 100644 --- a/node_modules/.gitignore +++ b/node_modules/.gitignore @@ -3,6 +3,12 @@ /* !/.gitignore # Allow all bundled deps +!/@gar/ +/@gar/* +!/@gar/promise-retry +!/@gar/promise-retry/node_modules/ +/@gar/promise-retry/node_modules/* +!/@gar/promise-retry/node_modules/retry !/@isaacs/ /@isaacs/* !/@isaacs/fs-minipass diff --git a/node_modules/@gar/promise-retry/LICENSE b/node_modules/@gar/promise-retry/LICENSE new file mode 100644 index 0000000000000..db5e914de1f58 --- /dev/null +++ b/node_modules/@gar/promise-retry/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 IndigoUnited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/node_modules/@gar/promise-retry/lib/index.js b/node_modules/@gar/promise-retry/lib/index.js new file mode 100644 index 0000000000000..fd034d88ac332 --- /dev/null +++ b/node_modules/@gar/promise-retry/lib/index.js @@ -0,0 +1,28 @@ +const retry = require('retry') + +const isRetryError = (err) => err?.code === 'EPROMISERETRY' && Object.hasOwn(err, 'retried') + +async function promiseRetry (fn, options = {}) { + const operation = retry.operation(options) + + return new Promise(function (resolve, reject) { + operation.attempt(async number => { + try { + const result = await fn(err => { + throw Object.assign(new Error('Retrying'), { code: 'EPROMISERETRY', retried: err }) + }, number) + return resolve(result) + } catch (err) { + if (isRetryError(err)) { + if (operation.retry(err.retried || new Error())) { + return + } + return reject(err.retried) + } + return reject(err) + } + }) + }) +} + +module.exports = { promiseRetry } diff --git a/node_modules/@gar/promise-retry/node_modules/retry/License b/node_modules/@gar/promise-retry/node_modules/retry/License new file mode 100644 index 0000000000000..0b58de379fb30 --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/License @@ -0,0 +1,21 @@ +Copyright (c) 2011: +Tim Koschützki (tim@debuggable.com) +Felix Geisendörfer (felix@debuggable.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/node_modules/@gar/promise-retry/node_modules/retry/example/dns.js b/node_modules/@gar/promise-retry/node_modules/retry/example/dns.js new file mode 100644 index 0000000000000..446729b6f9af6 --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/example/dns.js @@ -0,0 +1,31 @@ +var dns = require('dns'); +var retry = require('../lib/retry'); + +function faultTolerantResolve(address, cb) { + var opts = { + retries: 2, + factor: 2, + minTimeout: 1 * 1000, + maxTimeout: 2 * 1000, + randomize: true + }; + var operation = retry.operation(opts); + + operation.attempt(function(currentAttempt) { + dns.resolve(address, function(err, addresses) { + if (operation.retry(err)) { + return; + } + + cb(operation.mainError(), operation.errors(), addresses); + }); + }); +} + +faultTolerantResolve('nodejs.org', function(err, errors, addresses) { + console.warn('err:'); + console.log(err); + + console.warn('addresses:'); + console.log(addresses); +}); \ No newline at end of file diff --git a/node_modules/@gar/promise-retry/node_modules/retry/example/stop.js b/node_modules/@gar/promise-retry/node_modules/retry/example/stop.js new file mode 100644 index 0000000000000..e1ceafeebafc5 --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/example/stop.js @@ -0,0 +1,40 @@ +var retry = require('../lib/retry'); + +function attemptAsyncOperation(someInput, cb) { + var opts = { + retries: 2, + factor: 2, + minTimeout: 1 * 1000, + maxTimeout: 2 * 1000, + randomize: true + }; + var operation = retry.operation(opts); + + operation.attempt(function(currentAttempt) { + failingAsyncOperation(someInput, function(err, result) { + + if (err && err.message === 'A fatal error') { + operation.stop(); + return cb(err); + } + + if (operation.retry(err)) { + return; + } + + cb(operation.mainError(), operation.errors(), result); + }); + }); +} + +attemptAsyncOperation('test input', function(err, errors, result) { + console.warn('err:'); + console.log(err); + + console.warn('result:'); + console.log(result); +}); + +function failingAsyncOperation(input, cb) { + return setImmediate(cb.bind(null, new Error('A fatal error'))); +} diff --git a/node_modules/@gar/promise-retry/node_modules/retry/index.js b/node_modules/@gar/promise-retry/node_modules/retry/index.js new file mode 100644 index 0000000000000..ee62f3a112c28 --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/retry'); \ No newline at end of file diff --git a/node_modules/@gar/promise-retry/node_modules/retry/lib/retry.js b/node_modules/@gar/promise-retry/node_modules/retry/lib/retry.js new file mode 100644 index 0000000000000..5e85e79197d36 --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/lib/retry.js @@ -0,0 +1,100 @@ +var RetryOperation = require('./retry_operation'); + +exports.operation = function(options) { + var timeouts = exports.timeouts(options); + return new RetryOperation(timeouts, { + forever: options && (options.forever || options.retries === Infinity), + unref: options && options.unref, + maxRetryTime: options && options.maxRetryTime + }); +}; + +exports.timeouts = function(options) { + if (options instanceof Array) { + return [].concat(options); + } + + var opts = { + retries: 10, + factor: 2, + minTimeout: 1 * 1000, + maxTimeout: Infinity, + randomize: false + }; + for (var key in options) { + opts[key] = options[key]; + } + + if (opts.minTimeout > opts.maxTimeout) { + throw new Error('minTimeout is greater than maxTimeout'); + } + + var timeouts = []; + for (var i = 0; i < opts.retries; i++) { + timeouts.push(this.createTimeout(i, opts)); + } + + if (options && options.forever && !timeouts.length) { + timeouts.push(this.createTimeout(i, opts)); + } + + // sort the array numerically ascending + timeouts.sort(function(a,b) { + return a - b; + }); + + return timeouts; +}; + +exports.createTimeout = function(attempt, opts) { + var random = (opts.randomize) + ? (Math.random() + 1) + : 1; + + var timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt)); + timeout = Math.min(timeout, opts.maxTimeout); + + return timeout; +}; + +exports.wrap = function(obj, options, methods) { + if (options instanceof Array) { + methods = options; + options = null; + } + + if (!methods) { + methods = []; + for (var key in obj) { + if (typeof obj[key] === 'function') { + methods.push(key); + } + } + } + + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + var original = obj[method]; + + obj[method] = function retryWrapper(original) { + var op = exports.operation(options); + var args = Array.prototype.slice.call(arguments, 1); + var callback = args.pop(); + + args.push(function(err) { + if (op.retry(err)) { + return; + } + if (err) { + arguments[0] = op.mainError(); + } + callback.apply(this, arguments); + }); + + op.attempt(function() { + original.apply(obj, args); + }); + }.bind(obj, original); + obj[method].options = options; + } +}; diff --git a/node_modules/@gar/promise-retry/node_modules/retry/lib/retry_operation.js b/node_modules/@gar/promise-retry/node_modules/retry/lib/retry_operation.js new file mode 100644 index 0000000000000..105ce72b2be8e --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/lib/retry_operation.js @@ -0,0 +1,162 @@ +function RetryOperation(timeouts, options) { + // Compatibility for the old (timeouts, retryForever) signature + if (typeof options === 'boolean') { + options = { forever: options }; + } + + this._originalTimeouts = JSON.parse(JSON.stringify(timeouts)); + this._timeouts = timeouts; + this._options = options || {}; + this._maxRetryTime = options && options.maxRetryTime || Infinity; + this._fn = null; + this._errors = []; + this._attempts = 1; + this._operationTimeout = null; + this._operationTimeoutCb = null; + this._timeout = null; + this._operationStart = null; + this._timer = null; + + if (this._options.forever) { + this._cachedTimeouts = this._timeouts.slice(0); + } +} +module.exports = RetryOperation; + +RetryOperation.prototype.reset = function() { + this._attempts = 1; + this._timeouts = this._originalTimeouts.slice(0); +} + +RetryOperation.prototype.stop = function() { + if (this._timeout) { + clearTimeout(this._timeout); + } + if (this._timer) { + clearTimeout(this._timer); + } + + this._timeouts = []; + this._cachedTimeouts = null; +}; + +RetryOperation.prototype.retry = function(err) { + if (this._timeout) { + clearTimeout(this._timeout); + } + + if (!err) { + return false; + } + var currentTime = new Date().getTime(); + if (err && currentTime - this._operationStart >= this._maxRetryTime) { + this._errors.push(err); + this._errors.unshift(new Error('RetryOperation timeout occurred')); + return false; + } + + this._errors.push(err); + + var timeout = this._timeouts.shift(); + if (timeout === undefined) { + if (this._cachedTimeouts) { + // retry forever, only keep last error + this._errors.splice(0, this._errors.length - 1); + timeout = this._cachedTimeouts.slice(-1); + } else { + return false; + } + } + + var self = this; + this._timer = setTimeout(function() { + self._attempts++; + + if (self._operationTimeoutCb) { + self._timeout = setTimeout(function() { + self._operationTimeoutCb(self._attempts); + }, self._operationTimeout); + + if (self._options.unref) { + self._timeout.unref(); + } + } + + self._fn(self._attempts); + }, timeout); + + if (this._options.unref) { + this._timer.unref(); + } + + return true; +}; + +RetryOperation.prototype.attempt = function(fn, timeoutOps) { + this._fn = fn; + + if (timeoutOps) { + if (timeoutOps.timeout) { + this._operationTimeout = timeoutOps.timeout; + } + if (timeoutOps.cb) { + this._operationTimeoutCb = timeoutOps.cb; + } + } + + var self = this; + if (this._operationTimeoutCb) { + this._timeout = setTimeout(function() { + self._operationTimeoutCb(); + }, self._operationTimeout); + } + + this._operationStart = new Date().getTime(); + + this._fn(this._attempts); +}; + +RetryOperation.prototype.try = function(fn) { + console.log('Using RetryOperation.try() is deprecated'); + this.attempt(fn); +}; + +RetryOperation.prototype.start = function(fn) { + console.log('Using RetryOperation.start() is deprecated'); + this.attempt(fn); +}; + +RetryOperation.prototype.start = RetryOperation.prototype.try; + +RetryOperation.prototype.errors = function() { + return this._errors; +}; + +RetryOperation.prototype.attempts = function() { + return this._attempts; +}; + +RetryOperation.prototype.mainError = function() { + if (this._errors.length === 0) { + return null; + } + + var counts = {}; + var mainError = null; + var mainErrorCount = 0; + + for (var i = 0; i < this._errors.length; i++) { + var error = this._errors[i]; + var message = error.message; + var count = (counts[message] || 0) + 1; + + counts[message] = count; + + if (count >= mainErrorCount) { + mainError = error; + mainErrorCount = count; + } + } + + return mainError; +}; diff --git a/node_modules/@gar/promise-retry/node_modules/retry/package.json b/node_modules/@gar/promise-retry/node_modules/retry/package.json new file mode 100644 index 0000000000000..48f35e8cff285 --- /dev/null +++ b/node_modules/@gar/promise-retry/node_modules/retry/package.json @@ -0,0 +1,36 @@ +{ + "author": "Tim Koschützki (http://debuggable.com/)", + "name": "retry", + "description": "Abstraction for exponential and custom retry strategies for failed operations.", + "license": "MIT", + "version": "0.13.1", + "homepage": "https://github.com/tim-kos/node-retry", + "repository": { + "type": "git", + "url": "git://github.com/tim-kos/node-retry.git" + }, + "files": [ + "lib", + "example" + ], + "directories": { + "lib": "./lib" + }, + "main": "index.js", + "engines": { + "node": ">= 4" + }, + "dependencies": {}, + "devDependencies": { + "fake": "0.2.0", + "istanbul": "^0.4.5", + "tape": "^4.8.0" + }, + "scripts": { + "test": "./node_modules/.bin/istanbul cover ./node_modules/tape/bin/tape ./test/integration/*.js", + "release:major": "env SEMANTIC=major npm run release", + "release:minor": "env SEMANTIC=minor npm run release", + "release:patch": "env SEMANTIC=patch npm run release", + "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && git push && git push --tags && npm publish" + } +} diff --git a/node_modules/@gar/promise-retry/package.json b/node_modules/@gar/promise-retry/package.json new file mode 100644 index 0000000000000..9810a964ec94a --- /dev/null +++ b/node_modules/@gar/promise-retry/package.json @@ -0,0 +1,40 @@ +{ + "name": "@gar/promise-retry", + "version": "1.0.0", + "description": "Retries a function that returns a promise, leveraging the power of the retry module.", + "main": "./lib/index.js", + "files": [ + "lib" + ], + "type": "commonjs", + "exports": { + "." : [ { "default": "./lib/index.js" }, "./lib/index.js" ] + }, + "scripts": { + "lint": "npx standard", + "lint:fix": "npx standard --fix", + "test": "node --test --experimental-test-coverage --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100", + "posttest": "npm run lint" + }, + "bugs": { + "url": "https://github.com/wraithgar/node-promise-retry/issues/" + }, + "repository": { + "type": "git", + "url": "git://github.com/wraithgar/node-promise-retry.git" + }, + "keywords": [ + "retry", + "promise", + "backoff", + "repeat", + "replay" + ], + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } +} diff --git a/package-lock.json b/package-lock.json index 64ff854d71f76..549adf011dd84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1177,6 +1177,27 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@gar/promise-retry": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.0.tgz", + "integrity": "sha512-KcKKfklNXm3lop072VT58NnhnZYmMJmgKps9aqRT58tRDt939lnBT0frYR052xDpX6kdQB4AU05l/P3LU7dZxg==", + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@gar/promise-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@google-automations/git-file-utils": { "version": "3.0.0", "dev": true, @@ -14524,6 +14545,7 @@ "version": "10.2.2", "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/arborist": "^9.3.1", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", @@ -14531,7 +14553,6 @@ "npm-package-arg": "^13.0.0", "pacote": "^21.0.2", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "read": "^5.0.1", "semver": "^7.3.7", "signal-exit": "^4.1.0", diff --git a/workspaces/libnpmexec/lib/with-lock.js b/workspaces/libnpmexec/lib/with-lock.js index c7ba531ca5484..f6cc381c14602 100644 --- a/workspaces/libnpmexec/lib/with-lock.js +++ b/workspaces/libnpmexec/lib/with-lock.js @@ -1,6 +1,6 @@ const fs = require('node:fs/promises') const { rmdirSync } = require('node:fs') -const promiseRetry = require('promise-retry') +const { promiseRetry } = require('@gar/promise-retry') const { onExit } = require('signal-exit') // a lockfile implementation inspired by the unmaintained proper-lockfile library @@ -67,12 +67,7 @@ async function withLock (lockPath, cb) { } function acquireLock (lockPath) { - return promiseRetry({ - minTimeout: 100, - maxTimeout: 5_000, - // if another process legitimately holds the lock, wait for it to release; if it dies abnormally and the lock becomes stale, we'll acquire it automatically - forever: true, - }, async (retry) => { + return promiseRetry(async (retry) => { try { await fs.mkdir(lockPath) } catch (err) { @@ -107,6 +102,11 @@ function acquireLock (lockPath) { } catch (err) { throw Object.assign(new Error('Lock compromised'), { code: 'ECOMPROMISED' }) } + }, { + minTimeout: 100, + maxTimeout: 5_000, + // if another process legitimately holds the lock, wait for it to release; if it dies abnormally and the lock becomes stale, we'll acquire it automatically + forever: true, }) } diff --git a/workspaces/libnpmexec/package.json b/workspaces/libnpmexec/package.json index 08c47ec67cc26..953019c2a9c17 100644 --- a/workspaces/libnpmexec/package.json +++ b/workspaces/libnpmexec/package.json @@ -60,6 +60,7 @@ "tap": "^16.3.8" }, "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/arborist": "^9.3.1", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", @@ -67,7 +68,6 @@ "npm-package-arg": "^13.0.0", "pacote": "^21.0.2", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "read": "^5.0.1", "semver": "^7.3.7", "signal-exit": "^4.1.0",