@mcptoolshop/registry-stats 0.3.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.cjs ADDED
@@ -0,0 +1,669 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all2) => {
7
+ for (var name in all2)
8
+ __defProp(target, name, { get: all2[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ RegistryError: () => RegistryError,
24
+ calc: () => calc,
25
+ createCache: () => createCache,
26
+ createHandler: () => createHandler,
27
+ defaultConfig: () => defaultConfig,
28
+ loadConfig: () => loadConfig,
29
+ registerProvider: () => registerProvider,
30
+ serve: () => serve,
31
+ starterConfig: () => starterConfig,
32
+ stats: () => stats
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/types.ts
37
+ var RegistryError = class extends Error {
38
+ constructor(registry, statusCode, message, retryAfter) {
39
+ super(`[${registry}] ${message}`);
40
+ this.registry = registry;
41
+ this.statusCode = statusCode;
42
+ this.retryAfter = retryAfter;
43
+ this.name = "RegistryError";
44
+ }
45
+ };
46
+
47
+ // src/fetch.ts
48
+ var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
49
+ var MAX_RETRIES = 3;
50
+ var BASE_DELAY = 1e3;
51
+ async function fetchWithRetry(url, registry, init) {
52
+ let lastError;
53
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
54
+ const res = await fetch(url, init);
55
+ if (res.status === 404) return null;
56
+ if (res.ok) return res.json();
57
+ const retryAfter = res.headers.get("retry-after");
58
+ const retryAfterMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : void 0;
59
+ lastError = new RegistryError(
60
+ registry,
61
+ res.status,
62
+ `${res.statusText}: ${url}`,
63
+ retryAfter ? parseInt(retryAfter, 10) : void 0
64
+ );
65
+ if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
66
+ const delay = retryAfterMs ?? BASE_DELAY * Math.pow(2, attempt);
67
+ await new Promise((r) => setTimeout(r, delay));
68
+ }
69
+ throw lastError;
70
+ }
71
+
72
+ // src/providers/npm.ts
73
+ var API = "https://api.npmjs.org/downloads";
74
+ var npm = {
75
+ name: "npm",
76
+ async getStats(pkg) {
77
+ const [day, week, month] = await Promise.all([
78
+ fetchWithRetry(`${API}/point/last-day/${pkg}`, "npm"),
79
+ fetchWithRetry(`${API}/point/last-week/${pkg}`, "npm"),
80
+ fetchWithRetry(`${API}/point/last-month/${pkg}`, "npm")
81
+ ]);
82
+ if (!day && !week && !month) return null;
83
+ return {
84
+ registry: "npm",
85
+ package: pkg,
86
+ downloads: {
87
+ lastDay: day?.downloads,
88
+ lastWeek: week?.downloads,
89
+ lastMonth: month?.downloads
90
+ },
91
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
92
+ };
93
+ },
94
+ async getRange(pkg, start, end) {
95
+ const startDate = new Date(start);
96
+ const endDate = new Date(end);
97
+ const maxDays = 549;
98
+ const chunks = [];
99
+ let cursor = startDate;
100
+ while (cursor < endDate) {
101
+ const chunkEnd = new Date(cursor);
102
+ chunkEnd.setDate(chunkEnd.getDate() + maxDays - 1);
103
+ const actualEnd = chunkEnd > endDate ? endDate : chunkEnd;
104
+ const s = fmt(cursor);
105
+ const e = fmt(actualEnd);
106
+ const data = await fetchWithRetry(`${API}/range/${s}:${e}/${pkg}`, "npm");
107
+ if (data) {
108
+ for (const d of data.downloads) {
109
+ chunks.push({ date: d.day, downloads: d.downloads });
110
+ }
111
+ }
112
+ cursor = new Date(actualEnd);
113
+ cursor.setDate(cursor.getDate() + 1);
114
+ }
115
+ return chunks;
116
+ }
117
+ };
118
+ function fmt(d) {
119
+ return d.toISOString().slice(0, 10);
120
+ }
121
+
122
+ // src/providers/pypi.ts
123
+ var API2 = "https://pypistats.org/api";
124
+ var pypi = {
125
+ name: "pypi",
126
+ rateLimit: { maxRequests: 30, windowSeconds: 60, authRaisesLimit: false },
127
+ async getStats(pkg) {
128
+ const [recent, overall] = await Promise.all([
129
+ fetchWithRetry(`${API2}/packages/${pkg}/recent`, "pypi"),
130
+ fetchWithRetry(`${API2}/packages/${pkg}/overall?mirrors=false`, "pypi")
131
+ ]);
132
+ if (!recent && !overall) return null;
133
+ const total = overall?.data?.filter((d) => d.category === "without_mirrors")?.reduce((sum, d) => sum + d.downloads, 0);
134
+ return {
135
+ registry: "pypi",
136
+ package: pkg,
137
+ downloads: {
138
+ total: total || void 0,
139
+ lastDay: recent?.data.last_day,
140
+ lastWeek: recent?.data.last_week,
141
+ lastMonth: recent?.data.last_month
142
+ },
143
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
144
+ };
145
+ },
146
+ async getRange(pkg, start, end) {
147
+ const data = await fetchWithRetry(
148
+ `${API2}/packages/${pkg}/overall?mirrors=false`,
149
+ "pypi"
150
+ );
151
+ if (!data) return [];
152
+ const startDate = new Date(start);
153
+ const endDate = new Date(end);
154
+ return data.data.filter((d) => {
155
+ if (!d.date || d.category !== "without_mirrors") return false;
156
+ const date = new Date(d.date);
157
+ return date >= startDate && date <= endDate;
158
+ }).map((d) => ({ date: d.date, downloads: d.downloads })).sort((a, b) => a.date.localeCompare(b.date));
159
+ }
160
+ };
161
+
162
+ // src/providers/nuget.ts
163
+ var SEARCH_API = "https://azuresearch-usnc.nuget.org/query";
164
+ var nuget = {
165
+ name: "nuget",
166
+ async getStats(pkg) {
167
+ const url = `${SEARCH_API}?q=packageid:${encodeURIComponent(pkg)}&take=1`;
168
+ const json2 = await fetchWithRetry(url, "nuget");
169
+ if (!json2) return null;
170
+ const match = json2.data.find(
171
+ (d) => d.id.toLowerCase() === pkg.toLowerCase()
172
+ );
173
+ if (!match) return null;
174
+ return {
175
+ registry: "nuget",
176
+ package: match.id,
177
+ downloads: {
178
+ total: match.totalDownloads
179
+ },
180
+ extra: {
181
+ version: match.version
182
+ },
183
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
184
+ };
185
+ }
186
+ };
187
+
188
+ // src/providers/vscode.ts
189
+ var API3 = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery";
190
+ function getStat(stats2, name) {
191
+ return stats2.find((s) => s.statisticName === name)?.value;
192
+ }
193
+ var vscode = {
194
+ name: "vscode",
195
+ async getStats(pkg) {
196
+ let json2;
197
+ try {
198
+ json2 = await fetchWithRetry(API3, "vscode", {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ Accept: "application/json;api-version=3.0-preview.1"
203
+ },
204
+ body: JSON.stringify({
205
+ filters: [
206
+ {
207
+ criteria: [{ filterType: 7, value: pkg }]
208
+ }
209
+ ],
210
+ flags: 256
211
+ // IncludeStatistics
212
+ })
213
+ });
214
+ } catch (e) {
215
+ if (e.statusCode === 400) return null;
216
+ throw e;
217
+ }
218
+ if (!json2) return null;
219
+ const ext = json2.results?.[0]?.extensions?.[0];
220
+ if (!ext) return null;
221
+ const stats2 = ext.statistics || [];
222
+ return {
223
+ registry: "vscode",
224
+ package: `${ext.publisher.publisherName}.${ext.extensionName}`,
225
+ downloads: {
226
+ total: getStat(stats2, "install")
227
+ },
228
+ extra: {
229
+ displayName: ext.displayName,
230
+ rating: getStat(stats2, "averagerating"),
231
+ ratingCount: getStat(stats2, "ratingcount"),
232
+ trendingDaily: getStat(stats2, "trendingdaily"),
233
+ trendingWeekly: getStat(stats2, "trendingweekly"),
234
+ trendingMonthly: getStat(stats2, "trendingmonthly")
235
+ },
236
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
237
+ };
238
+ }
239
+ };
240
+
241
+ // src/providers/docker.ts
242
+ var API4 = "https://hub.docker.com/v2/repositories";
243
+ var docker = {
244
+ name: "docker",
245
+ rateLimit: { maxRequests: 10, windowSeconds: 3600, authRaisesLimit: true },
246
+ async getStats(pkg, options) {
247
+ const headers = {};
248
+ if (options?.dockerToken) {
249
+ headers["Authorization"] = `Bearer ${options.dockerToken}`;
250
+ }
251
+ const json2 = await fetchWithRetry(`${API4}/${pkg}`, "docker", { headers });
252
+ if (!json2 || !json2.name || !json2.namespace) return null;
253
+ return {
254
+ registry: "docker",
255
+ package: `${json2.namespace}/${json2.name}`,
256
+ downloads: {
257
+ total: json2.pull_count
258
+ },
259
+ extra: {
260
+ stars: json2.star_count,
261
+ lastUpdated: json2.last_updated
262
+ },
263
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
264
+ };
265
+ }
266
+ };
267
+
268
+ // src/calc.ts
269
+ var calc = {
270
+ total(records) {
271
+ return records.reduce((sum, r) => sum + r.downloads, 0);
272
+ },
273
+ avg(records) {
274
+ if (records.length === 0) return 0;
275
+ return calc.total(records) / records.length;
276
+ },
277
+ group(records, fn) {
278
+ const groups = {};
279
+ for (const r of records) {
280
+ const key = fn(r);
281
+ (groups[key] ??= []).push(r);
282
+ }
283
+ return groups;
284
+ },
285
+ monthly(records) {
286
+ return calc.group(records, (r) => r.date.slice(0, 7));
287
+ },
288
+ yearly(records) {
289
+ return calc.group(records, (r) => r.date.slice(0, 4));
290
+ },
291
+ groupTotals(grouped) {
292
+ const result = {};
293
+ for (const [key, records] of Object.entries(grouped)) {
294
+ result[key] = calc.total(records);
295
+ }
296
+ return result;
297
+ },
298
+ groupAvgs(grouped) {
299
+ const result = {};
300
+ for (const [key, records] of Object.entries(grouped)) {
301
+ result[key] = calc.avg(records);
302
+ }
303
+ return result;
304
+ },
305
+ trend(records, windowDays = 7) {
306
+ if (records.length < windowDays * 2) {
307
+ return { slope: 0, direction: "flat", changePercent: 0 };
308
+ }
309
+ const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date));
310
+ const recent = sorted.slice(-windowDays);
311
+ const previous = sorted.slice(-windowDays * 2, -windowDays);
312
+ const recentAvg = calc.avg(recent);
313
+ const previousAvg = calc.avg(previous);
314
+ const slope = recentAvg - previousAvg;
315
+ const changePercent = previousAvg === 0 ? 0 : (recentAvg - previousAvg) / previousAvg * 100;
316
+ const threshold = previousAvg * 0.05;
317
+ const direction = slope > threshold ? "up" : slope < -threshold ? "down" : "flat";
318
+ return { slope: Math.round(slope * 100) / 100, direction, changePercent: Math.round(changePercent * 100) / 100 };
319
+ },
320
+ movingAvg(records, windowDays = 7) {
321
+ if (records.length < windowDays) return [];
322
+ const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date));
323
+ const result = [];
324
+ for (let i = windowDays - 1; i < sorted.length; i++) {
325
+ let sum = 0;
326
+ for (let j = i - windowDays + 1; j <= i; j++) {
327
+ sum += sorted[j].downloads;
328
+ }
329
+ result.push({
330
+ date: sorted[i].date,
331
+ downloads: Math.round(sum / windowDays * 100) / 100
332
+ });
333
+ }
334
+ return result;
335
+ },
336
+ popularity(records) {
337
+ if (records.length === 0) return 0;
338
+ const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date));
339
+ const recentDays = Math.min(30, sorted.length);
340
+ const recent = sorted.slice(-recentDays);
341
+ const avgDaily = calc.avg(recent);
342
+ const score = Math.max(0, Math.min(100, Math.log10(Math.max(1, avgDaily)) / 6 * 100));
343
+ return Math.round(score * 10) / 10;
344
+ },
345
+ toCSV(records) {
346
+ const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date));
347
+ const lines = ["date,downloads"];
348
+ for (const r of sorted) {
349
+ lines.push(`${r.date},${r.downloads}`);
350
+ }
351
+ return lines.join("\n");
352
+ },
353
+ toChartData(records, label = "downloads") {
354
+ const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date));
355
+ return {
356
+ labels: sorted.map((r) => r.date),
357
+ datasets: [{ label, data: sorted.map((r) => r.downloads) }]
358
+ };
359
+ }
360
+ };
361
+
362
+ // src/config.ts
363
+ var import_node_fs = require("fs");
364
+ var import_node_path = require("path");
365
+ var CONFIG_NAME = "registry-stats.config.json";
366
+ function loadConfig(startDir) {
367
+ let dir = startDir ?? process.cwd();
368
+ while (true) {
369
+ const configPath = (0, import_node_path.resolve)(dir, CONFIG_NAME);
370
+ if ((0, import_node_fs.existsSync)(configPath)) {
371
+ const raw = (0, import_node_fs.readFileSync)(configPath, "utf-8");
372
+ return JSON.parse(raw);
373
+ }
374
+ const parent = (0, import_node_path.dirname)(dir);
375
+ if (parent === dir) break;
376
+ dir = parent;
377
+ }
378
+ return null;
379
+ }
380
+ function defaultConfig() {
381
+ return {
382
+ registries: ["npm", "pypi", "nuget", "vscode", "docker"],
383
+ packages: {},
384
+ cache: true,
385
+ cacheTtlMs: 3e5,
386
+ concurrency: 5
387
+ };
388
+ }
389
+ var STARTER = `{
390
+ "registries": ["npm", "pypi", "nuget", "vscode", "docker"],
391
+ "packages": {
392
+ "my-package": {
393
+ "npm": "my-package",
394
+ "pypi": "my-package"
395
+ }
396
+ },
397
+ "cache": true,
398
+ "cacheTtlMs": 300000,
399
+ "concurrency": 5
400
+ }
401
+ `;
402
+ function starterConfig() {
403
+ return STARTER;
404
+ }
405
+
406
+ // src/server.ts
407
+ var import_node_http = require("http");
408
+ function json(res, data, status = 200) {
409
+ res.writeHead(status, { "Content-Type": "application/json" });
410
+ res.end(JSON.stringify(data));
411
+ }
412
+ function error(res, message, status = 400) {
413
+ json(res, { error: message }, status);
414
+ }
415
+ function parseUrl(url) {
416
+ const [pathname, search] = url.split("?");
417
+ const path = pathname.replace(/^\/api\//, "/").split("/").filter(Boolean);
418
+ const query = {};
419
+ if (search) {
420
+ for (const pair of search.split("&")) {
421
+ const [k, v] = pair.split("=");
422
+ if (k) query[decodeURIComponent(k)] = decodeURIComponent(v ?? "");
423
+ }
424
+ }
425
+ return { path, query };
426
+ }
427
+ function createHandler(opts) {
428
+ const options = { ...opts };
429
+ if (!options.cache) {
430
+ options.cache = createCache();
431
+ }
432
+ return async (req, res) => {
433
+ res.setHeader("Access-Control-Allow-Origin", "*");
434
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
435
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
436
+ if (req.method === "OPTIONS") {
437
+ res.writeHead(204);
438
+ res.end();
439
+ return;
440
+ }
441
+ if (req.method !== "GET") {
442
+ error(res, "Method not allowed", 405);
443
+ return;
444
+ }
445
+ const { path, query } = parseUrl(req.url ?? "/");
446
+ try {
447
+ if (path[0] === "stats") {
448
+ if (path.length === 2) {
449
+ const pkg = decodeURIComponent(path[1]);
450
+ const results = await stats.all(pkg, options);
451
+ json(res, results);
452
+ return;
453
+ }
454
+ if (path.length >= 3) {
455
+ const registry = path[1];
456
+ const pkg = path.slice(2).join("/");
457
+ const result = await stats(registry, pkg, options);
458
+ if (!result) {
459
+ error(res, `Package "${pkg}" not found on ${registry}`, 404);
460
+ return;
461
+ }
462
+ json(res, result);
463
+ return;
464
+ }
465
+ }
466
+ if (path[0] === "compare" && path.length >= 2) {
467
+ const pkg = decodeURIComponent(path[1]);
468
+ const registries = query.registries ? query.registries.split(",") : void 0;
469
+ const result = await stats.compare(pkg, registries, options);
470
+ json(res, result);
471
+ return;
472
+ }
473
+ if (path[0] === "range" && path.length >= 3) {
474
+ const registry = path[1];
475
+ const pkg = path.slice(2).join("/");
476
+ const { start, end, format } = query;
477
+ if (!start || !end) {
478
+ error(res, "Missing start and end query parameters");
479
+ return;
480
+ }
481
+ const data = await stats.range(registry, pkg, start, end, options);
482
+ if (format === "csv") {
483
+ res.writeHead(200, {
484
+ "Content-Type": "text/csv",
485
+ "Content-Disposition": `attachment; filename="${pkg}-${start}-${end}.csv"`
486
+ });
487
+ res.end(calc.toCSV(data));
488
+ return;
489
+ }
490
+ if (format === "chart") {
491
+ json(res, calc.toChartData(data, `${pkg} (${registry})`));
492
+ return;
493
+ }
494
+ json(res, data);
495
+ return;
496
+ }
497
+ if (path.length === 0) {
498
+ json(res, {
499
+ name: "@mcptoolshop/registry-stats",
500
+ endpoints: [
501
+ "GET /stats/:package",
502
+ "GET /stats/:registry/:package",
503
+ "GET /compare/:package?registries=npm,pypi",
504
+ "GET /range/:registry/:package?start=YYYY-MM-DD&end=YYYY-MM-DD&format=json|csv|chart"
505
+ ]
506
+ });
507
+ return;
508
+ }
509
+ error(res, "Not found", 404);
510
+ } catch (e) {
511
+ error(res, e.message, 500);
512
+ }
513
+ };
514
+ }
515
+ function serve(opts) {
516
+ const port = opts?.port ?? 3e3;
517
+ const handler = createHandler({
518
+ cache: opts?.cache !== false ? createCache() : void 0
519
+ });
520
+ const server = (0, import_node_http.createServer)(handler);
521
+ server.listen(port, () => {
522
+ console.log(`registry-stats server listening on http://localhost:${port}`);
523
+ console.log(`
524
+ Endpoints:`);
525
+ console.log(` GET /stats/:package`);
526
+ console.log(` GET /stats/:registry/:package`);
527
+ console.log(` GET /compare/:package?registries=npm,pypi`);
528
+ console.log(` GET /range/:registry/:package?start=...&end=...&format=json|csv|chart`);
529
+ });
530
+ return server;
531
+ }
532
+
533
+ // src/index.ts
534
+ function createCache() {
535
+ const store = /* @__PURE__ */ new Map();
536
+ return {
537
+ get(key) {
538
+ const entry = store.get(key);
539
+ if (!entry) return void 0;
540
+ if (Date.now() > entry.expiresAt) {
541
+ store.delete(key);
542
+ return void 0;
543
+ }
544
+ return entry.value;
545
+ },
546
+ set(key, value, ttlMs) {
547
+ store.set(key, { value, expiresAt: Date.now() + ttlMs });
548
+ }
549
+ };
550
+ }
551
+ function pLimit(concurrency) {
552
+ let active = 0;
553
+ const queue = [];
554
+ function next() {
555
+ if (queue.length > 0 && active < concurrency) {
556
+ active++;
557
+ queue.shift()();
558
+ }
559
+ }
560
+ return (fn) => new Promise((resolve2, reject) => {
561
+ queue.push(() => {
562
+ fn().then(resolve2, reject).finally(() => {
563
+ active--;
564
+ next();
565
+ });
566
+ });
567
+ next();
568
+ });
569
+ }
570
+ var providers = {
571
+ npm,
572
+ pypi,
573
+ nuget,
574
+ vscode,
575
+ docker
576
+ };
577
+ function registerProvider(provider) {
578
+ providers[provider.name] = provider;
579
+ }
580
+ var DEFAULT_TTL = 3e5;
581
+ async function stats(registry, pkg, options) {
582
+ const provider = providers[registry];
583
+ if (!provider) {
584
+ throw new RegistryError(registry, 0, `Unknown registry "${registry}". Use registerProvider() to add custom registries.`);
585
+ }
586
+ const cache = options?.cache;
587
+ if (cache) {
588
+ const key = `stats:${registry}:${pkg}`;
589
+ const cached = cache.get(key);
590
+ if (cached) return cached;
591
+ const result = await provider.getStats(pkg, options);
592
+ if (result) cache.set(key, result, options?.cacheTtlMs ?? DEFAULT_TTL);
593
+ return result;
594
+ }
595
+ return provider.getStats(pkg, options);
596
+ }
597
+ stats.all = async function all(pkg, options) {
598
+ const results = await Promise.allSettled(
599
+ Object.values(providers).map((p) => p.getStats(pkg, options))
600
+ );
601
+ return results.filter(
602
+ (r) => r.status === "fulfilled" && r.value !== null
603
+ ).map((r) => r.value);
604
+ };
605
+ stats.bulk = async function bulk(registry, packages, options) {
606
+ const provider = providers[registry];
607
+ if (!provider) {
608
+ throw new RegistryError(registry, 0, `Unknown registry "${registry}".`);
609
+ }
610
+ const concurrency = options?.concurrency ?? 5;
611
+ const limit = pLimit(concurrency);
612
+ return Promise.all(packages.map((pkg) => limit(() => stats(registry, pkg, options))));
613
+ };
614
+ stats.range = async function range(registry, pkg, start, end, options) {
615
+ const provider = providers[registry];
616
+ if (!provider) {
617
+ throw new RegistryError(registry, 0, `Unknown registry "${registry}".`);
618
+ }
619
+ if (!provider.getRange) {
620
+ throw new RegistryError(
621
+ registry,
622
+ 0,
623
+ `${registry} does not support time-series data. Only npm and pypi support getRange().`
624
+ );
625
+ }
626
+ const cache = options?.cache;
627
+ if (cache) {
628
+ const key = `range:${registry}:${pkg}:${start}:${end}`;
629
+ const cached = cache.get(key);
630
+ if (cached) return cached;
631
+ const result = await provider.getRange(pkg, start, end);
632
+ cache.set(key, result, options?.cacheTtlMs ?? DEFAULT_TTL);
633
+ return result;
634
+ }
635
+ return provider.getRange(pkg, start, end);
636
+ };
637
+ stats.compare = async function compare(pkg, registries, options) {
638
+ const regs = registries ?? Object.keys(providers);
639
+ const results = await Promise.allSettled(
640
+ regs.map(async (reg) => {
641
+ const result = await stats(reg, pkg, options);
642
+ return result ? { reg, result } : null;
643
+ })
644
+ );
645
+ const registryMap = {};
646
+ for (const r of results) {
647
+ if (r.status === "fulfilled" && r.value) {
648
+ registryMap[r.value.reg] = r.value.result;
649
+ }
650
+ }
651
+ return {
652
+ package: pkg,
653
+ registries: registryMap,
654
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
655
+ };
656
+ };
657
+ // Annotate the CommonJS export names for ESM import in node:
658
+ 0 && (module.exports = {
659
+ RegistryError,
660
+ calc,
661
+ createCache,
662
+ createHandler,
663
+ defaultConfig,
664
+ loadConfig,
665
+ registerProvider,
666
+ serve,
667
+ starterConfig,
668
+ stats
669
+ });