| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- 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;
|