import axios from 'axios'; import fs from 'fs'; import os from 'os'; import path from 'path'; const DEFAULT_LOCK_DIR = path.join(os.tmpdir(), 'api-tiny-n8n-rate-limit.lock'); const DEFAULT_STATE_FILE = path.join(os.tmpdir(), 'api-tiny-n8n-rate-limit.json'); 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) => { const configured = Number(process.env[name] || fallback); return Number.isFinite(configured) && configured > 0 ? configured : 0; }; const getMinIntervalMs = () => getPositiveNumberEnv('TINY_MIN_REQUEST_INTERVAL_MS', 0); const getBlockRetryDelayMs = () => getPositiveNumberEnv('TINY_BLOCK_RETRY_DELAY_MS', DEFAULT_BLOCK_RETRY_DELAY_MS); const getBlockMaxRetries = () => getPositiveNumberEnv('TINY_BLOCK_MAX_RETRIES', DEFAULT_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 readState = (): TinyRateLimitState => { try { const parsed = JSON.parse(fs.readFileSync(getStateFile(), 'utf-8')); return { lastRequestAt: Number(parsed.lastRequestAt || 0), blockedUntil: Number(parsed.blockedUntil || 0) }; } catch { return {}; } }; const writeState = (state: TinyRateLimitState) => { fs.writeFileSync(getStateFile(), JSON.stringify(state, null, 2)); }; const removeStaleLock = (lockDir: string) => { try { const stats = fs.statSync(lockDir); if (Date.now() - stats.mtimeMs > LOCK_STALE_MS) { fs.rmSync(lockDir, { recursive: true, force: true }); } } catch { // Lock does not exist or cannot be inspected. The acquire loop will retry. } }; const acquireLock = async () => { const lockDir = getLockDir(); while (true) { try { fs.mkdirSync(lockDir); return lockDir; } catch (error: any) { if (error?.code !== 'EEXIST') throw error; removeStaleLock(lockDir); await sleep(100); } } }; const releaseLock = (lockDir: string) => { fs.rmSync(lockDir, { recursive: true, force: true }); }; const waitForTinySlot = async () => { const minIntervalMs = getMinIntervalMs(); 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 state = readState(); writeState({ ...state, blockedUntil: Math.max(Number(state.blockedUntil || 0), Date.now() + delayMs) }); } finally { releaseLock(lockDir); } }; const getTinyErrors = (data: any) => { const erros = data?.retorno?.erros; if (!erros) return []; return Array.isArray(erros) ? erros : [erros]; }; const isTinyApiBlocked = (data: any) => { return getTinyErrors(data).some((entry: any) => { const message = String(entry?.erro || entry || '').toLowerCase(); return message.includes('api bloqueada') || message.includes('excedido o nĂºmero de acessos'); }); }; export const tinyPost = async (servicePhp: string, params: URLSearchParams) => { let attempt = 0; while (true) { await waitForTinySlot(); const response = await axios.post(`https://api.tiny.com.br/api2/${servicePhp}`, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (!isTinyApiBlocked(response.data)) { return response; } attempt += 1; const maxRetries = getBlockMaxRetries(); if (attempt > maxRetries) { return response; } const delayMs = getBlockRetryDelayMs(); 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); } };