Skip to main content
Glama
base.ts6.94 kB
/** * Base Registrar Adapter. * * Abstract class that all registrar adapters extend. * Provides common functionality: * - Rate limiting (token bucket) * - Retry with exponential backoff * - Error handling and logging */ import type { DomainResult, TLDInfo } from '../types.js'; import { logger } from '../utils/logger.js'; import { RateLimitError, RegistrarApiError, TimeoutError, wrapError, } from '../utils/errors.js'; import { config } from '../config.js'; /** * Token bucket rate limiter. */ export class RateLimiter { private tokens: number; private readonly maxTokens: number; private readonly refillRate: number; // tokens per second private lastRefill: number; constructor(maxPerMinute: number = 60) { this.maxTokens = maxPerMinute; this.tokens = maxPerMinute; this.refillRate = maxPerMinute / 60; // Convert to per-second this.lastRefill = Date.now(); } /** * Try to consume a token. Returns true if successful. */ tryConsume(): boolean { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; return true; } return false; } /** * Wait until a token is available. */ async waitForToken(): Promise<void> { while (!this.tryConsume()) { // Calculate wait time for next token const waitMs = Math.ceil(1000 / this.refillRate); await sleep(waitMs); } } /** * Refill tokens based on elapsed time. */ private refill(): void { const now = Date.now(); const elapsed = (now - this.lastRefill) / 1000; const refillAmount = elapsed * this.refillRate; this.tokens = Math.min(this.maxTokens, this.tokens + refillAmount); this.lastRefill = now; } /** * Get seconds until next token is available. */ getWaitSeconds(): number { if (this.tokens >= 1) return 0; const neededTokens = 1 - this.tokens; return Math.ceil(neededTokens / this.refillRate); } } /** * Sleep helper. */ function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Abstract base class for registrar adapters. */ export abstract class RegistrarAdapter { /** Human-readable name of the registrar */ abstract readonly name: string; /** Identifier used in results */ abstract readonly id: string; /** Rate limiter for this registrar */ protected readonly rateLimiter: RateLimiter; /** Max retry attempts */ protected readonly maxRetries: number = 3; /** Base delay for exponential backoff (ms) */ protected readonly baseDelayMs: number = 2000; /** Request timeout (ms) */ protected readonly timeoutMs: number = 10000; constructor(requestsPerMinute: number = 60) { this.rateLimiter = new RateLimiter(requestsPerMinute); } /** * Check domain availability and get pricing. * This is the main method each adapter must implement. */ abstract search(domain: string, tld: string): Promise<DomainResult>; /** * Get information about a TLD. * Optional - not all registrars provide this. */ abstract getTldInfo(tld: string): Promise<TLDInfo | null>; /** * Check if this adapter is enabled (has required credentials). */ abstract isEnabled(): boolean; /** * Execute a function with rate limiting. */ protected async rateLimitedCall<T>(fn: () => Promise<T>): Promise<T> { // Check if we should even try (dry run mode) if (config.dryRun) { throw new RegistrarApiError(this.name, 'Dry run mode - no API calls made'); } // Wait for rate limit const waitSeconds = this.rateLimiter.getWaitSeconds(); if (waitSeconds > 0) { logger.debug(`Rate limiting: waiting ${waitSeconds}s for ${this.name}`); } await this.rateLimiter.waitForToken(); return fn(); } /** * Execute with retry and exponential backoff. */ protected async retryWithBackoff<T>( fn: () => Promise<T>, operation: string, ): Promise<T> { let lastError: Error | undefined; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { return await this.rateLimitedCall(fn); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Check if we should retry const wrapped = wrapError(error); if (!wrapped.retryable) { throw wrapped; } // Check if it's a rate limit error with retry-after if (error instanceof RateLimitError && error.retryAfter) { const waitMs = error.retryAfter - Date.now(); if (waitMs > 0 && waitMs < 60000) { logger.info(`Rate limited, waiting ${waitMs}ms`, { registrar: this.name, attempt, }); await sleep(waitMs); continue; } } // Exponential backoff if (attempt < this.maxRetries) { const delay = this.baseDelayMs * Math.pow(2, attempt - 1); logger.warn(`Retry ${attempt}/${this.maxRetries} for ${operation}`, { registrar: this.name, delay_ms: delay, error: lastError.message, }); await sleep(delay); } } } // All retries failed throw lastError || new Error(`Failed after ${this.maxRetries} retries`); } /** * Create a timeout wrapper for a promise. */ protected withTimeout<T>( promise: Promise<T>, operation: string, timeoutMs: number = this.timeoutMs, ): Promise<T> { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new TimeoutError(operation, timeoutMs)); }, timeoutMs); promise .then((result) => { clearTimeout(timer); resolve(result); }) .catch((error) => { clearTimeout(timer); reject(error); }); }); } /** * Log an error with context. */ protected logError(error: Error, context?: Record<string, unknown>): void { logger.logError(`${this.name} error`, error, { registrar: this.id, ...context, }); } /** * Create a standardized DomainResult. */ protected createResult( domain: string, tld: string, data: Partial<DomainResult>, ): DomainResult { return { domain: `${domain}.${tld}`, available: data.available ?? false, premium: data.premium ?? false, price_first_year: data.price_first_year ?? null, price_renewal: data.price_renewal ?? null, currency: data.currency ?? 'USD', privacy_included: data.privacy_included ?? false, transfer_price: data.transfer_price ?? null, registrar: this.id, source: data.source ?? (`${this.id}_api` as DomainResult['source']), checked_at: new Date().toISOString(), premium_reason: data.premium_reason, tld_restrictions: data.tld_restrictions, score: data.score, }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dorukardahan/domain-search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server