UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

197 lines 6.86 kB
/** * Base API client with rate limiting and retry logic * * @module research/clients/base */ import { ResearchError, ResearchErrorCode, } from '../types.js'; /** * Token bucket rate limiter */ export class RateLimiter { config; tokens; lastRefill; constructor(config) { this.config = config; this.tokens = config.currentTokens; this.lastRefill = config.lastRefill; // Use config value, not Date.now() } /** * Refill tokens based on elapsed time */ refill() { const now = Date.now(); const elapsed = (now - this.lastRefill) / 1000; // Convert to seconds const tokensToAdd = elapsed * this.config.refillRate; this.tokens = Math.min(this.config.maxTokens, this.tokens + tokensToAdd); this.lastRefill = now; } /** * Acquire a token, waiting if necessary */ async acquire() { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; return; } // Calculate wait time const deficit = 1 - this.tokens; const waitMs = (deficit / this.config.refillRate) * 1000; await this.sleep(waitMs); // Refill again after waiting this.refill(); this.tokens -= 1; } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Get current token count (for testing) */ getCurrentTokens() { this.refill(); return this.tokens; } } /** * Base API client with common functionality */ export class BaseClient { config; rateLimiter; retryConfig; constructor(config) { this.config = config; this.rateLimiter = new RateLimiter(config.rateLimit); this.retryConfig = config.retry; } /** * Execute an HTTP request with rate limiting and retry logic */ async request(url, options = {}) { // Apply rate limiting await this.rateLimiter.acquire(); // Execute with retry return this.executeWithRetry(url, options); } /** * Execute request with exponential backoff retry */ async executeWithRetry(url, options, attempt = 0, firstError) { const controller = new AbortController(); let timeoutId; try { // Create timeout promise const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { controller.abort(); reject(new ResearchError(ResearchErrorCode.RF_104, `Request timeout after ${this.config.timeout}ms`)); }, this.config.timeout); }); // Create fetch promise const fetchPromise = fetch(url, { ...options, signal: controller.signal, }); // Race between fetch and timeout const response = await Promise.race([fetchPromise, timeoutPromise]); // Clear timeout if fetch completed first if (timeoutId) { clearTimeout(timeoutId); } if (!response.ok) { const error = await this.handleHttpError(response); throw error; } return (await response.json()); } catch (error) { // Clear timeout on error if (timeoutId) { clearTimeout(timeoutId); } // Handle abort (timeout) if (error instanceof Error && error.name === 'AbortError') { throw new ResearchError(ResearchErrorCode.RF_104, `Request timeout after ${this.config.timeout}ms`, error); } // Track the first error for better error reporting const currentFirstError = firstError || (error instanceof ResearchError ? error : undefined); // Don't retry ResearchErrors (they're already handled) if (error instanceof ResearchError) { // Retry on transient errors if (this.isRetriable(error) && attempt < this.retryConfig.maxRetries) { const delay = this.calculateBackoff(attempt); await this.sleep(delay); return this.executeWithRetry(url, options, attempt + 1, currentFirstError); } throw error; } // Retry on network errors if (attempt < this.retryConfig.maxRetries) { const delay = this.calculateBackoff(attempt); await this.sleep(delay); return this.executeWithRetry(url, options, attempt + 1, currentFirstError); } // If we have a first error from an HTTP response, throw that instead of generic network error if (currentFirstError) { throw currentFirstError; } throw new ResearchError(ResearchErrorCode.RF_500, 'Network error', error); } } /** * Handle HTTP error responses */ async handleHttpError(response) { const status = response.status; if (status === 404) { return new ResearchError(ResearchErrorCode.RF_300, 'Resource not found'); } if (status === 429) { return new ResearchError(ResearchErrorCode.RF_103, 'Rate limit exceeded'); } if (status === 401 || status === 403) { return new ResearchError(ResearchErrorCode.RF_102, 'Invalid API key or unauthorized'); } if (status >= 500) { return new ResearchError(ResearchErrorCode.RF_200, 'API server error'); } return new ResearchError(ResearchErrorCode.RF_100, `HTTP error ${status}`); } /** * Determine if an error is retriable */ isRetriable(error) { if (error instanceof ResearchError) { // Only retry on server errors (not rate limits or client errors) return error.code === ResearchErrorCode.RF_200; } // Retry on network errors return true; } /** * Calculate exponential backoff delay */ calculateBackoff(attempt) { const delay = this.retryConfig.initialDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt); return Math.min(delay, this.retryConfig.maxDelay); } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Build URL with query parameters */ buildUrl(path, params) { const url = new URL(path, this.config.baseUrl); Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, String(value)); } }); return url.toString(); } } //# sourceMappingURL=base.js.map