Session 21: All adapters through gateway, ntfy alerts, provider registry correction (1486 tests)
This commit is contained in:
@@ -30,6 +30,15 @@
|
||||
|
||||
const axios = require('axios');
|
||||
const { cacheGet, cacheSet } = require('../../utils/redis');
|
||||
// Session 21 — gateway wrap. The adapter still keeps its own
|
||||
// `apifootball:daily_count` counter (which has the SOFT_LIMIT=90
|
||||
// soft-stop logic), but every successful HTTP call now also
|
||||
// advances the canonical per-provider quota counter the admin
|
||||
// dashboard reads from. Two counters, one truth-source: the
|
||||
// gateway-tracker counter is the one the WARN/BLOCK thresholds
|
||||
// fire against; the local daily_count is the legacy
|
||||
// stale-while-revalidate trigger.
|
||||
const gateway = require('../providerGateway');
|
||||
|
||||
const BASE_URL = 'https://v3.football.api-sports.io';
|
||||
const HTTP_TIMEOUT_MS = 8_000;
|
||||
@@ -93,10 +102,14 @@ async function fetchWithCache(path, cacheKey, ttl) {
|
||||
|
||||
// 4. Network.
|
||||
try {
|
||||
const res = await axios.get(`${BASE_URL}${path}`, {
|
||||
headers: { 'x-apisports-key': process.env.API_FOOTBALL_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
});
|
||||
const res = await gateway.fetch(
|
||||
'api-football',
|
||||
() => axios.get(`${BASE_URL}${path}`, {
|
||||
headers: { 'x-apisports-key': process.env.API_FOOTBALL_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
}),
|
||||
{ capability: 'lineups', sport: 'soccer' },
|
||||
);
|
||||
await bumpDailyCount();
|
||||
const body = res.data;
|
||||
if (body && typeof body === 'object') {
|
||||
|
||||
@@ -34,6 +34,14 @@
|
||||
|
||||
const axios = require('axios');
|
||||
const { cacheGet, cacheSet } = require('../../utils/redis');
|
||||
// Session 21 — gateway wrap. The adapter's in-process token bucket
|
||||
// (8 req/min) is the FIRST line of defense (synchronous; no Redis
|
||||
// round-trip), and the gateway's per-minute counter is the
|
||||
// observability layer (cross-process, visible in /admin). The
|
||||
// bucket short-circuits before the gateway in burst scenarios, so
|
||||
// the counter only ticks up for calls that actually went over the
|
||||
// wire.
|
||||
const gateway = require('../providerGateway');
|
||||
|
||||
const BASE_URL = 'https://api.football-data.org/v4';
|
||||
const HTTP_TIMEOUT_MS = 8_000;
|
||||
@@ -103,10 +111,14 @@ async function fetchWithCache(path, cacheKey, ttl) {
|
||||
|
||||
// 4. Hit the network.
|
||||
try {
|
||||
const res = await axios.get(`${BASE_URL}${path}`, {
|
||||
headers: { 'X-Auth-Token': process.env.FOOTBALL_DATA_API_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
});
|
||||
const res = await gateway.fetch(
|
||||
'football-data',
|
||||
() => axios.get(`${BASE_URL}${path}`, {
|
||||
headers: { 'X-Auth-Token': process.env.FOOTBALL_DATA_API_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
}),
|
||||
{ capability: 'fixtures', sport: 'soccer' },
|
||||
);
|
||||
const body = res.data;
|
||||
if (body && typeof body === 'object') {
|
||||
// Write to BOTH the live and stale keys. Stale key has a much
|
||||
|
||||
@@ -15,6 +15,12 @@ const axios = require('axios');
|
||||
const { createLimiter, createCircuitBreaker, API_BUDGETS } = require('../../utils/rateLimiter');
|
||||
const { devig } = require('../../utils/odds');
|
||||
const { getSupabaseServiceClient } = require('../../utils/supabase');
|
||||
// Session 21 — gateway wrap. ODDSPAPI is the Pinnacle-close
|
||||
// provider, not a live-odds source. Each call hits ONCE per game
|
||||
// at tip-off, so traffic is bounded — the gateway counter mostly
|
||||
// gives the admin dashboard visibility rather than acting as
|
||||
// hot-path throttle (the in-process limiter does that).
|
||||
const gateway = require('../providerGateway');
|
||||
|
||||
const HTTP_TIMEOUT_MS = 10_000;
|
||||
const BASE_URL = process.env.ODDSPAPI_BASE_URL || 'https://api.oddspapi.io/v1';
|
||||
@@ -47,11 +53,15 @@ async function fetchPinnacleProp(sport, gameId, playerName, statType) {
|
||||
await limiter.waitForToken();
|
||||
try {
|
||||
return await breaker.call(async () => {
|
||||
const res = await axios.get(`${BASE_URL}/sports/${sportKey(sport)}/events/${gameId}/odds`, {
|
||||
params: { bookmaker: 'pinnacle', market: 'player_props' },
|
||||
headers: { 'X-Api-Key': process.env.ODDSPAPI_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
});
|
||||
const res = await gateway.fetch(
|
||||
'oddspapi',
|
||||
() => axios.get(`${BASE_URL}/sports/${sportKey(sport)}/events/${gameId}/odds`, {
|
||||
params: { bookmaker: 'pinnacle', market: 'player_props' },
|
||||
headers: { 'X-Api-Key': process.env.ODDSPAPI_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
}),
|
||||
{ capability: 'closing_lines', sport },
|
||||
);
|
||||
const props = res.data?.props || res.data?.data || [];
|
||||
return Array.isArray(props)
|
||||
? props.find(
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
const axios = require('axios');
|
||||
const { createLimiter, createCircuitBreaker } = require('../../utils/rateLimiter');
|
||||
const { cacheGet, cacheSet } = require('../../utils/redis');
|
||||
// Session 21 — gateway wrap. ParlayAPI is the historical-archive
|
||||
// source (1K credits/month). The in-process limiter caps to
|
||||
// 5 req/min so we can spread the monthly budget; the gateway
|
||||
// counter is what the admin dashboard reads against.
|
||||
const gateway = require('../providerGateway');
|
||||
|
||||
const SOURCE = 'parlayapi';
|
||||
const HTTP_TIMEOUT_MS = 10_000;
|
||||
@@ -61,12 +66,16 @@ async function fetchWithGuards(url, params, cacheKey) {
|
||||
await limiter.waitForToken();
|
||||
try {
|
||||
const data = await breaker.call(async () => {
|
||||
const res = await axios.get(url, {
|
||||
params,
|
||||
headers: { 'X-Api-Key': process.env.PARLAYAPI_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
validateStatus: (s) => (s >= 200 && s < 300) || s === 429,
|
||||
});
|
||||
const res = await gateway.fetch(
|
||||
'parlayapi',
|
||||
() => axios.get(url, {
|
||||
params,
|
||||
headers: { 'X-Api-Key': process.env.PARLAYAPI_KEY },
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
validateStatus: (s) => (s >= 200 && s < 300) || s === 429,
|
||||
}),
|
||||
{ capability: 'historical_props', sport: params && params.sport },
|
||||
);
|
||||
if (res.status === 429) {
|
||||
const err = new Error('parlayapi rate limited');
|
||||
err.code = 'PARLAYAPI_429';
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
|
||||
const axios = require('axios');
|
||||
const { cacheGet, cacheSet } = require('../../utils/redis');
|
||||
// Session 21 — gateway wrap; see tank01NbaAdapter for the rationale.
|
||||
// Same provider ID since both NBA and MLB endpoints share the
|
||||
// RAPID_API_KEY quota (1,000 req/month combined).
|
||||
const gateway = require('../providerGateway');
|
||||
|
||||
const HTTP_TIMEOUT_MS = 8_000;
|
||||
const DEFAULT_HOST = 'tank01-mlb-live-in-game-real-time-statistics.p.rapidapi.com';
|
||||
@@ -41,13 +45,17 @@ async function fetchWithCache(path, cacheKey, ttl) {
|
||||
|
||||
try {
|
||||
const host = getHost();
|
||||
const res = await axios.get(`https://${host}${path}`, {
|
||||
headers: {
|
||||
'x-rapidapi-key': process.env.RAPID_API_KEY,
|
||||
'x-rapidapi-host': host,
|
||||
},
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
});
|
||||
const res = await gateway.fetch(
|
||||
'tank01',
|
||||
() => axios.get(`https://${host}${path}`, {
|
||||
headers: {
|
||||
'x-rapidapi-key': process.env.RAPID_API_KEY,
|
||||
'x-rapidapi-host': host,
|
||||
},
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
}),
|
||||
{ capability: 'box_scores', sport: 'mlb' },
|
||||
);
|
||||
const body = res.data;
|
||||
if (body && typeof body === 'object') {
|
||||
await cacheSet(cacheKey, body, ttl);
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
|
||||
const axios = require('axios');
|
||||
const { cacheGet, cacheSet } = require('../../utils/redis');
|
||||
// Session 21 — every Tank01 hit flows through the provider gateway
|
||||
// so the monthly counter advances and the WARN/BLOCK thresholds
|
||||
// catch us before RapidAPI 429s. The cache layer below still
|
||||
// short-circuits before the gateway in the common path.
|
||||
const gateway = require('../providerGateway');
|
||||
|
||||
const HTTP_TIMEOUT_MS = 8_000;
|
||||
const DEFAULT_HOST = 'tank01-fantasy-stats.p.rapidapi.com';
|
||||
@@ -50,13 +55,17 @@ async function fetchWithCache(path, cacheKey, ttl) {
|
||||
|
||||
try {
|
||||
const host = getHost();
|
||||
const res = await axios.get(`https://${host}${path}`, {
|
||||
headers: {
|
||||
'x-rapidapi-key': process.env.RAPID_API_KEY,
|
||||
'x-rapidapi-host': host,
|
||||
},
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
});
|
||||
const res = await gateway.fetch(
|
||||
'tank01',
|
||||
() => axios.get(`https://${host}${path}`, {
|
||||
headers: {
|
||||
'x-rapidapi-key': process.env.RAPID_API_KEY,
|
||||
'x-rapidapi-host': host,
|
||||
},
|
||||
timeout: HTTP_TIMEOUT_MS,
|
||||
}),
|
||||
{ capability: 'box_scores', sport: 'nba' },
|
||||
);
|
||||
const body = res.data;
|
||||
if (body && typeof body === 'object') {
|
||||
await cacheSet(cacheKey, body, ttl);
|
||||
|
||||
Reference in New Issue
Block a user