Session 20: Provider intelligence — quota tracker, gateway with fallback cascade, admin quota dashboard (1476 tests)
This commit is contained in:
+49
-13
@@ -1,6 +1,12 @@
|
||||
const axios = require('axios');
|
||||
const { getRedisClient } = require('../utils/redis');
|
||||
const { normalizeProps, extractSpreads, MARKET_MAP } = require('../utils/oddsNormalizer');
|
||||
// Session 20 — every odds-api hit now flows through the gateway so
|
||||
// the quota counter advances and the fallback chain (oddspapi,
|
||||
// parlayapi) can take over when we approach the monthly cap. The
|
||||
// gateway is intentionally light-weight on the hot path — it adds
|
||||
// one Redis GET + SET per call (degrades open if Redis is down).
|
||||
const gateway = require('./providerGateway');
|
||||
|
||||
const ODDS_API_BASE = 'https://api.the-odds-api.com/v4/sports';
|
||||
const CACHE_TTL = 900; // 15 minutes in seconds
|
||||
@@ -137,6 +143,19 @@ async function getQuotaRemaining(redis) {
|
||||
async function updateQuota(redis, headers) {
|
||||
const remaining = headers['x-requests-remaining'];
|
||||
const used = headers['x-requests-used'];
|
||||
// Session 20 — sync the per-provider tracker from the same
|
||||
// headers. The gateway's syncHeadersFrom already does this on
|
||||
// each call; doing it here too is belt-and-suspenders for any
|
||||
// call path that bypassed the gateway. Lazy-required so the
|
||||
// tests that don't mock providerGateway don't crash on load.
|
||||
try {
|
||||
const quotaTracker = require('./quotaTracker');
|
||||
await quotaTracker.syncFromHeaders('odds-api', headers);
|
||||
} catch (e) {
|
||||
// Tracker failure must never break odds delivery — it's a
|
||||
// signal, not a dependency.
|
||||
console.warn('[oddsService] quotaTracker sync failed:', e.message);
|
||||
}
|
||||
if (remaining != null) {
|
||||
const key = getQuotaKey();
|
||||
await redis.hset(key, 'remaining', String(remaining), 'used', String(used || 0), 'last_checked', new Date().toISOString());
|
||||
@@ -150,10 +169,19 @@ async function updateQuota(redis, headers) {
|
||||
|
||||
async function fetchEventsFromApi(sportKey, apiKey) {
|
||||
const url = `${ODDS_API_BASE}/${sportKey}/events`;
|
||||
const response = await axios.get(url, {
|
||||
params: { apiKey },
|
||||
timeout: 10000,
|
||||
});
|
||||
const response = await gateway.fetch(
|
||||
'odds-api',
|
||||
() => axios.get(url, { params: { apiKey }, timeout: 10000 }),
|
||||
{
|
||||
capability: 'odds',
|
||||
// Best-effort sport tag for the fallback chain. The events
|
||||
// endpoint isn't sport-scoped on the fallback providers, but
|
||||
// passing it through lets the registry filter to relevant
|
||||
// candidates.
|
||||
sport: sportKey.replace(/^.*?_/, ''),
|
||||
syncHeadersFrom: (r) => r && r.headers,
|
||||
},
|
||||
);
|
||||
return { data: response.data, headers: response.headers };
|
||||
}
|
||||
|
||||
@@ -163,16 +191,24 @@ async function fetchEventsFromApi(sportKey, apiKey) {
|
||||
// market set, which is what every legacy caller assumed.
|
||||
async function fetchEventOddsFromApi(sportKey, eventId, apiKey, sport) {
|
||||
const url = `${ODDS_API_BASE}/${sportKey}/events/${eventId}/odds`;
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
apiKey,
|
||||
regions: 'us',
|
||||
markets: getMarketsForSport(sport),
|
||||
bookmakers: BOOKMAKERS,
|
||||
oddsFormat: 'american',
|
||||
const response = await gateway.fetch(
|
||||
'odds-api',
|
||||
() => axios.get(url, {
|
||||
params: {
|
||||
apiKey,
|
||||
regions: 'us',
|
||||
markets: getMarketsForSport(sport),
|
||||
bookmakers: BOOKMAKERS,
|
||||
oddsFormat: 'american',
|
||||
},
|
||||
timeout: 10000,
|
||||
}),
|
||||
{
|
||||
capability: 'odds',
|
||||
sport,
|
||||
syncHeadersFrom: (r) => r && r.headers,
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
);
|
||||
return { data: response.data, headers: response.headers };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user