Files
vyndr/scripts/odds-prefetch.js
T

118 lines
4.2 KiB
JavaScript

'use strict';
/**
* Odds-cache prewarmer (Session 22).
*
* On the free 500-credit/month odds-api tier this script is a
* negative-EV operation: a single fetchAllOdds() call costs
* 1 (events lookup) + N (per-event odds) credits, and a 1h-TTL
* warmer for one sport would spend ~24 credits/day, blowing the
* monthly budget across multiple sports. It only makes sense
* once the account is on a higher tier.
*
* Therefore: gated by `ODDS_PREWARM=1`. The script REFUSES to run
* when the flag is unset — operators opt in explicitly per
* deployment. The flag check is the first thing main() does so
* accidental cron invocations no-op before any provider call.
*
* Usage (CLI):
* node scripts/odds-prefetch.js --sports=nba,mlb,wnba
* ODDS_PREWARM=1 node scripts/odds-prefetch.js --dry-run
*
* Returns a summary object with the per-sport outcome and the
* total cost in odds-api credits (read from the tracker delta).
* Designed to be wrapped by an internal HTTP endpoint or a cron;
* not auto-mounted to either.
*/
function parseArgs(argv) {
const flags = { sports: ['nba', 'wnba', 'mlb'], dryRun: false };
for (const a of argv.slice(2)) {
if (a === '--dry-run') flags.dryRun = true;
else if (a.startsWith('--sports=')) {
const list = a.slice('--sports='.length).split(',').map((s) => s.trim()).filter(Boolean);
if (list.length) flags.sports = list;
}
}
return flags;
}
async function main(argv = process.argv) {
const enabled = process.env.ODDS_PREWARM === '1';
const args = parseArgs(argv);
const summary = {
enabled,
dryRun: args.dryRun,
sports: {},
creditsSpent: 0,
startedAt: new Date().toISOString(),
};
if (!enabled) {
console.warn('[odds-prefetch] disabled — set ODDS_PREWARM=1 to enable');
summary.skipped = 'not_enabled';
return summary;
}
// Lazy-require so the script can be loaded for inspection (or
// imported by tests) without dragging in axios + redis just to
// hit the env-flag short-circuit above.
const { getOdds } = require('../src/services/oddsService');
const quotaTracker = require('../src/services/quotaTracker');
const before = await quotaTracker.getQuotaStatus('odds-api');
console.log(`[odds-prefetch] starting — sports=${args.sports.join(',')} dry_run=${args.dryRun} quota_before=${before.used}/${before.limit}`);
for (const sport of args.sports) {
if (args.dryRun) {
summary.sports[sport] = { skipped: 'dry_run' };
console.log(`[odds-prefetch] ${sport}: dry-run`);
continue;
}
// Pre-flight: if quota is already blocked, bail out for the
// remaining sports too — no point spending the next sport's
// credits on a check that the tracker already failed.
const status = await quotaTracker.getQuotaStatus('odds-api');
if (!status.allowed) {
summary.sports[sport] = { skipped: 'quota_blocked', pct: status.pct };
console.warn(`[odds-prefetch] ${sport}: skipped — quota ${(status.pct * 100).toFixed(0)}%`);
continue;
}
try {
const result = await getOdds(sport);
const propCount = Array.isArray(result.props) ? result.props.length : 0;
summary.sports[sport] = {
source: result.source || 'live',
props: propCount,
quota_remaining: result.quota_remaining,
};
console.log(`[odds-prefetch] ${sport}: ${result.source} (${propCount} props, quota=${result.quota_remaining})`);
} catch (err) {
summary.sports[sport] = { error: err && err.message ? err.message : String(err) };
console.warn(`[odds-prefetch] ${sport}: failed — ${err && err.message}`);
}
}
const after = await quotaTracker.getQuotaStatus('odds-api');
summary.creditsSpent = Math.max(0, after.used - before.used);
summary.quota = { before: before.used, after: after.used, limit: after.limit, pct: after.pct };
console.log(`[odds-prefetch] done — credits_spent=${summary.creditsSpent} quota=${after.used}/${after.limit} (${(after.pct * 100).toFixed(0)}%)`);
return summary;
}
if (require.main === module) {
main().then((s) => {
process.exit(s.skipped === 'not_enabled' ? 2 : 0);
}).catch((err) => {
console.error('[odds-prefetch] fatal:', err);
process.exit(1);
});
}
module.exports = {
main,
__internals: { parseArgs },
};