import express from 'express'; import { getCurrenciesInfo, getEventsByIds } from '../libs/globalData.js'; import { pinnacleGet, pinnaclePost } from '../libs/pinnacleClient.js'; const router = express.Router(); const SECOND = 1000; const MINUTE = 60 * SECOND; const getCacheTtls = new Map([ ['/v3/sports', MINUTE], ['/v2/inrunning', 2 * SECOND], ['/v1/bets/betting-status', SECOND], ['/v1/cancellationreasons', 10 * MINUTE], ]); const getRateLimitTtls = new Map([ ['/v3/leagues', MINUTE], ['/v3/bets', 2 * SECOND], ]); const responseCache = new Map(); const inFlightRequests = new Map(); const lastRequests = new Map(); const allowedEndpoints = new Map([ ['GET /v2/line', 'Get Straight Line - v2'], ['POST /v3/line/parlay', 'Get Parlay Line - v3'], ['POST /v1/line/teaser', 'Get Teaser Line - v1'], ['GET /v2/line/special', 'Get Special Line - v2'], ['GET /v3/sports', 'Get Sports - v3'], ['GET /v3/leagues', 'Get Leagues - v3'], ['GET /v1/periods', 'Get Periods - v1'], ['GET /v2/inrunning', 'Get In-Running - v2'], ['GET /v1/cancellationreasons', 'Get Cancellation Reasons - v1'], ['GET /v2/currencies', 'Get Currencies - v2'], ['GET /v1/client/balance', 'Get Client Balance - v1'], ['POST /v4/bets/place', 'Place straight bet - v4'], ['POST /v4/bets/parlay', 'Place parlay bet - v4'], ['POST /v4/bets/teaser', 'Place teaser bet - v4'], ['POST /v4/bets/special', 'Place specials bet - v4'], ['GET /v3/bets', 'Get Bets - v3'], ['GET /v3/bets/settled', 'Get Bets Settled - v3'], ['GET /v1/regrades/wager-history', 'Get Regrades Wager History - v1'], ['GET /v1/bets/betting-status', 'Get Betting Status'], ]); const normalizePath = (path) => { if (!path || path === '/') { return '/'; } return path.endsWith('/') ? path.slice(0, -1) : path; }; const stableStringify = (value) => { if (Array.isArray(value)) { return `[${value.map(stableStringify).join(',')}]`; } if (value && typeof value === 'object') { return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`; } return JSON.stringify(value); }; const getCacheKey = (method, targetPath, query) => `${method} ${targetPath} ${stableStringify(query)}`; const checkRateLimit = (targetPath) => { const ttl = getRateLimitTtls.get(targetPath); if (!ttl) { return null; } const lastRequestAt = lastRequests.get(targetPath) ?? 0; const retryAfterMs = lastRequestAt + ttl - Date.now(); if (retryAfterMs > 0) { return retryAfterMs; } lastRequests.set(targetPath, Date.now()); return null; }; const getCachedPinnacleData = async (targetPath, query) => { if (targetPath === '/v2/currencies') { const { data, updatedAt } = await getCurrenciesInfo(); return { cacheStatus: 'GLOBAL_DATA', data, updatedAt }; } const ttl = getCacheTtls.get(targetPath); if (!ttl) { return { cacheStatus: 'BYPASS', data: await pinnacleGet(targetPath, query) }; } const cacheKey = getCacheKey('GET', targetPath, query); const cached = responseCache.get(cacheKey); if (cached && Date.now() - cached.createdAt < ttl) { return { cacheStatus: 'HIT', data: cached.data }; } if (inFlightRequests.has(cacheKey)) { return { cacheStatus: 'INFLIGHT', data: await inFlightRequests.get(cacheKey) }; } const request = pinnacleGet(targetPath, query) .then(data => { responseCache.set(cacheKey, { createdAt: Date.now(), data }); return data; }) .finally(() => { inFlightRequests.delete(cacheKey); }); inFlightRequests.set(cacheKey, request); return { cacheStatus: cached ? 'STALE_REFRESH' : 'MISS', data: await request }; }; const getEventIdsFromRequest = (req) => { const source = req.method === 'GET' ? req.query : req.body; return source?.ids ?? source?.id ?? source?.eventIds ?? source?.eventId; }; router.use(async (req, res) => { const method = req.method.toUpperCase(); const targetPath = normalizePath(req.path); if (targetPath === '/events' && (method === 'GET' || method === 'POST')) { const ids = getEventIdsFromRequest(req); return res.status(200).json(getEventsByIds(ids)); } const endpointKey = `${method} ${targetPath}`; if (!allowedEndpoints.has(endpointKey)) { return res.status(404).json({ statusCode: 404, code: -1, message: 'Unsupported Pinnacle API endpoint', data: { method, path: targetPath }, }); } try { if (method === 'GET') { const retryAfterMs = checkRateLimit(targetPath); if (retryAfterMs) { res.set('Retry-After', String(Math.ceil(retryAfterMs / SECOND))); return res.status(429).json({ statusCode: 429, code: -1, message: 'Pinnacle API endpoint is rate limited, please retry later', data: { path: targetPath, retryAfterMs, }, }); } } if (method === 'GET') { const data = await getCachedPinnacleData(targetPath, req.query); res.set('X-Pinnacle-Proxy-Cache', data.cacheStatus); if (data.updatedAt) { res.set('X-Pinnacle-Proxy-Updated-At', String(data.updatedAt)); } return res.status(200).json(data.data); } const data = await pinnaclePost(targetPath, req.body); return res.status(200).json(data); } catch (err) { const status = err.response?.status ?? err.status ?? err.cause ?? 500; const data = err.response?.data ?? err.data; const message = data?.message ?? err.message ?? 'Pinnacle API request failed'; return res.status(status).json(data ?? { statusCode: status, code: -1, message, }); } }); export default router;