From 52b025ca5ab6ce7126e229ff42a50cb155e5a5e3 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Tue, 17 Feb 2026 03:41:21 -0300 Subject: [PATCH] =?UTF-8?q?refactor(types):=20Wave=203b=20=E2=80=94=20usag?= =?UTF-8?q?e,=20CLI=20runtime,=20SSE=20auth/logger=20typed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - callLogs.ts: typed all CRUD params + error catches - usageHistory.ts: typed pending requests, filter/entry params - cliRuntime.ts: typed CLI_TOOLS map + all arrow function params - sse/auth.ts: typed credentials/fallback/mutex params + Date.getTime() - sse/utils/logger.ts: typed all logger functions + made data optional TS errors: 578 → 490 (-88) Total reduction: 984 → 490 (-494, 50.2%) Build: ✅ Tests: 368/368 ✅ --- src/lib/usage/callLogs.ts | 26 ++++++++++----------- src/lib/usage/usageHistory.ts | 23 +++++++++++-------- src/shared/services/cliRuntime.ts | 26 ++++++++++----------- src/sse/services/auth.ts | 38 ++++++++++++++----------------- src/sse/utils/logger.ts | 18 +++++++-------- 5 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts index 412d1d83ec..25ac374178 100644 --- a/src/lib/usage/callLogs.ts +++ b/src/lib/usage/callLogs.ts @@ -24,7 +24,7 @@ function generateLogId() { /** * Save a structured call log entry. */ -export async function saveCallLog(entry) { +export async function saveCallLog(entry: any) { if (!shouldPersistToDisk) return; try { @@ -38,7 +38,7 @@ export async function saveCallLog(entry) { } catch {} // Truncate large payloads for DB storage (keep under 8KB each) - const truncatePayload = (obj) => { + const truncatePayload = (obj: any) => { if (!obj) return null; const str = JSON.stringify(obj); if (str.length <= 8192) return str; @@ -107,7 +107,7 @@ export async function saveCallLog(entry) { entry.requestBody, entry.responseBody ); - } catch (error) { + } catch (error: any) { console.error("[callLogs] Failed to save call log:", error.message); } } @@ -115,7 +115,7 @@ export async function saveCallLog(entry) { /** * Write call log as JSON file to disk (full payloads, not truncated). */ -function writeCallLogToDisk(logEntry, requestBody, responseBody) { +function writeCallLogToDisk(logEntry: any, requestBody: any, responseBody: any) { if (!CALL_LOGS_DIR) return; try { @@ -136,7 +136,7 @@ function writeCallLogToDisk(logEntry, requestBody, responseBody) { }; fs.writeFileSync(path.join(dir, filename), JSON.stringify(fullEntry, null, 2)); - } catch (err) { + } catch (err: any) { console.error("[callLogs] Failed to write disk log:", err.message); } } @@ -160,7 +160,7 @@ export function rotateCallLogs() { console.log(`[callLogs] Rotated old logs: ${entry}`); } } - } catch (err) { + } catch (err: any) { console.error("[callLogs] Failed to rotate logs:", err.message); } } @@ -175,11 +175,11 @@ if (shouldPersistToDisk) { /** * Get call logs with optional filtering. */ -export async function getCallLogs(filter = {}) { +export async function getCallLogs(filter: any = {}) { const db = getDbInstance(); let sql = "SELECT * FROM call_logs"; - const conditions = []; - const params = {}; + const conditions: string[] = []; + const params: Record = {}; if (filter.status) { if (filter.status === "error") { @@ -257,7 +257,7 @@ export async function getCallLogs(filter = {}) { /** * Get a single call log by ID (with full payloads from disk when available). */ -export async function getCallLogById(id) { +export async function getCallLogById(id: string) { const db = getDbInstance(); const row = db.prepare("SELECT * FROM call_logs WHERE id = ?").get(id); if (!row) return null; @@ -296,7 +296,7 @@ export async function getCallLogById(id) { responseBody: diskEntry.responseBody ?? entry.responseBody, }; } - } catch (err) { + } catch (err: any) { console.error("[callLogs] Failed to read full log from disk:", err.message); } } @@ -307,7 +307,7 @@ export async function getCallLogById(id) { /** * Read the full (untruncated) log entry from disk. */ -function readFullLogFromDisk(entry) { +function readFullLogFromDisk(entry: any) { if (!CALL_LOGS_DIR || !entry.timestamp) return null; try { @@ -332,7 +332,7 @@ function readFullLogFromDisk(entry) { if (files.length > 0) { return JSON.parse(fs.readFileSync(path.join(dir, files[0]), "utf8")); } - } catch (err) { + } catch (err: any) { console.error("[callLogs] Disk log read error:", err.message); } diff --git a/src/lib/usage/usageHistory.ts b/src/lib/usage/usageHistory.ts index e02b82ff3b..5457c7ebee 100644 --- a/src/lib/usage/usageHistory.ts +++ b/src/lib/usage/usageHistory.ts @@ -13,7 +13,10 @@ import { shouldPersistToDisk } from "./migrations"; // ──────────────── Pending Requests (in-memory) ──────────────── -const pendingRequests = { +const pendingRequests: { + byModel: Record; + byAccount: Record>; +} = { byModel: {}, byAccount: {}, }; @@ -21,7 +24,7 @@ const pendingRequests = { /** * Track a pending request. */ -export function trackPendingRequest(model, provider, connectionId, started) { +export function trackPendingRequest(model: string, provider: string, connectionId: string | null, started: boolean) { const modelKey = provider ? `${model} (${provider})` : model; if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0; @@ -84,7 +87,7 @@ export async function getUsageDb() { /** * Save request usage entry to SQLite. */ -export async function saveRequestUsage(entry) { +export async function saveRequestUsage(entry: any) { if (!shouldPersistToDisk) return; try { @@ -122,11 +125,11 @@ export async function saveRequestUsage(entry) { /** * Get usage history with optional filters. */ -export async function getUsageHistory(filter = {}) { +export async function getUsageHistory(filter: any = {}) { const db = getDbInstance(); let sql = "SELECT * FROM usage_history"; - const conditions = []; - const params = {}; + const conditions: string[] = []; + const params: Record = {}; if (filter.provider) { conditions.push("provider = @provider"); @@ -175,7 +178,7 @@ import fs from "fs"; import { LOG_FILE } from "./migrations"; function formatLogDate(date = new Date()) { - const pad = (n) => String(n).padStart(2, "0"); + const pad = (n: number) => String(n).padStart(2, "0"); const d = pad(date.getDate()); const m = pad(date.getMonth() + 1); const y = date.getFullYear(); @@ -188,7 +191,7 @@ function formatLogDate(date = new Date()) { /** * Append to log.txt. */ -export async function appendRequestLog({ model, provider, connectionId, tokens, status }) { +export async function appendRequestLog({ model, provider, connectionId, tokens, status }: { model?: string; provider?: string; connectionId?: string; tokens?: any; status?: string | number }) { if (!shouldPersistToDisk) return; try { @@ -225,7 +228,7 @@ export async function appendRequestLog({ model, provider, connectionId, tokens, if (lines.length > 200) { fs.writeFileSync(LOG_FILE, lines.slice(-200).join("\n") + "\n"); } - } catch (error) { + } catch (error: any) { console.error("Failed to append to log.txt:", error.message); } } @@ -243,7 +246,7 @@ export async function getRecentLogs(limit = 200) { const content = fs.readFileSync(LOG_FILE, "utf-8"); const lines = content.trim().split("\n"); return lines.slice(-limit).reverse(); - } catch (error) { + } catch (error: any) { console.error("[usageDb] Failed to read log.txt:", error.message); return []; } diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index 1b61edb7bc..57db407485 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -6,7 +6,7 @@ import { spawn } from "child_process"; const VALID_RUNTIME_MODES = new Set(["auto", "host", "container"]); const FALSE_VALUES = new Set(["0", "false", "no", "off"]); -const CLI_TOOLS = { +const CLI_TOOLS: Record = { claude: { defaultCommand: "claude", envBinKey: "CLI_CLAUDE_BIN", @@ -90,12 +90,12 @@ const CLI_TOOLS = { const isWindows = () => process.platform === "win32"; -const parseBoolean = (value, defaultValue = true) => { +const parseBoolean = (value: unknown, defaultValue = true) => { if (value == null || value === "") return defaultValue; return !FALSE_VALUES.has(String(value).trim().toLowerCase()); }; -const runProcess = (command, args, { env, timeoutMs = 3000 } = {}) => +const runProcess = (command: string, args: string[], { env, timeoutMs = 3000 }: { env?: Record; timeoutMs?: number } = {}): Promise => new Promise((resolve) => { let stdout = ""; let stderr = ""; @@ -108,7 +108,7 @@ const runProcess = (command, args, { env, timeoutMs = 3000 } = {}) => child.kill("SIGKILL"); }, timeoutMs); - const done = (result) => { + const done = (result: any) => { if (settled) return; settled = true; clearTimeout(timer); @@ -168,7 +168,7 @@ const getLookupEnv = () => { return env; }; -const resolveToolCommands = (toolId) => { +const resolveToolCommands = (toolId: string): string[] => { const tool = CLI_TOOLS[toolId]; if (!tool) return []; const envCommand = String(process.env[tool.envBinKey] || "").trim(); @@ -179,7 +179,7 @@ const resolveToolCommands = (toolId) => { return tool.defaultCommand ? [tool.defaultCommand] : []; }; -const checkExplicitPath = async (commandPath) => { +const checkExplicitPath = async (commandPath: string) => { try { await fs.access(commandPath, fs.constants.F_OK); } catch { @@ -194,7 +194,7 @@ const checkExplicitPath = async (commandPath) => { } }; -const locateCommand = async (command, env) => { +const locateCommand = async (command: string, env: Record) => { if (!command) { return { installed: false, commandPath: null, reason: "missing_command" }; } @@ -231,7 +231,7 @@ const locateCommand = async (command, env) => { return { installed: !!first, commandPath: first, reason: first ? null : "not_found" }; }; -const locateCommandCandidate = async (commands, env) => { +const locateCommandCandidate = async (commands: string[], env: Record) => { if (!Array.isArray(commands) || commands.length === 0) { return { command: null, installed: false, commandPath: null, reason: "missing_command" }; } @@ -246,7 +246,7 @@ const locateCommandCandidate = async (commands, env) => { return { command: commands[0], installed: false, commandPath: null, reason: "not_found" }; }; -const checkRunnable = async (commandPath, env, timeoutMs = 4000) => { +const checkRunnable = async (commandPath: string, env: Record, timeoutMs = 4000) => { for (const args of [["--version"], ["-v"]]) { const result = await runProcess(commandPath, args, { env, timeoutMs }); if (result.ok) { @@ -267,23 +267,23 @@ export const ensureCliConfigWriteAllowed = () => { export const getCliConfigHome = () => String(process.env.CLI_CONFIG_HOME || "").trim() || os.homedir(); -export const getCliConfigPaths = (toolId) => { +export const getCliConfigPaths = (toolId: string) => { const tool = CLI_TOOLS[toolId]; if (!tool) return null; const home = getCliConfigHome(); return Object.fromEntries( - Object.entries(tool.paths).map(([key, relativePath]) => [key, path.join(home, relativePath)]) + Object.entries(tool.paths).map(([key, relativePath]) => [key, path.join(home, relativePath as string)]) ); }; -export const getCliPrimaryConfigPath = (toolId) => { +export const getCliPrimaryConfigPath = (toolId: string) => { const paths = getCliConfigPaths(toolId); if (!paths) return null; const firstKey = Object.keys(paths)[0]; return firstKey ? paths[firstKey] : null; }; -export const getCliRuntimeStatus = async (toolId) => { +export const getCliRuntimeStatus = async (toolId: string) => { const tool = CLI_TOOLS[toolId]; const runtimeMode = getRuntimeMode(); if (!tool) { diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 32aad27742..f405ba6622 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -21,7 +21,7 @@ let selectionMutex = Promise.resolve(); // ─── Anti-Thundering Herd: per-connection mutex for markAccountUnavailable ─── // Prevents multiple concurrent requests from marking the same connection // unavailable in parallel, which was the root cause of cascading 502 lockouts. -const markMutexes = new Map(); +const markMutexes = new Map>(); /** * Get provider credentials from localDb @@ -29,10 +29,10 @@ const markMutexes = new Map(); * @param {string} provider - Provider name * @param {string|null} excludeConnectionId - Connection ID to exclude (for retry with next account) */ -export async function getProviderCredentials(provider, excludeConnectionId = null) { +export async function getProviderCredentials(provider: string, excludeConnectionId: string | null = null) { // Acquire mutex to prevent race conditions const currentMutex = selectionMutex; - let resolveMutex; + let resolveMutex: (() => void) | undefined; selectionMutex = new Promise((resolve) => { resolveMutex = resolve; }); @@ -105,7 +105,7 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul (c) => c.rateLimitedUntil && new Date(c.rateLimitedUntil).getTime() > Date.now() ); const earliestConn = rateLimitedConns.sort( - (a, b) => new Date(a.rateLimitedUntil) - new Date(b.rateLimitedUntil) + (a: any, b: any) => new Date(a.rateLimitedUntil).getTime() - new Date(b.rateLimitedUntil).getTime() )[0]; log.warn( "AUTH", @@ -131,11 +131,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul const stickyLimit = settings.stickyRoundRobinLimit || 3; // Sort by lastUsed (most recent first) to find current candidate - const byRecency = [...availableConnections].sort((a, b) => { + const byRecency = [...availableConnections].sort((a: any, b: any) => { if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); if (!a.lastUsedAt) return 1; if (!b.lastUsedAt) return -1; - return new Date(b.lastUsedAt) - new Date(a.lastUsedAt); + return new Date(b.lastUsedAt).getTime() - new Date(a.lastUsedAt).getTime(); }); const current = byRecency[0]; @@ -151,11 +151,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul }); } else { // Pick the least recently used (excluding current if possible) - const sortedByOldest = [...availableConnections].sort((a, b) => { + const sortedByOldest = [...availableConnections].sort((a: any, b: any) => { if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); if (!a.lastUsedAt) return -1; if (!b.lastUsedAt) return 1; - return new Date(a.lastUsedAt) - new Date(b.lastUsedAt); + return new Date(a.lastUsedAt).getTime() - new Date(b.lastUsedAt).getTime(); }); connection = sortedByOldest[0]; @@ -200,18 +200,14 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul * @returns {{ shouldFallback: boolean, cooldownMs: number }} */ export async function markAccountUnavailable( - connectionId, - status, - errorText, - provider = null, - model = null + connectionId: string, + status: number, + errorText: string, + provider: string | null = null, + model: string | null = null ) { - // ─── Anti-Thundering Herd Mutex ───────────────────────────────── - // Wait for any in-flight markAccountUnavailable call for the SAME connection. - // This prevents 5 concurrent 502s from each independently marking the account - // with backoffLevel 0 → 1, when only the first should. const currentMutex = markMutexes.get(connectionId) || Promise.resolve(); - let resolveMutex; + let resolveMutex: (() => void) | undefined; markMutexes.set( connectionId, new Promise((resolve) => { @@ -284,7 +280,7 @@ export async function markAccountUnavailable( * Clear account error status (only if currently has error) * Optimized to avoid unnecessary DB updates */ -export async function clearAccountError(connectionId, currentConnection) { +export async function clearAccountError(connectionId: string, currentConnection: any) { // Only update if currently has error status const hasError = currentConnection.testStatus === "unavailable" || @@ -306,7 +302,7 @@ export async function clearAccountError(connectionId, currentConnection) { /** * Extract API key from request headers */ -export function extractApiKey(request) { +export function extractApiKey(request: Request) { const authHeader = request.headers.get("Authorization"); if (authHeader?.startsWith("Bearer ")) { return authHeader.slice(7); @@ -317,7 +313,7 @@ export function extractApiKey(request) { /** * Validate API key (optional - for local use can skip) */ -export async function isValidApiKey(apiKey) { +export async function isValidApiKey(apiKey: string) { if (!apiKey) return false; return await validateApiKey(apiKey); } diff --git a/src/sse/utils/logger.ts b/src/sse/utils/logger.ts index da1186886e..c3caf39cfc 100644 --- a/src/sse/utils/logger.ts +++ b/src/sse/utils/logger.ts @@ -8,27 +8,27 @@ import { createLogger, logger as rootLogger } from "@/shared/utils/logger"; const log = createLogger("sse"); -export function debug(tag, message, data) { +export function debug(tag: string, message: string, data?: unknown) { log.debug({ tag, ...spreadData(data) }, message); } -export function info(tag, message, data) { +export function info(tag: string, message: string, data?: unknown) { log.info({ tag, ...spreadData(data) }, message); } -export function warn(tag, message, data) { +export function warn(tag: string, message: string, data?: unknown) { log.warn({ tag, ...spreadData(data) }, message); } -export function error(tag, message, data) { +export function error(tag: string, message: string, data?: unknown) { log.error({ tag, ...spreadData(data) }, message); } -export function request(method, path, extra) { +export function request(method: string, path: string, extra?: unknown) { log.info({ tag: "HTTP", method, path, ...spreadData(extra) }, `📥 ${method} ${path}`); } -export function response(status, duration, extra) { +export function response(status: number, duration: number, extra?: unknown) { const level = status < 400 ? "info" : "error"; log[level]( { tag: "HTTP", status, duration, ...spreadData(extra) }, @@ -36,7 +36,7 @@ export function response(status, duration, extra) { ); } -export function stream(event, data) { +export function stream(event: string, data?: unknown) { log.debug({ tag: "STREAM", event, ...spreadData(data) }, `🌊 ${event}`); } @@ -44,9 +44,9 @@ export function stream(event, data) { export { maskKey } from "@/shared/utils/formatting"; // Helper to spread data into structured fields -function spreadData(data) { +function spreadData(data: unknown): Record { if (!data) return {}; if (typeof data === "string") return { detail: data }; - if (typeof data === "object") return data; + if (typeof data === "object") return data as Record; return { detail: String(data) }; }