fast-npm-meta 0.4.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +54 -1
- package/dist/index.mjs +277 -4
- package/package.json +4 -1
package/dist/index.d.mts
CHANGED
|
@@ -28,6 +28,7 @@ interface PackageVersionsInfoWithMetadata extends PackageManifest {
|
|
|
28
28
|
specifier: string;
|
|
29
29
|
}
|
|
30
30
|
interface PackageError {
|
|
31
|
+
status: number;
|
|
31
32
|
name: string;
|
|
32
33
|
error: string;
|
|
33
34
|
}
|
|
@@ -45,6 +46,40 @@ interface ResolvedPackageVersion {
|
|
|
45
46
|
interface ResolvedPackageVersionWithMetadata extends ResolvedPackageVersion, PackageVersionMeta {
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
interface RetryOptions {
|
|
50
|
+
/**
|
|
51
|
+
* The number of times to retry the operation.
|
|
52
|
+
*
|
|
53
|
+
* @default 5
|
|
54
|
+
*/
|
|
55
|
+
retries?: number;
|
|
56
|
+
/**
|
|
57
|
+
* The exponential factor to use.
|
|
58
|
+
*
|
|
59
|
+
* @default 2
|
|
60
|
+
*/
|
|
61
|
+
factor?: number;
|
|
62
|
+
/**
|
|
63
|
+
* The number of milliseconds before starting the first retry.
|
|
64
|
+
*
|
|
65
|
+
* Set this to `0` to retry immediately with no delay.
|
|
66
|
+
*
|
|
67
|
+
* @default 1000
|
|
68
|
+
*/
|
|
69
|
+
minTimeout?: number;
|
|
70
|
+
/**
|
|
71
|
+
* The maximum number of milliseconds between two retries.
|
|
72
|
+
*
|
|
73
|
+
* @default Infinity
|
|
74
|
+
*/
|
|
75
|
+
maxTimeout?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Randomizes the timeouts by multiplying with a factor between 1 and 2.
|
|
78
|
+
*
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
randomize?: boolean;
|
|
82
|
+
}
|
|
48
83
|
interface FetchOptions<Throw extends boolean = true> {
|
|
49
84
|
/**
|
|
50
85
|
* API endpoint for fetching package versions
|
|
@@ -90,6 +125,15 @@ interface GetVersionsOptions<Metadata extends boolean = false, Throw extends boo
|
|
|
90
125
|
* @default undefined
|
|
91
126
|
*/
|
|
92
127
|
after?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Retry options for the built-in retry mechanism
|
|
130
|
+
*
|
|
131
|
+
* Can be:
|
|
132
|
+
* - `RetryOptions` object for fine-grained control
|
|
133
|
+
* - `number` for simple retry count (uses defaults for other options)
|
|
134
|
+
* - `false` to disable retries
|
|
135
|
+
*/
|
|
136
|
+
retry?: RetryOptions | number | false;
|
|
93
137
|
}
|
|
94
138
|
interface GetLatestVersionOptions<Metadata extends boolean = false, Throw extends boolean = true> extends FetchOptions<Throw> {
|
|
95
139
|
/**
|
|
@@ -104,6 +148,15 @@ interface GetLatestVersionOptions<Metadata extends boolean = false, Throw extend
|
|
|
104
148
|
* @default false
|
|
105
149
|
*/
|
|
106
150
|
metadata?: Metadata;
|
|
151
|
+
/**
|
|
152
|
+
* Retry options for the built-in retry mechanism
|
|
153
|
+
*
|
|
154
|
+
* Can be:
|
|
155
|
+
* - `RetryOptions` object for fine-grained control
|
|
156
|
+
* - `number` for simple retry count (uses defaults for other options)
|
|
157
|
+
* - `false` to disable retries
|
|
158
|
+
*/
|
|
159
|
+
retry?: RetryOptions | number | false;
|
|
107
160
|
}
|
|
108
161
|
type InferGetVersionsResult<Metadata, Throw> = Metadata extends true ? Throw extends true ? PackageVersionsInfoWithMetadata : MaybeError<PackageVersionsInfoWithMetadata> : Throw extends true ? PackageVersionsInfo : MaybeError<PackageVersionsInfo>;
|
|
109
162
|
type InferGetLatestVersionResult<Metadata, Throw> = Metadata extends true ? Throw extends true ? ResolvedPackageVersionWithMetadata : MaybeError<ResolvedPackageVersionWithMetadata> : Throw extends true ? ResolvedPackageVersion : MaybeError<ResolvedPackageVersion>;
|
|
@@ -131,4 +184,4 @@ declare const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
|
131
184
|
declare function pickRegistry(scope: string | null | undefined, npmConfigs: Record<string, unknown>, defaultRegistry?: string): string;
|
|
132
185
|
|
|
133
186
|
export { NPM_REGISTRY, defaultOptions, getLatestVersion, getLatestVersionBatch, getVersions, getVersionsBatch, pickRegistry };
|
|
134
|
-
export type { Engines, FetchOptions, GetLatestVersionOptions, GetVersionsOptions, InferGetLatestVersionResult, InferGetVersionsResult, MaybeError, PackageError, PackageManifest, PackageManifestError, PackageVersionMeta, PackageVersionsInfo, PackageVersionsInfoWithMetadata, ResolvedPackageVersion, ResolvedPackageVersionWithMetadata };
|
|
187
|
+
export type { Engines, FetchOptions, GetLatestVersionOptions, GetVersionsOptions, InferGetLatestVersionResult, InferGetVersionsResult, MaybeError, PackageError, PackageManifest, PackageManifestError, PackageVersionMeta, PackageVersionsInfo, PackageVersionsInfoWithMetadata, ResolvedPackageVersion, ResolvedPackageVersionWithMetadata, RetryOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,268 @@
|
|
|
1
|
+
const objectToString = Object.prototype.toString;
|
|
2
|
+
|
|
3
|
+
const isError = value => objectToString.call(value) === '[object Error]';
|
|
4
|
+
|
|
5
|
+
const errorMessages = new Set([
|
|
6
|
+
'network error', // Chrome
|
|
7
|
+
'Failed to fetch', // Chrome
|
|
8
|
+
'NetworkError when attempting to fetch resource.', // Firefox
|
|
9
|
+
'The Internet connection appears to be offline.', // Safari 16
|
|
10
|
+
'Network request failed', // `cross-fetch`
|
|
11
|
+
'fetch failed', // Undici (Node.js)
|
|
12
|
+
'terminated', // Undici (Node.js)
|
|
13
|
+
' A network error occurred.', // Bun (WebKit)
|
|
14
|
+
'Network connection lost', // Cloudflare Workers (fetch)
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function isNetworkError(error) {
|
|
18
|
+
const isValid = error
|
|
19
|
+
&& isError(error)
|
|
20
|
+
&& error.name === 'TypeError'
|
|
21
|
+
&& typeof error.message === 'string';
|
|
22
|
+
|
|
23
|
+
if (!isValid) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {message, stack} = error;
|
|
28
|
+
|
|
29
|
+
// Safari 17+ has generic message but no stack for network errors
|
|
30
|
+
if (message === 'Load failed') {
|
|
31
|
+
return stack === undefined
|
|
32
|
+
// Sentry adds its own stack trace to the fetch error, so also check for that
|
|
33
|
+
|| '__sentry_captured__' in error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Deno network errors start with specific text
|
|
37
|
+
if (message.startsWith('error sending request for url')) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Standard network error messages
|
|
42
|
+
return errorMessages.has(message);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateRetries(retries) {
|
|
46
|
+
if (typeof retries === 'number') {
|
|
47
|
+
if (retries < 0) {
|
|
48
|
+
throw new TypeError('Expected `retries` to be a non-negative number.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Number.isNaN(retries)) {
|
|
52
|
+
throw new TypeError('Expected `retries` to be a valid number or Infinity, got NaN.');
|
|
53
|
+
}
|
|
54
|
+
} else if (retries !== undefined) {
|
|
55
|
+
throw new TypeError('Expected `retries` to be a number or Infinity.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateNumberOption(name, value, {min = 0, allowInfinity = false} = {}) {
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
65
|
+
throw new TypeError(`Expected \`${name}\` to be a number${allowInfinity ? ' or Infinity' : ''}.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!allowInfinity && !Number.isFinite(value)) {
|
|
69
|
+
throw new TypeError(`Expected \`${name}\` to be a finite number.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value < min) {
|
|
73
|
+
throw new TypeError(`Expected \`${name}\` to be \u2265 ${min}.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class AbortError extends Error {
|
|
78
|
+
constructor(message) {
|
|
79
|
+
super();
|
|
80
|
+
|
|
81
|
+
if (message instanceof Error) {
|
|
82
|
+
this.originalError = message;
|
|
83
|
+
({message} = message);
|
|
84
|
+
} else {
|
|
85
|
+
this.originalError = new Error(message);
|
|
86
|
+
this.originalError.stack = this.stack;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.name = 'AbortError';
|
|
90
|
+
this.message = message;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function calculateDelay(retriesConsumed, options) {
|
|
95
|
+
const attempt = Math.max(1, retriesConsumed + 1);
|
|
96
|
+
const random = options.randomize ? (Math.random() + 1) : 1;
|
|
97
|
+
|
|
98
|
+
let timeout = Math.round(random * options.minTimeout * (options.factor ** (attempt - 1)));
|
|
99
|
+
timeout = Math.min(timeout, options.maxTimeout);
|
|
100
|
+
|
|
101
|
+
return timeout;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function calculateRemainingTime(start, max) {
|
|
105
|
+
if (!Number.isFinite(max)) {
|
|
106
|
+
return max;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return max - (performance.now() - start);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function onAttemptFailure({error, attemptNumber, retriesConsumed, startTime, options}) {
|
|
113
|
+
const normalizedError = error instanceof Error
|
|
114
|
+
? error
|
|
115
|
+
: new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
|
|
116
|
+
|
|
117
|
+
if (normalizedError instanceof AbortError) {
|
|
118
|
+
throw normalizedError.originalError;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const retriesLeft = Number.isFinite(options.retries)
|
|
122
|
+
? Math.max(0, options.retries - retriesConsumed)
|
|
123
|
+
: options.retries;
|
|
124
|
+
|
|
125
|
+
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
|
|
126
|
+
|
|
127
|
+
const context = Object.freeze({
|
|
128
|
+
error: normalizedError,
|
|
129
|
+
attemptNumber,
|
|
130
|
+
retriesLeft,
|
|
131
|
+
retriesConsumed,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await options.onFailedAttempt(context);
|
|
135
|
+
|
|
136
|
+
if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
|
|
137
|
+
throw normalizedError;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const consumeRetry = await options.shouldConsumeRetry(context);
|
|
141
|
+
|
|
142
|
+
const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
|
|
143
|
+
|
|
144
|
+
if (remainingTime <= 0 || retriesLeft <= 0) {
|
|
145
|
+
throw normalizedError;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
|
|
149
|
+
if (consumeRetry) {
|
|
150
|
+
throw normalizedError;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
options.signal?.throwIfAborted();
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!await options.shouldRetry(context)) {
|
|
158
|
+
throw normalizedError;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!consumeRetry) {
|
|
162
|
+
options.signal?.throwIfAborted();
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const delayTime = calculateDelay(retriesConsumed, options);
|
|
167
|
+
const finalDelay = Math.min(delayTime, remainingTime);
|
|
168
|
+
|
|
169
|
+
if (finalDelay > 0) {
|
|
170
|
+
await new Promise((resolve, reject) => {
|
|
171
|
+
const onAbort = () => {
|
|
172
|
+
clearTimeout(timeoutToken);
|
|
173
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
174
|
+
reject(options.signal.reason);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const timeoutToken = setTimeout(() => {
|
|
178
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
179
|
+
resolve();
|
|
180
|
+
}, finalDelay);
|
|
181
|
+
|
|
182
|
+
if (options.unref) {
|
|
183
|
+
timeoutToken.unref?.();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
options.signal?.addEventListener('abort', onAbort, {once: true});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
options.signal?.throwIfAborted();
|
|
191
|
+
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function pRetry(input, options = {}) {
|
|
196
|
+
options = {...options};
|
|
197
|
+
|
|
198
|
+
validateRetries(options.retries);
|
|
199
|
+
|
|
200
|
+
if (Object.hasOwn(options, 'forever')) {
|
|
201
|
+
throw new Error('The `forever` option is no longer supported. For many use-cases, you can set `retries: Infinity` instead.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
options.retries ??= 10;
|
|
205
|
+
options.factor ??= 2;
|
|
206
|
+
options.minTimeout ??= 1000;
|
|
207
|
+
options.maxTimeout ??= Number.POSITIVE_INFINITY;
|
|
208
|
+
options.maxRetryTime ??= Number.POSITIVE_INFINITY;
|
|
209
|
+
options.randomize ??= false;
|
|
210
|
+
options.onFailedAttempt ??= () => {};
|
|
211
|
+
options.shouldRetry ??= () => true;
|
|
212
|
+
options.shouldConsumeRetry ??= () => true;
|
|
213
|
+
|
|
214
|
+
// Validate numeric options and normalize edge cases
|
|
215
|
+
validateNumberOption('factor', options.factor, {min: 0, allowInfinity: false});
|
|
216
|
+
validateNumberOption('minTimeout', options.minTimeout, {min: 0, allowInfinity: false});
|
|
217
|
+
validateNumberOption('maxTimeout', options.maxTimeout, {min: 0, allowInfinity: true});
|
|
218
|
+
validateNumberOption('maxRetryTime', options.maxRetryTime, {min: 0, allowInfinity: true});
|
|
219
|
+
|
|
220
|
+
// Treat non-positive factor as 1 to avoid zero backoff or negative behavior
|
|
221
|
+
if (!(options.factor > 0)) {
|
|
222
|
+
options.factor = 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
options.signal?.throwIfAborted();
|
|
226
|
+
|
|
227
|
+
let attemptNumber = 0;
|
|
228
|
+
let retriesConsumed = 0;
|
|
229
|
+
const startTime = performance.now();
|
|
230
|
+
|
|
231
|
+
while (Number.isFinite(options.retries) ? retriesConsumed <= options.retries : true) {
|
|
232
|
+
attemptNumber++;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
options.signal?.throwIfAborted();
|
|
236
|
+
|
|
237
|
+
const result = await input(attemptNumber);
|
|
238
|
+
|
|
239
|
+
options.signal?.throwIfAborted();
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (await onAttemptFailure({
|
|
244
|
+
error,
|
|
245
|
+
attemptNumber,
|
|
246
|
+
retriesConsumed,
|
|
247
|
+
startTime,
|
|
248
|
+
options,
|
|
249
|
+
})) {
|
|
250
|
+
retriesConsumed++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Should not reach here, but in case it does, throw an error
|
|
256
|
+
throw new Error('Retry attempts exhausted without throwing an error.');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const defaultRetryOptions = {
|
|
260
|
+
retries: 5,
|
|
261
|
+
factor: 2,
|
|
262
|
+
minTimeout: 1e3,
|
|
263
|
+
maxTimeout: Infinity,
|
|
264
|
+
randomize: false
|
|
265
|
+
};
|
|
1
266
|
const defaultOptions = {
|
|
2
267
|
/**
|
|
3
268
|
* API endpoint for fetching package versions
|
|
@@ -10,7 +275,8 @@ async function getLatestVersionBatch(packages, options = {}) {
|
|
|
10
275
|
const {
|
|
11
276
|
apiEndpoint = defaultOptions.apiEndpoint,
|
|
12
277
|
fetch: fetchApi = fetch,
|
|
13
|
-
throw: throwError = true
|
|
278
|
+
throw: throwError = true,
|
|
279
|
+
retry = defaultRetryOptions
|
|
14
280
|
} = options;
|
|
15
281
|
let query = [
|
|
16
282
|
options.force ? "force=true" : "",
|
|
@@ -19,7 +285,9 @@ async function getLatestVersionBatch(packages, options = {}) {
|
|
|
19
285
|
].filter(Boolean).join("&");
|
|
20
286
|
if (query)
|
|
21
287
|
query = `?${query}`;
|
|
22
|
-
const
|
|
288
|
+
const fetchFn = () => fetchApi(new URL(packages.join("+") + query, apiEndpoint)).then((r) => r.json());
|
|
289
|
+
const retryOptions = typeof retry === "number" ? { ...defaultRetryOptions, retries: retry } : retry;
|
|
290
|
+
const data = await (retryOptions === false ? fetchFn() : pRetry(fetchFn, retryOptions));
|
|
23
291
|
const list = toArray(data);
|
|
24
292
|
return throwError ? throwErrorObject(list) : list;
|
|
25
293
|
}
|
|
@@ -31,7 +299,8 @@ async function getVersionsBatch(packages, options = {}) {
|
|
|
31
299
|
const {
|
|
32
300
|
apiEndpoint = defaultOptions.apiEndpoint,
|
|
33
301
|
fetch: fetchApi = fetch,
|
|
34
|
-
throw: throwError = true
|
|
302
|
+
throw: throwError = true,
|
|
303
|
+
retry = defaultRetryOptions
|
|
35
304
|
} = options;
|
|
36
305
|
let query = [
|
|
37
306
|
options.force ? "force=true" : "",
|
|
@@ -42,7 +311,11 @@ async function getVersionsBatch(packages, options = {}) {
|
|
|
42
311
|
].filter(Boolean).join("&");
|
|
43
312
|
if (query)
|
|
44
313
|
query = `?${query}`;
|
|
45
|
-
const
|
|
314
|
+
const fetchFn = () => fetchApi(new URL(`/versions/${packages.join("+")}${query}`, apiEndpoint)).then((r) => r.json());
|
|
315
|
+
const data = await (retry === false ? fetchFn() : pRetry(
|
|
316
|
+
fetchFn,
|
|
317
|
+
typeof retry === "number" ? { ...defaultRetryOptions, retries: retry } : retry
|
|
318
|
+
));
|
|
46
319
|
const list = toArray(data);
|
|
47
320
|
return throwError ? throwErrorObject(list) : list;
|
|
48
321
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-npm-meta",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "Get npm package metadata",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist"
|
|
25
25
|
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"p-retry": "^7.1.0"
|
|
28
|
+
},
|
|
26
29
|
"scripts": {
|
|
27
30
|
"build": "unbuild",
|
|
28
31
|
"dev": "unbuild --stub"
|