import cron from 'node-cron' import { ASNSampler } from './sampler' import { ASPACollector } from './collector' import { AdoptionAggregator } from './aggregator' import { AdoptionForecaster } from './forecaster' import { ASPADatabaseClient } from './db-client' export class ASPAAdoptionScheduler { private task: cron.ScheduledTask | null = null private sampler = new ASNSampler() private collector = new ASPACollector() private aggregator = new AdoptionAggregator() private forecaster = new AdoptionForecaster() private dbClient = new ASPADatabaseClient() private isRunning = false /** * Start the daily ASPA adoption sampling job * Runs at 2 AM UTC every day (0 2 * * *) */ start(): void { if (this.task) { console.log('[ASPA Scheduler] Job already scheduled') return } this.task = cron.schedule('0 2 * * *', () => { if (!this.isRunning) { this.runDailyJob().catch((error) => { console.error('[ASPA Scheduler] Daily job failed:', error) }) } }) console.log('[ASPA Scheduler] Scheduled daily job at 2:00 AM UTC') } /** * Stop the scheduled job */ stop(): void { if (this.task) { this.task.stop() this.task = null console.log('[ASPA Scheduler] Job stopped') } } /** * Manually trigger the daily job (useful for testing) */ async runManual(): Promise { if (this.isRunning) { throw new Error('Job is already running') } await this.runDailyJob() } private async runDailyJob(): Promise { this.isRunning = true const startTime = Date.now() try { console.log('[ASPA Scheduler] Starting daily adoption snapshot') const sampleDate = new Date() sampleDate.setUTCHours(0, 0, 0, 0) // Step 1: Stratified sample of ASNs console.log('[ASPA Scheduler] Sampling ASNs...') const samplingResult = this.sampler.sample(750, { stratifiedByRegion: true, minASNsPerRegion: 100, }) console.log(`[ASPA Scheduler] Sampled ${samplingResult.totalSampled} ASNs`) // Step 2: Collect ASPA data for sampled ASNs console.log('[ASPA Scheduler] Collecting ASPA data...') const snapshots = await this.collector.collect(samplingResult.asns, { maxRetries: 3, timeoutMs: 5000, rateLimit: 100, }) console.log(`[ASPA Scheduler] Collected ASPA data for ${snapshots.length} ASNs`) // Step 3: Aggregate by region and IXP console.log('[ASPA Scheduler] Aggregating data by region and IXP...') const { overall, regions, ixps } = this.aggregator.aggregate(snapshots, { includeRegions: true, includeIXPs: true, }) console.log( `[ASPA Scheduler] Aggregation complete: ${overall.withASPA}/${overall.total} ASNs with ASPA (${overall.coverage}%)` ) // Step 4: Fetch historical data and forecast console.log('[ASPA Scheduler] Calculating forecast...') const history = await this.dbClient.getAdoptionHistory(90) const timeSeriesData = history .reverse() .map((h) => ({ date: h.sampleDate, coverage: h.coveragePercentage, })) const forecast = this.forecaster.forecast(timeSeriesData, { historyDays: 90, forecastMonths: 6, regressionAlpha: 0.05, }) console.log(`[ASPA Scheduler] Forecast: ${forecast.predictedCoverage6m}% coverage in 6 months (confidence: ${forecast.confidence})`) // Step 5: Store results in database console.log('[ASPA Scheduler] Storing results...') const topAdopters = snapshots .filter((s) => s.hasASPA) .sort((a, b) => b.providers.length - a.providers.length) .slice(0, 10) .map((s) => ({ asn: s.asn, name: `AS${s.asn}`, providers: s.providers.length, })) const metadata = { samplingMethod: 'stratified-regional', forecastData: forecast, dataCollectionTimeMs: Date.now() - startTime, } await this.dbClient.recordAdoptionSnapshot(sampleDate, overall.total, overall.withASPA, regions, topAdopters, metadata) await this.dbClient.recordRegionalBreakdown(sampleDate, regions) await this.dbClient.recordIXPBreakdown(sampleDate, ixps) console.log(`[ASPA Scheduler] Daily job completed in ${Date.now() - startTime}ms`) } catch (error) { console.error('[ASPA Scheduler] Job failed:', error instanceof Error ? error.message : String(error)) throw error } finally { this.isRunning = false } } isJobRunning(): boolean { return this.isRunning } isScheduled(): boolean { return this.task !== null } } export const aspaScheduler = new ASPAAdoptionScheduler()