diff --git a/apps/api/.env.example b/apps/api/.env.example index b1132ab..d1c9a04 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -17,6 +17,10 @@ # running app, and by scripts/test-env.cjs for tests. DATABASE_URL="postgresql://fintrack:fintrack@localhost:5432/fintrack?schema=public" DIRECT_URL="postgresql://fintrack:fintrack@localhost:5432/fintrack?schema=public" +# In production behind a transaction pooler (e.g. Supabase, port 6543), append +# ?pgbouncer=true&connection_limit=1 to DATABASE_URL — Prisma must skip prepared +# statements or pooled requests fail with `prepared statement "sN" does not exist`. +# Keep DIRECT_URL on a direct connection (port 5432, no flag) for migrations. REDIS_URL="redis://localhost:6379" # Secrets & tokens diff --git a/apps/bot/src/services/apiClient.ts b/apps/bot/src/services/apiClient.ts index 084c3bf..e6b7e2d 100644 --- a/apps/bot/src/services/apiClient.ts +++ b/apps/bot/src/services/apiClient.ts @@ -1,7 +1,7 @@ import { config } from "../config.js"; +import { logger } from "../lib/logger.js"; import { type TokenPair, - deleteTokens, getTokens, hasTokens, setTokens, @@ -47,6 +47,21 @@ async function exchangeTokens( return data; } +// Dedupe concurrent re-exchanges per user: a burst of 401s would otherwise fire +// one exchange each and blow the API's exchange rate limit (10 / 15 min). +const inflightExchange = new Map>(); + +function exchangeOnce(telegramId: number, name: string): Promise { + let inflight = inflightExchange.get(telegramId); + if (!inflight) { + inflight = exchangeTokens(telegramId, name).finally(() => { + inflightExchange.delete(telegramId); + }); + inflightExchange.set(telegramId, inflight); + } + return inflight; +} + async function apiFetch( method: string, path: string, @@ -67,14 +82,18 @@ async function apiFetch( if (res.status === 401 && tokens && !retried) { try { - await exchangeTokens(telegramId, tokens.name); - } catch { - await deleteTokens(telegramId); + await exchangeOnce(telegramId, tokens.name); + } catch (err) { + logger.warn({ err, path }, "token re-exchange failed"); return res; } return apiFetch(method, path, telegramId, body, true); } + if (!res.ok) { + logger.warn({ status: res.status, method, path }, "api request failed"); + } + return res; }