UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

179 lines 7.04 kB
/** * AccountPool — manages a pool of proxy accounts with round-robin / fill-first * selection and exponential-backoff cooldowns for quota-exceeded accounts. * * @module auth/accountPool */ import { tokenStore } from "./tokenStore.js"; // ============================================================================= // CONSTANTS // ============================================================================= const DEFAULT_COOLDOWN_MS = 300_000; const DEFAULT_MAX_COOLDOWN_MS = 1_800_000; // ============================================================================= // IMPLEMENTATION // ============================================================================= export class AccountPool { accounts = new Map(); cursor = 0; config; constructor(config = {}) { this.config = { strategy: config.strategy ?? "round-robin", defaultCooldownMs: config.defaultCooldownMs ?? DEFAULT_COOLDOWN_MS, maxCooldownMs: config.maxCooldownMs ?? DEFAULT_MAX_COOLDOWN_MS, maxRetryAccounts: config.maxRetryAccounts ?? 0, }; } /** Add (or replace) an account in the pool. */ addAccount(account) { // Clear any stale token refresher for a previously-registered account // with the same id — wireTokenRefresh() captures the old account object // in its closure, so leaving it behind would refresh the wrong tokens. tokenStore.clearTokenRefresher(account.id); this.accounts.set(account.id, { ...account }); } /** Remove an account from the pool by id. */ removeAccount(id) { tokenStore.clearTokenRefresher(id); this.accounts.delete(id); } /** * Return the next healthy account according to the configured strategy. * Returns `null` when no healthy accounts are available. */ getNextAccount() { this._expireCooldowns(); const healthy = this._getHealthyAccounts(); if (healthy.length === 0) { return null; } if (this.config.strategy === "fill-first") { const account = healthy[0]; account.requestCount++; account.lastUsed = Date.now(); return account; } // round-robin const index = this.cursor % healthy.length; this.cursor++; const account = healthy[index]; account.requestCount++; account.lastUsed = Date.now(); return account; } /** * Mark an account as quota-exceeded. The account enters a cooldown state * with exponential backoff capped at `maxCooldownMs`. * * @deprecated Proxy routes now use `RuntimeAccountState` in `claudeProxyRoutes.ts` * for runtime account state management. This method is retained for public API compatibility. * * @param id Account id * @param retryAfterMs Optional explicit retry-after duration (from server header) */ markQuotaExceeded(id, retryAfterMs) { const account = this.accounts.get(id); if (!account) { return; } account.consecutiveFailures++; const backoff = Math.min(this.config.defaultCooldownMs * Math.pow(2, account.consecutiveFailures - 1), this.config.maxCooldownMs); account.cooldownUntil = Date.now() + (retryAfterMs ?? backoff); account.status = "cooling"; } /** * Manually mark an account as available (healthy), clearing its cooldown. * * @deprecated Proxy routes now use `RuntimeAccountState` in `claudeProxyRoutes.ts` * for runtime account state management. This method is retained for public API compatibility. */ markAvailable(id) { const account = this.accounts.get(id); if (!account) { return; } account.status = "healthy"; account.cooldownUntil = undefined; } /** * Mark an account as having completed a request successfully. * * @deprecated Proxy routes now use `RuntimeAccountState` in `claudeProxyRoutes.ts` * for runtime account state management. This method is retained for public API compatibility. */ markSuccess(id) { const account = this.accounts.get(id); if (!account) { return; } account.status = "healthy"; account.cooldownUntil = undefined; account.consecutiveFailures = 0; } /** Return the number of currently healthy (not cooling/disabled) accounts. */ getHealthyCount() { this._expireCooldowns(); return this._getHealthyAccounts().length; } /** Return a snapshot of all accounts in the pool. */ getAllAccounts() { return Array.from(this.accounts.values()); } /** Return the configured selection strategy. */ getStrategy() { return this.config.strategy; } /** * Wire automatic token refresh for an OAuth account. * Call after addAccount() for OAuth accounts that have a refresh token. * * @param accountId The account id to wire refresh for * @param refreshFn Function that takes a refresh token and returns new token data */ wireTokenRefresh(accountId, refreshFn) { const account = this.accounts.get(accountId); if (!account || account.type !== "oauth" || !account.tokens?.refreshToken) { return; } tokenStore.setTokenRefresher(accountId, async (refreshToken) => { const result = await refreshFn(refreshToken); const newTokens = { accessToken: result.accessToken, refreshToken: result.refreshToken ?? refreshToken, expiresAt: result.expiresAt instanceof Date ? result.expiresAt.getTime() : result.expiresAt, tokenType: result.tokenType ?? "Bearer", }; // Update in-memory account if (account.tokens) { account.tokens.accessToken = newTokens.accessToken; account.tokens.expiresAt = newTokens.expiresAt; if (newTokens.refreshToken) { account.tokens.refreshToken = newTokens.refreshToken; } } return newTokens; }); } // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- _getHealthyAccounts() { return Array.from(this.accounts.values()).filter((a) => a.status === "healthy"); } _expireCooldowns() { const now = Date.now(); for (const account of this.accounts.values()) { if (account.status === "cooling" && account.cooldownUntil && now >= account.cooldownUntil) { account.status = "healthy"; account.cooldownUntil = undefined; } } } } //# sourceMappingURL=accountPool.js.map