From c315265d16a14907e2fb0953fd48841e1233a91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Mon, 22 Jun 2026 16:27:23 -0300 Subject: [PATCH] fix: pause Tiny calls globally after rate blocks --- src/services/tiny-api.service.ts | 75 ++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/src/services/tiny-api.service.ts b/src/services/tiny-api.service.ts index 9adbe72..08890cb 100644 --- a/src/services/tiny-api.service.ts +++ b/src/services/tiny-api.service.ts @@ -9,6 +9,11 @@ const LOCK_STALE_MS = 30000; const DEFAULT_BLOCK_RETRY_DELAY_MS = 120000; const DEFAULT_BLOCK_MAX_RETRIES = 30; +type TinyRateLimitState = { + lastRequestAt?: number; + blockedUntil?: number; +}; + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const getPositiveNumberEnv = (name: string, fallback: number) => { @@ -22,17 +27,20 @@ const getBlockMaxRetries = () => getPositiveNumberEnv('TINY_BLOCK_MAX_RETRIES', const getLockDir = () => process.env.TINY_RATE_LIMIT_LOCK_DIR || DEFAULT_LOCK_DIR; const getStateFile = () => process.env.TINY_RATE_LIMIT_STATE_FILE || DEFAULT_STATE_FILE; -const readLastRequestAt = () => { +const readState = (): TinyRateLimitState => { try { const parsed = JSON.parse(fs.readFileSync(getStateFile(), 'utf-8')); - return Number(parsed.lastRequestAt || 0); + return { + lastRequestAt: Number(parsed.lastRequestAt || 0), + blockedUntil: Number(parsed.blockedUntil || 0) + }; } catch { - return 0; + return {}; } }; -const writeLastRequestAt = (timestamp: number) => { - fs.writeFileSync(getStateFile(), JSON.stringify({ lastRequestAt: timestamp }, null, 2)); +const writeState = (state: TinyRateLimitState) => { + fs.writeFileSync(getStateFile(), JSON.stringify(state, null, 2)); }; const removeStaleLock = (lockDir: string) => { @@ -67,21 +75,53 @@ const releaseLock = (lockDir: string) => { const waitForTinySlot = async () => { const minIntervalMs = getMinIntervalMs(); - if (!minIntervalMs) return; + while (true) { + const lockDir = await acquireLock(); + let waitMs = 0; + let reason = 'next Tiny API request'; + + try { + const state = readState(); + const now = Date.now(); + const lastRequestAt = Number(state.lastRequestAt || 0); + const blockedUntil = Number(state.blockedUntil || 0); + const intervalWaitMs = minIntervalMs ? Math.max(0, minIntervalMs - (now - lastRequestAt)) : 0; + const blockWaitMs = Math.max(0, blockedUntil - now); + + if (blockWaitMs > intervalWaitMs) { + waitMs = blockWaitMs; + reason = 'Tiny API block cooldown'; + } else { + waitMs = intervalWaitMs; + } + + if (!waitMs) { + writeState({ + ...state, + lastRequestAt: now, + blockedUntil: blockWaitMs ? blockedUntil : 0 + }); + return; + } + } finally { + releaseLock(lockDir); + } + + console.log(`[Tiny Rate Limit] Waiting ${waitMs}ms before ${reason}.`); + await sleep(waitMs); + } +}; + +const pauseAllTinyRequests = async (delayMs: number) => { const lockDir = await acquireLock(); try { - const lastRequestAt = readLastRequestAt(); - const elapsed = Date.now() - lastRequestAt; - const waitMs = Math.max(0, minIntervalMs - elapsed); - - if (waitMs > 0) { - console.log(`[Tiny Rate Limit] Waiting ${waitMs}ms before next Tiny API request.`); - await sleep(waitMs); - } - - writeLastRequestAt(Date.now()); + const state = readState(); + writeState({ + ...state, + blockedUntil: Math.max(Number(state.blockedUntil || 0), Date.now() + delayMs) + }); } finally { releaseLock(lockDir); } @@ -121,7 +161,8 @@ export const tinyPost = async (servicePhp: string, params: URLSearchParams) => { } const delayMs = getBlockRetryDelayMs(); - console.warn(`[Tiny Rate Limit] Tiny blocked ${servicePhp}. Waiting ${delayMs}ms before retry ${attempt}/${maxRetries}.`); + await pauseAllTinyRequests(delayMs); + console.warn(`[Tiny Rate Limit] Tiny blocked ${servicePhp}. Pausing all Tiny API requests for ${delayMs}ms before retry ${attempt}/${maxRetries}.`); await sleep(delayMs); } };