Session 21: All adapters through gateway, ntfy alerts, provider registry correction (1486 tests)

This commit is contained in:
Kev
2026-06-12 02:06:22 -04:00
parent 9b10bb4138
commit ea848e327e
14 changed files with 614 additions and 46 deletions
+17 -4
View File
@@ -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') {
+16 -4
View File
@@ -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 -5
View File
@@ -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(
+15 -6
View File
@@ -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 -7
View File
@@ -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);
+16 -7
View File
@@ -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);