Session 32: Grades pipeline + NFL/NHL wiring + rate limiting + audit cleanup (1718 tests)
- gradeSlateService writes grades:{sport} cache (closes content pipeline →
dataLevel full); fire-and-forget from oddsService.recordDownstream, gated
by shouldGradeSlate (off in test, GRADE_SLATE_ON_FETCH override)
- NFL/NHL wired: oddsService SPORT_KEYS/SPORT_MARKETS (correct the-odds-api
keys americanfootball_nfl/icehockey_nhl), proplineAdapter MARKETS, NHL
MARKET_MAP keys to avoid silent-zero
- rate limiting mounted on 8 public cached routers (odds/parlay 30/min,
rest 60/min)
- jsonlLogger writes to temp under test (no more dirtied tracked artifact);
5MB pipeline test given 20s timeout
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,12 @@ const SPORT_KEYS = {
|
||||
// array with a friendly message in that case.
|
||||
wnba: 'basketball_wnba',
|
||||
mlb: 'baseball_mlb',
|
||||
// Session 32 — NFL + NHL. odds-api keys per the-odds-api sports list.
|
||||
// Off-season returns an empty events array; the route layer surfaces an
|
||||
// empty slate (never a crash). NFL props normalize through the MARKET_MAP
|
||||
// keys added in Session 31; NHL keys were added alongside this wiring.
|
||||
nfl: 'americanfootball_nfl',
|
||||
nhl: 'icehockey_nhl',
|
||||
// Soccer (Session 7j) — odds-api sport keys verified against
|
||||
// https://the-odds-api.com/sports-odds-data/sports-apis.html
|
||||
soccer_wc: 'soccer_fifa_world_cup',
|
||||
@@ -106,6 +112,24 @@ const MLB_MARKETS = [
|
||||
'pitcher_strikeouts',
|
||||
'pitcher_outs',
|
||||
];
|
||||
// Session 32 — NFL + NHL market lists. NFL keys mirror the MARKET_MAP
|
||||
// entries added Session 31 (both _yds and _yards spellings normalize). NHL
|
||||
// keys were added to MARKET_MAP alongside this wiring so they don't
|
||||
// silently normalize to zero in-season.
|
||||
const NFL_MARKETS = [
|
||||
'player_pass_yds',
|
||||
'player_pass_tds',
|
||||
'player_rush_yds',
|
||||
'player_reception_yds',
|
||||
'player_receptions',
|
||||
'player_anytime_td',
|
||||
];
|
||||
const NHL_MARKETS = [
|
||||
'player_goals',
|
||||
'player_shots_on_goal',
|
||||
'player_assists',
|
||||
'goalie_saves',
|
||||
];
|
||||
const SOCCER_MARKETS = [
|
||||
'player_goals',
|
||||
'player_shots_on_target',
|
||||
@@ -129,6 +153,8 @@ const SPORT_MARKETS = Object.freeze({
|
||||
nba: buildMarketString(NBA_MARKETS),
|
||||
wnba: buildMarketString(WNBA_MARKETS),
|
||||
mlb: buildMarketString(MLB_MARKETS),
|
||||
nfl: buildMarketString(NFL_MARKETS),
|
||||
nhl: buildMarketString(NHL_MARKETS),
|
||||
ncaab: buildMarketString(NBA_MARKETS), // NCAAB markets mirror NBA
|
||||
// Every soccer league code shares the same market set.
|
||||
...Object.fromEntries(
|
||||
@@ -283,7 +309,7 @@ function parseQuota(headers) {
|
||||
// Best-effort post-fetch processing shared by both providers (PropLine +
|
||||
// odds-api): line movement, scratch cascade, and rolling line snapshots.
|
||||
// Never throws — a failure here must not break the odds response.
|
||||
async function recordDownstream(sport, props) {
|
||||
async function recordDownstream(sport, props, provider = 'odds-api') {
|
||||
let movements = [];
|
||||
let scratchedPlayers = [];
|
||||
try {
|
||||
@@ -298,9 +324,34 @@ async function recordDownstream(sport, props) {
|
||||
} catch (e) {
|
||||
console.warn('[VYNDR] Movement/cascade detection error:', e.message);
|
||||
}
|
||||
|
||||
// Session 32 — grade the slate into the `grades:{sport}` cache that
|
||||
// contentTemplateService reads, closing the content pipeline. Fire-and-
|
||||
// forget: grading fans out to feature computation and must NOT hold the
|
||||
// odds HTTP response — content endpoints read the grades cache later and
|
||||
// independently. Errors are self-contained inside the service.
|
||||
if (shouldGradeSlate()) {
|
||||
require('./gradeSlateService')
|
||||
.gradeAndCacheSlate(sport, props, { source: provider })
|
||||
.catch((e) => console.warn('[VYNDR] slate grading error:', e.message));
|
||||
}
|
||||
|
||||
return { movements, scratchedPlayers };
|
||||
}
|
||||
|
||||
// Auto-grade the slate on a fresh odds fetch by default (closes the content
|
||||
// pipeline). Skipped under the test env so its background feature-computation
|
||||
// fan-out doesn't pollute call-count assertions in the odds integration
|
||||
// tests; `GRADE_SLATE_ON_FETCH` is the explicit operator override
|
||||
// ('1' forces on even in test, '0' is the production kill-switch if the
|
||||
// feature-compute cost ever needs to be shed).
|
||||
function shouldGradeSlate() {
|
||||
const flag = process.env.GRADE_SLATE_ON_FETCH;
|
||||
if (flag === '1') return true;
|
||||
if (flag === '0') return false;
|
||||
return process.env.NODE_ENV !== 'test';
|
||||
}
|
||||
|
||||
async function getOdds(sport) {
|
||||
const redis = getRedisClient();
|
||||
const apiKey = process.env.ODDS_API_KEY;
|
||||
@@ -335,7 +386,7 @@ async function getOdds(sport) {
|
||||
const now = new Date().toISOString();
|
||||
const cacheData = { updated_at: now, props: pl.props, spreads: pl.spreads || [], provider: 'propline' };
|
||||
await redis.set(cacheKey, JSON.stringify(cacheData), 'EX', CACHE_TTL);
|
||||
const { movements, scratchedPlayers } = await recordDownstream(sport, pl.props);
|
||||
const { movements, scratchedPlayers } = await recordDownstream(sport, pl.props, 'propline');
|
||||
return {
|
||||
sport,
|
||||
updated_at: now,
|
||||
@@ -389,7 +440,7 @@ async function getOdds(sport) {
|
||||
await redis.set(cacheKey, JSON.stringify(cacheData), 'EX', CACHE_TTL);
|
||||
|
||||
// Line movement + cascade + snapshots (best-effort; shared helper).
|
||||
const { movements, scratchedPlayers } = await recordDownstream(sport, props);
|
||||
const { movements, scratchedPlayers } = await recordDownstream(sport, props, 'odds-api');
|
||||
|
||||
return {
|
||||
sport,
|
||||
@@ -458,4 +509,6 @@ module.exports = {
|
||||
// Session 22 — exposed for tests that exercise env-driven TTL
|
||||
// resolution without re-loading the module.
|
||||
getConfiguredCacheTTL,
|
||||
// Session 32 — slate auto-grade gate (exposed for tests).
|
||||
shouldGradeSlate,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user