chore: update dependencies to include @octokit/plugin-retry and @octokit/plugin-throttling; refactor GitHubTracker to utilize these plugins for improved rate limiting and error handling

This commit is contained in:
Luke Hagar
2025-07-30 11:17:44 -05:00
parent b46dc85001
commit 5aa7ed039e
10 changed files with 285 additions and 406 deletions

View File

@@ -5,6 +5,8 @@
"name": "usage-statistics",
"dependencies": {
"@actions/core": "1.11.1",
"@octokit/plugin-retry": "^7.0.0",
"@octokit/plugin-throttling": "^7.0.0",
"@octokit/rest": "22.0.0",
},
"devDependencies": {
@@ -41,9 +43,13 @@
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
"@octokit/plugin-retry": ["@octokit/plugin-retry@7.2.1", "", { "dependencies": { "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A=="],
"@octokit/plugin-throttling": ["@octokit/plugin-throttling@7.0.0", "", { "dependencies": { "@octokit/types": "^11.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^5.0.0" } }, "sha512-KL2k/d0uANc8XqP5S64YcNFCudR3F5AaKO39XWdUtlJIjT9Ni79ekWJ6Kj5xvAw87udkOMEPcVf9xEge2+ahew=="],
"@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
"@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
@@ -55,6 +61,8 @@
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@@ -70,5 +78,13 @@
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/plugin-throttling/@octokit/types": ["@octokit/types@11.1.0", "", { "dependencies": { "@octokit/openapi-types": "^18.0.0" } }, "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ=="],
"@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/plugin-throttling/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@18.1.1", "", {}, "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="],
}
}

