'use strict'; /** * Per-sport snapshot cadence (Job 1) — the config that replaces baseball's * one-size-fits-all rhythm. * * WHY THIS EXISTS * The snapshot cron fired every ACTIVE sport at all five MLB slots * (14/19/22/1/3 UTC). But sports post their lines on entirely different * clocks, so that inheritance was wasteful in two directions: * - WNBA props aren't posted at 14:00 UTC (10am ET) → that slot always * graded 0 for WNBA (the "wnba:0" the audit flagged). Wasted work. * - Soccer odds come from the odds-api key (500/MONTH, scarce). Running it * at five slots/day is a third of the monthly budget for 1–2 matches. * * THE RULE: high frequency only when a sport's lines actually post/move, * near-zero otherwise. This is a SCHEDULING fix, not a volume one — "never a * gap" must never become "pull constantly", because burning the odds-api * quota by noon means nothing all afternoon (strictly worse than a gap). * * QUOTA HEADROOM (mapped from reality, 2026-07): * - PropLine (mlb/nba/wnba props): 3 keys × 3,000/day = 9,000/day. Abundant. * Five slots × a few sports × intraday is a rounding error against that. * - The Odds API (soccer props + futures): 500/MONTH. Every soccer slot is * ~1 credit. So soccer gets the fewest, best-timed slots and NO intraday * refresh (a 20-min soccer refresh would burn ~36 credits/day = the whole * month in ~2 weeks). * * DESIGN CONSTRAINT (low blast radius): every sport's hours are a SUBSET of the * scheduler's firing grid (snapshotScheduler.HOURS_UTC, default 14/19/22/1/3). * The scheduler still fires at those hours; this config just decides WHICH * sports run at each one. Keeping the union inside the grid means the * missed-cron watchdog (which references MLB) needs no change. If a sport ever * genuinely needs an hour outside the grid, add it to HOURS_UTC too AND scope * the overdue watchdog to MLB's hours (see snapshotScheduler). * * POSTING RHYTHMS (why each sport gets the hours it does): * - mlb — games + props all day; the full grid is correct. Keep 14/19/22/1/3. * - nba — off-season now (self-skips empty on PropLine, ~0 cost); in-season * posts morning→afternoon ET. The full grid fits both. Keep. * - wnba — games tip evening ET; props post ~afternoon ET. 19 UTC (3pm ET) * catches early games, 22 UTC (6pm ET) the bulk, 1 UTC (9pm ET) the * west-coast lock. 14 UTC (10am ET) and 3 UTC (11pm ET, games done) * are dropped — that's the wasted "wnba:0" slot removed. * - soccer— World Cup live now (final Jul 19), club leagues resume Aug. Lines * post well ahead and barely move; two well-placed odds-api reads * (14 UTC morning research, 19 UTC pre-afternoon-match lock) cover a * match day for ~2 credits. NO intraday (protects the 500/mo key). * * config-over-constants: to retune a sport, edit ONLY this table. */ const SPORT_CADENCE = Object.freeze({ mlb: { hours: [14, 19, 22, 1, 3], intraday: true, source: 'propline' }, nba: { hours: [14, 19, 22, 1, 3], intraday: true, source: 'propline' }, wnba: { hours: [19, 22, 1], intraday: true, source: 'propline' }, soccer: { hours: [14, 19], intraday: false, source: 'odds-api' }, }); const ALL_SPORTS = Object.freeze(Object.keys(SPORT_CADENCE)); // The union of every sport's snapshot hours — the set the scheduler must fire // at so no sport's slot is missed. Kept for the cross-check that this union is // a subset of the scheduler grid. const ALL_HOURS = Object.freeze( [...new Set(ALL_SPORTS.flatMap((s) => SPORT_CADENCE[s].hours))].sort((a, b) => a - b), ); /** * sportsForHour — which sports are scheduled to snapshot at this UTC hour. * @param {number} hour UTC hour 0..23 * @param {string[]} [sports] restrict to this candidate set (defaults to all) * @returns {string[]} */ function sportsForHour(hour, sports = ALL_SPORTS) { const h = Number(hour); return sports.filter((s) => SPORT_CADENCE[s] && SPORT_CADENCE[s].hours.includes(h)); } /** intradaySports — sports that opt into the 20-min intraday line refresh. * Soccer is excluded on purpose (odds-api quota). */ function intradaySports(sports = ALL_SPORTS) { return sports.filter((s) => SPORT_CADENCE[s] && SPORT_CADENCE[s].intraday); } /** hoursFor — a single sport's snapshot hours (empty array if unknown). */ function hoursFor(sport) { const c = SPORT_CADENCE[String(sport || '').toLowerCase()]; return c ? c.hours.slice() : []; } module.exports = { SPORT_CADENCE, ALL_SPORTS, ALL_HOURS, sportsForHour, intradaySports, hoursFor, };