98
dist/action.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -24,7 +24,9 @@
},
"dependencies": {
"@actions/core": "1.11.1",
"@octokit/rest": "22.0.0"
"@octokit/rest": "22.0.0",
"@octokit/plugin-retry": "^7.0.0",
"@octokit/plugin-throttling": "^7.0.0"
},
"engines": {
"bun": ">=1.0.0"

View File

@@ -29,7 +29,14 @@ async function run() {
// Set environment variables
if (githubToken) {
process.env.GITHUB_TOKEN = githubToken;
core.info('✅ Using custom GitHub token for authentication');
} else if (process.env.GITHUB_TOKEN) {
// Use the default GitHub token if no custom token provided
core.info('✅ Using default GitHub token from environment');
} else {
core.warning('⚠️ No GitHub token provided. Some API calls may be rate limited.');
}
if (postmanApiKey) {
process.env.POSTMAN_API_KEY = postmanApiKey;
}

View File

@@ -3,7 +3,7 @@
* Combines and analyzes statistics from all platform trackers
*/
import type { BaseDownloadStats, DownloadStatsAggregator, TrackingConfig } from './types';
import type { BaseDownloadStats, TrackingConfig } from './types';
import NpmTracker from './trackers/npm';
import GitHubTracker from './trackers/github';
import PyPiTracker from './trackers/pypi';
@@ -11,7 +11,6 @@ import HomebrewTracker from './trackers/homebrew';
import PowerShellTracker from './trackers/powershell';
import PostmanTracker from './trackers/postman';
import GoTracker from './trackers/go';
import { globalRateLimiter } from './utils/rate-limiter';
export interface AggregatedStats {
totalDownloads: number;
@@ -29,7 +28,7 @@ export interface AggregatedStats {
}>;
}
export class DownloadStatsAggregator implements DownloadStatsAggregator {
export class DownloadStatsAggregator {
private trackers: Map<string, any> = new Map();
private config: TrackingConfig;
@@ -113,136 +112,122 @@ export class DownloadStatsAggregator implements DownloadStatsAggregator {
async collectAllStats(): Promise<BaseDownloadStats[]> {
const allStats: BaseDownloadStats[] = [];
// Collect NPM stats with rate limiting
// Collect NPM stats
if (this.config.npmPackages) {
const npmOperations = this.config.npmPackages.map(packageName =>
async () => {
try {
const npmTracker = this.trackers.get('npm');
const stats = await npmTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting NPM stats for ${packageName}:`, error);
return [];
}
const npmPromises = this.config.npmPackages.map(async packageName => {
try {
const npmTracker = this.trackers.get('npm');
const stats = await npmTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting NPM stats for ${packageName}:`, error);
return [];
}
);
});
const npmResults = await globalRateLimiter.throttleRequests(npmOperations, 2, 2000);
const npmResults = await Promise.all(npmPromises);
npmResults.forEach(stats => allStats.push(...stats));
}
// Collect GitHub stats with rate limiting
// Collect GitHub stats (Octokit plugins handle rate limiting)
if (this.config.githubRepos) {
const githubOperations = this.config.githubRepos.map(repo =>
async () => {
try {
const githubTracker = this.trackers.get('github');
const stats = await githubTracker.getDownloadStats(repo);
return stats;
} catch (error) {
console.error(`Error collecting GitHub stats for ${repo}:`, error);
return [];
}
const githubPromises = this.config.githubRepos.map(async repo => {
try {
const githubTracker = this.trackers.get('github');
const stats = await githubTracker.getDownloadStats(repo);
return stats;
} catch (error) {
console.error(`Error collecting GitHub stats for ${repo}:`, error);
return [];
}
);
});
const githubResults = await globalRateLimiter.throttleRequests(githubOperations, 1, 3000);
const githubResults = await Promise.all(githubPromises);
githubResults.forEach(stats => allStats.push(...stats));
}
// Collect PyPI stats with rate limiting
// Collect PyPI stats
if (this.config.pythonPackages) {
const pypiOperations = this.config.pythonPackages.map(packageName =>
async () => {
try {
const pypiTracker = this.trackers.get('pypi');
const stats = await pypiTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting PyPI stats for ${packageName}:`, error);
return [];
}
const pypiPromises = this.config.pythonPackages.map(async packageName => {
try {
const pypiTracker = this.trackers.get('pypi');
const stats = await pypiTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting PyPI stats for ${packageName}:`, error);
return [];
}
);
});
const pypiResults = await globalRateLimiter.throttleRequests(pypiOperations, 2, 1500);
const pypiResults = await Promise.all(pypiPromises);
pypiResults.forEach(stats => allStats.push(...stats));
}
// Collect Homebrew stats with rate limiting
// Collect Homebrew stats
if (this.config.homebrewPackages) {
const homebrewOperations = this.config.homebrewPackages.map(packageName =>
async () => {
try {
const homebrewTracker = this.trackers.get('homebrew');
const stats = await homebrewTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting Homebrew stats for ${packageName}:`, error);
return [];
}
const homebrewPromises = this.config.homebrewPackages.map(async packageName => {
try {
const homebrewTracker = this.trackers.get('homebrew');
const stats = await homebrewTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting Homebrew stats for ${packageName}:`, error);
return [];
}
);
});
const homebrewResults = await globalRateLimiter.throttleRequests(homebrewOperations, 2, 2000);
const homebrewResults = await Promise.all(homebrewPromises);
homebrewResults.forEach(stats => allStats.push(...stats));
}
// Collect PowerShell stats with rate limiting
// Collect PowerShell stats
if (this.config.powershellModules) {
const powershellOperations = this.config.powershellModules.map(moduleName =>
async () => {
try {
const powershellTracker = this.trackers.get('powershell');
const stats = await powershellTracker.getDownloadStats(moduleName);
return stats;
} catch (error) {
console.error(`Error collecting PowerShell stats for ${moduleName}:`, error);
return [];
}
const powershellPromises = this.config.powershellModules.map(async moduleName => {
try {
const powershellTracker = this.trackers.get('powershell');
const stats = await powershellTracker.getDownloadStats(moduleName);
return stats;
} catch (error) {
console.error(`Error collecting PowerShell stats for ${moduleName}:`, error);
return [];
}
);
});
const powershellResults = await globalRateLimiter.throttleRequests(powershellOperations, 2, 2000);
const powershellResults = await Promise.all(powershellPromises);
powershellResults.forEach(stats => allStats.push(...stats));
}
// Collect Postman stats with rate limiting
// Collect Postman stats
if (this.config.postmanCollections) {
const postmanOperations = this.config.postmanCollections.map(collectionId =>
async () => {
try {
const postmanTracker = this.trackers.get('postman');
const stats = await postmanTracker.getDownloadStats(collectionId);
return stats;
} catch (error) {
console.error(`Error collecting Postman stats for ${collectionId}:`, error);
return [];
}
const postmanPromises = this.config.postmanCollections.map(async collectionId => {
try {
const postmanTracker = this.trackers.get('postman');
const stats = await postmanTracker.getDownloadStats(collectionId);
return stats;
} catch (error) {
console.error(`Error collecting Postman stats for ${collectionId}:`, error);
return [];
}
);
});
const postmanResults = await globalRateLimiter.throttleRequests(postmanOperations, 2, 2000);
const postmanResults = await Promise.all(postmanPromises);
postmanResults.forEach(stats => allStats.push(...stats));
}
// Collect Go stats with rate limiting
// Collect Go stats
if (this.config.goModules) {
const goOperations = this.config.goModules.map(moduleName =>
async () => {
try {
const goTracker = this.trackers.get('go');
const stats = await goTracker.getDownloadStats(moduleName);
return stats;
} catch (error) {
console.error(`Error collecting Go stats for ${moduleName}:`, error);
return [];
}
const goPromises = this.config.goModules.map(async moduleName => {
try {
const goTracker = this.trackers.get('go');
const stats = await goTracker.getDownloadStats(moduleName);
return stats;
} catch (error) {
console.error(`Error collecting Go stats for ${moduleName}:`, error);
return [];
}
);
});
const goResults = await globalRateLimiter.throttleRequests(goOperations, 2, 2000);
const goResults = await Promise.all(goPromises);
goResults.forEach(stats => allStats.push(...stats));
}
@@ -263,19 +248,17 @@ export class DownloadStatsAggregator implements DownloadStatsAggregator {
const allStats: BaseDownloadStats[] = [];
const packages = this.getPackagesForPlatform(platform);
const operations = packages.map(packageName =>
async () => {
try {
const stats = await tracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting ${platform} stats for ${packageName}:`, error);
return [];
}
const promises = packages.map(async packageName => {
try {
const stats = await tracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting ${platform} stats for ${packageName}:`, error);
return [];
}
);
});
const results = await globalRateLimiter.throttleRequests(operations, 2, 2000);
const results = await Promise.all(promises);
results.forEach(stats => allStats.push(...stats));
return allStats;

View File

@@ -0,0 +1,52 @@
import { describe, it, expect, beforeEach } from "bun:test";
import GitHubTracker from "./trackers/github";
describe("GitHubTracker", () => {
let tracker: GitHubTracker;
beforeEach(() => {
// Use a test token or no token for testing
tracker = new GitHubTracker(process.env.GITHUB_TOKEN || undefined);
});
describe("Rate Limiting", () => {
it("should handle rate limiting gracefully", async () => {
// Test with a popular repository that might hit rate limits
const stats = await tracker.getDownloadStats('microsoft/vscode');
// Should return an array (even if empty due to rate limiting)
expect(Array.isArray(stats)).toBe(true);
}, 15000); // 15 second timeout for rate limit handling
it("should get package info", async () => {
try {
const info = await tracker.getPackageInfo('microsoft/vscode');
expect(info).toBeDefined();
expect(info.name).toBe('vscode');
expect(info.full_name).toBe('microsoft/vscode');
} catch (error) {
// If rate limited, that's expected behavior
console.log('Rate limited during test (expected):', error);
expect(error).toBeDefined();
}
}, 15000);
it("should get latest version", async () => {
try {
const version = await tracker.getLatestVersion('microsoft/vscode');
// Should return a version string or null
expect(version === null || typeof version === 'string').toBe(true);
} catch (error) {
// If rate limited, that's expected behavior
console.log('Rate limited during test (expected):', error);
expect(error).toBeDefined();
}
}, 15000);
});
describe("Configuration", () => {
it("should have correct name", () => {
expect(tracker.name).toBe('github');
});
});
});

View File

@@ -7,15 +7,15 @@ describe("UsageStatisticsManager", () => {
beforeEach(() => {
const config: TrackingConfig = {
enableLogging: true,
enableLogging: false, // Disable logging for tests
updateInterval: 60 * 60 * 1000,
npmPackages: ['lodash', 'axios'],
githubRepos: ['microsoft/vscode', 'facebook/react'],
pythonPackages: ['requests', 'numpy'],
homebrewPackages: ['git', 'node'],
npmPackages: ['lodash'], // Reduce to single package for faster tests
githubRepos: ['microsoft/vscode'], // Reduce to single repo
pythonPackages: ['requests'], // Reduce to single package
homebrewPackages: ['git'], // Reduce to single package
powershellModules: ['PowerShellGet'],
postmanCollections: [],
goModules: ['github.com/gin-gonic/gin', 'github.com/go-chi/chi']
goModules: ['github.com/go-chi/chi'] // Reduce to single module
};
manager = new UsageStatisticsManager(config);
});
@@ -31,7 +31,7 @@ describe("UsageStatisticsManager", () => {
expect(Array.isArray(report.topPackages)).toBe(true);
expect(report.topPackages.length).toBeGreaterThan(0);
expect(typeof report.platformBreakdown).toBe('object');
}, 30000); // 30 second timeout
}, 10000); // 10 second timeout
});
describe("getPlatformReport", () => {
@@ -54,7 +54,7 @@ describe("UsageStatisticsManager", () => {
// Should be valid JSON
const parsed = JSON.parse(jsonReport);
expect(parsed).toBeDefined();
}, 30000); // 30 second timeout
}, 10000); // 10 second timeout
it("should export CSV report", async () => {
const csvReport = await manager.exportReport('csv');
@@ -62,7 +62,7 @@ describe("UsageStatisticsManager", () => {
expect(csvReport).toBeDefined();
expect(typeof csvReport).toBe('string');
expect(csvReport.includes('Platform,Package,Downloads')).toBe(true);
}, 30000); // 30 second timeout
}, 10000); // 10 second timeout
});
describe("getLastUpdateTime", () => {

View File

@@ -5,6 +5,8 @@
import type { BaseDownloadStats, PlatformTracker } from '../types';
import { Octokit } from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
import { throttling } from '@octokit/plugin-throttling';
export interface GitHubDownloadStats extends BaseDownloadStats {
platform: 'github';
@@ -57,30 +59,35 @@ export class GitHubTracker implements PlatformTracker {
}
private async initializeOctokit() {
if (typeof window === 'undefined') {
// Server-side: use Octokit
this.octokit = new Octokit({
auth: this.token,
userAgent: 'usage-statistics-tracker',
timeZone: 'UTC',
baseUrl: 'https://api.github.com',
log: {
debug: () => {},
info: () => {},
warn: console.warn,
error: console.error
// Create Octokit with retry and throttling plugins
const MyOctokit = Octokit.plugin(retry, throttling);
this.octokit = new MyOctokit({
auth: this.token,
userAgent: 'usage-statistics-tracker',
timeZone: 'UTC',
baseUrl: 'https://api.github.com',
log: {
debug: () => {},
info: () => {},
warn: console.warn,
error: console.error
},
throttle: {
onRateLimit: (retryAfter: number, options: any) => {
console.warn(`Rate limit hit for ${options.request.url}, retrying after ${retryAfter} seconds`);
return true; // Retry after the specified time
},
request: {
timeout: 10000,
retries: 3,
retryAfterBaseValue: 1,
retryAfterMaxValue: 60
onSecondaryRateLimit: (retryAfter: number, options: any) => {
console.warn(`Secondary rate limit hit for ${options.request.url}, retrying after ${retryAfter} seconds`);
return true; // Retry after the specified time
}
});
} else {
// Client-side: use fetch API
this.octokit = null;
}
},
retry: {
doNotRetry: [400, 401, 403, 404, 422], // Don't retry on these status codes
enabled: true
}
});
}
async getDownloadStats(repository: string, options?: {
@@ -154,91 +161,41 @@ export class GitHubTracker implements PlatformTracker {
async getPackageInfo(repository: string): Promise<GitHubRepositoryInfo> {
const [owner, repo] = repository.split('/');
if (this.octokit) {
// Use Octokit if available
try {
const response = await this.octokit.repos.get({
owner,
repo
});
return response.data;
} catch (error: any) {
if (error.status === 403 && error.message.includes('abuse detection')) {
console.warn(`Rate limit hit for ${repository}, waiting 60 seconds...`);
await this.sleep(60000);
return this.getPackageInfo(repository); // Retry once
}
throw error;
}
} else {
// Fallback to fetch API
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
headers: this.token ? {
'Authorization': `token ${this.token}`,
'Accept': 'application/vnd.github.v3+json'
} : {
'Accept': 'application/vnd.github.v3+json'
}
if (!this.octokit) {
throw new Error('Octokit not initialized');
}
try {
const response = await this.octokit.repos.get({
owner,
repo
});
if (!response.ok) {
if (response.status === 403) {
console.warn(`Rate limit hit for ${repository}, waiting 60 seconds...`);
await this.sleep(60000);
return this.getPackageInfo(repository); // Retry once
}
throw new Error(`Failed to fetch repository info for ${repository}`);
}
return response.json();
return response.data;
} catch (error: any) {
console.error(`Error fetching repository info for ${repository}:`, error);
throw error;
}
}
private async getReleases(owner: string, repo: string): Promise<GitHubReleaseInfo[]> {
if (this.octokit) {
// Use Octokit if available
try {
const response = await this.octokit.repos.listReleases({
owner,
repo,
per_page: 100
});
return response.data;
} catch (error: any) {
if (error.status === 403 && error.message.includes('abuse detection')) {
console.warn(`Rate limit hit for ${owner}/${repo}, waiting 60 seconds...`);
await this.sleep(60000);
return this.getReleases(owner, repo); // Retry once
}
throw error;
}
} else {
// Fallback to fetch API
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`, {
headers: this.token ? {
'Authorization': `token ${this.token}`,
'Accept': 'application/vnd.github.v3+json'
} : {
'Accept': 'application/vnd.github.v3+json'
}
if (!this.octokit) {
throw new Error('Octokit not initialized');
}
try {
const response = await this.octokit.repos.listReleases({
owner,
repo,
per_page: 100
});
if (!response.ok) {
if (response.status === 403) {
console.warn(`Rate limit hit for ${owner}/${repo}, waiting 60 seconds...`);
await this.sleep(60000);
return this.getReleases(owner, repo); // Retry once
}
throw new Error(`Failed to fetch releases for ${owner}/${repo}`);
}
return response.json();
return response.data;
} catch (error: any) {
console.error(`Error fetching releases for ${owner}/${repo}:`, error);
throw error;
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export default GitHubTracker;

View File

@@ -17,14 +17,6 @@ export interface PlatformTracker {
getPackageInfo(packageName: string): Promise<any>;
}
export interface DownloadStatsAggregator {
aggregateStats(stats: BaseDownloadStats[]): {
totalDownloads: number;
uniquePackages: number;
platforms: string[];
};
}
export interface TrackingConfig {
npmPackages?: string[];
goModules?: string[];

View File

@@ -1,140 +0,0 @@
/**
* Rate Limiting Utility
* Handles API rate limits with exponential backoff and proper error handling
*/
export interface RateLimitConfig {
maxRetries: number;
baseDelay: number;
maxDelay: number;
backoffMultiplier: number;
}
export class RateLimiter {
private config: RateLimitConfig;
private requestCounts: Map<string, { count: number; resetTime: number }> = new Map();
constructor(config: Partial<RateLimitConfig> = {}) {
this.config = {
maxRetries: config.maxRetries || 3,
baseDelay: config.baseDelay || 1000,
maxDelay: config.maxDelay || 60000,
backoffMultiplier: config.backoffMultiplier || 2
};
}
async executeWithRetry<T>(
operation: () => Promise<T>,
operationName: string,
retryCount = 0
): Promise<T> {
try {
return await operation();
} catch (error: any) {
if (this.shouldRetry(error) && retryCount < this.config.maxRetries) {
const delay = this.calculateDelay(retryCount);
console.warn(`Rate limit hit for ${operationName}, retrying in ${delay}ms (attempt ${retryCount + 1}/${this.config.maxRetries})`);
await this.sleep(delay);
return this.executeWithRetry(operation, operationName, retryCount + 1);
}
throw error;
}
}
private shouldRetry(error: any): boolean {
// Check for rate limiting errors
if (error.status === 403 || error.status === 429) {
return true;
}
// Check for abuse detection
if (error.message && error.message.includes('abuse detection')) {
return true;
}
// Check for rate limit headers
if (error.headers && (error.headers['x-ratelimit-remaining'] === '0' || error.headers['retry-after'])) {
return true;
}
return false;
}
private calculateDelay(retryCount: number): number {
const delay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, retryCount);
return Math.min(delay, this.config.maxDelay);
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async throttleRequests<T>(
operations: Array<() => Promise<T>>,
maxConcurrent = 3,
delayBetweenRequests = 1000
): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < operations.length; i += maxConcurrent) {
const batch = operations.slice(i, i + maxConcurrent);
const batchPromises = batch.map(async (operation, index) => {
// Add delay between requests in the same batch
if (index > 0) {
await this.sleep(delayBetweenRequests);
}
return this.executeWithRetry(operation, `batch-${i}-${index}`);
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Add delay between batches
if (i + maxConcurrent < operations.length) {
await this.sleep(delayBetweenRequests * 2);
}
}
return results;
}
// Track rate limits for different APIs
trackRequest(apiName: string, resetTime?: number): void {
const now = Date.now();
const current = this.requestCounts.get(apiName) || { count: 0, resetTime: now + 3600000 }; // Default 1 hour
current.count++;
if (resetTime) {
current.resetTime = resetTime * 1000; // Convert to milliseconds
}
this.requestCounts.set(apiName, current);
}
isRateLimited(apiName: string): boolean {
const current = this.requestCounts.get(apiName);
if (!current) return false;
const now = Date.now();
if (now > current.resetTime) {
this.requestCounts.delete(apiName);
return false;
}
// Conservative estimate - assume we're rate limited if we've made many requests recently
return current.count > 50;
}
getRemainingTime(apiName: string): number {
const current = this.requestCounts.get(apiName);
if (!current) return 0;
const now = Date.now();
return Math.max(0, current.resetTime - now);
}
}
// Global rate limiter instance
export const globalRateLimiter = new RateLimiter();