diff --git a/.env.example b/.env.example index 0cf3d13..875adf9 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,8 @@ ORDER_CUTOFF=15:00 ORDER_ASK_UNTIL=15:30 ORDER_REMIND_MINUTES=10 ORDER_EXPIRE_MINUTES=30 + +# Result PDF fetching (IST): check from START until a draw is stored, give up at END, retry every N minutes +RESULTS_WINDOW_START=16:00 +RESULTS_WINDOW_END=19:00 +RESULTS_RETRY_MINUTES=10 diff --git a/src/config.test.ts b/src/config.test.ts index 704fc39..80cf628 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -21,6 +21,30 @@ describe('loadConfig', () => { expect(c.dbPath).toBe('/tmp/x.db'); }); + describe('results settings', () => { + const base = { SRS_SECRET: 'a'.repeat(20) }; + + it('defaults to 16:00-19:00, retry 10', () => { + expect(loadConfig(base).results).toEqual({ windowStartMin: 960, windowEndMin: 1140, retryMin: 10 }); + }); + + it('reads overrides', () => { + const c = loadConfig({ + ...base, + RESULTS_WINDOW_START: '16:30', + RESULTS_WINDOW_END: '20:00', + RESULTS_RETRY_MINUTES: '5', + }); + expect(c.results).toEqual({ windowStartMin: 990, windowEndMin: 1200, retryMin: 5 }); + }); + + it('rejects bad format, bad range and bad retry', () => { + expect(() => loadConfig({ ...base, RESULTS_WINDOW_START: '4pm' })).toThrow(/HH:MM/); + expect(() => loadConfig({ ...base, RESULTS_WINDOW_END: '16:00' })).toThrow(/later than RESULTS_WINDOW_START/); + expect(() => loadConfig({ ...base, RESULTS_RETRY_MINUTES: '0' })).toThrow(/RESULTS_RETRY_MINUTES/); + }); + }); + describe('order settings', () => { const base = { SRS_SECRET: 'a'.repeat(20) }; diff --git a/src/config.ts b/src/config.ts index 55103b7..f4e836c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,7 +21,14 @@ const schema = z.object({ ORDER_ASK_UNTIL: hhmm('15:30'), ORDER_REMIND_MINUTES: z.coerce.number().int().min(1).max(1440).default(10), ORDER_EXPIRE_MINUTES: z.coerce.number().int().min(1).max(1440).default(30), + RESULTS_WINDOW_START: hhmm('16:00'), + RESULTS_WINDOW_END: hhmm('19:00'), + RESULTS_RETRY_MINUTES: z.coerce.number().int().min(1).max(120).default(10), }) + .refine((e) => toMinutes(e.RESULTS_WINDOW_END) > toMinutes(e.RESULTS_WINDOW_START), { + path: ['RESULTS_WINDOW_END'], + message: 'must be later than RESULTS_WINDOW_START', + }) .refine((e) => toMinutes(e.ORDER_ASK_UNTIL) > toMinutes(e.ORDER_CUTOFF), { path: ['ORDER_ASK_UNTIL'], message: 'must be later than ORDER_CUTOFF', @@ -38,6 +45,12 @@ export interface OrderSettings { expireMin: number; } +export interface ResultsSettings { + windowStartMin: number; + windowEndMin: number; + retryMin: number; +} + export interface Config { port: number; host: string; @@ -47,6 +60,7 @@ export interface Config { bridgeUrl: string; defaultCountryCode: string; orders: OrderSettings; + results: ResultsSettings; } export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { @@ -75,5 +89,10 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { remindMin: e.ORDER_REMIND_MINUTES, expireMin: e.ORDER_EXPIRE_MINUTES, }, + results: { + windowStartMin: toMinutes(e.RESULTS_WINDOW_START), + windowEndMin: toMinutes(e.RESULTS_WINDOW_END), + retryMin: e.RESULTS_RETRY_MINUTES, + }, }; } diff --git a/src/results/scheduler.test.ts b/src/results/scheduler.test.ts index 288b06d..d6d89c0 100644 --- a/src/results/scheduler.test.ts +++ b/src/results/scheduler.test.ts @@ -60,7 +60,34 @@ describe('results scheduler', () => { await x.s.tick(); await x.s.tick(); expect(x.calls()).toBe(0); - expect(x.logs.filter((l) => String(l.msg).includes('by 7 PM'))).toHaveLength(1); + expect(x.logs.filter((l) => String(l.msg).includes('by end of window'))).toHaveLength(1); + }); + + it('honours a custom window and retry interval', async () => { + const db = makeDb(); + let calls = 0; + let clock = z('09:59'); // 15:29 IST + const s = createResultsScheduler({ + db, + log: () => {}, + now: () => clock, + run: async () => { + calls++; + return []; + }, + windowStartMin: 15 * 60 + 30, + windowEndMin: 15 * 60 + 50, + retryMin: 5, + }); + await s.tick(); + expect(calls).toBe(0); + clock = z('10:00'); // 15:30 + await s.tick(); + clock = z('10:05'); + await s.tick(); + clock = z('10:20'); // 15:50, window over + await s.tick(); + expect(calls).toBe(2); }); it('logs a held draw and survives a failing check', async () => { diff --git a/src/results/scheduler.ts b/src/results/scheduler.ts index c48e1ea..710a324 100644 --- a/src/results/scheduler.ts +++ b/src/results/scheduler.ts @@ -3,13 +3,12 @@ import { runResultsJob, type DrawReport } from './job'; import { istMinutes } from '../services/ist'; import { todayIst } from './portal'; -const WINDOW_START_MIN = 16 * 60; // 4 PM IST -const WINDOW_END_MIN = 19 * 60; // 7 PM IST -const EVERY_MS = 10 * 60_000; - export interface SchedulerOptions { db: Db; log: (entry: Record) => void; + windowStartMin?: number; + windowEndMin?: number; + retryMin?: number; now?: () => Date; run?: (db: Db) => Promise; } @@ -19,6 +18,9 @@ function todaysDrawReady(db: Db, today: string): boolean { } export function createResultsScheduler(opts: SchedulerOptions) { + const WINDOW_START_MIN = opts.windowStartMin ?? 16 * 60; + const WINDOW_END_MIN = opts.windowEndMin ?? 19 * 60; + const EVERY_MS = (opts.retryMin ?? 10) * 60_000; const now = opts.now ?? (() => new Date()); const run = opts.run ?? ((db: Db) => runResultsJob({ db })); let lastRunAt = 0; @@ -33,13 +35,13 @@ export function createResultsScheduler(opts: SchedulerOptions) { if (minutes >= WINDOW_END_MIN) { if (loggedGiveUpFor !== today && !todaysDrawReady(opts.db, today)) { loggedGiveUpFor = today; - opts.log({ level: 'warn', msg: 'results: no ready draw for today by 7 PM', date: today }); + opts.log({ level: 'warn', msg: 'results: no ready draw for today by end of window', date: today }); } return; } if (minutes < WINDOW_START_MIN || running) return; if (todaysDrawReady(opts.db, today)) return; - if (t.getTime() - lastRunAt < EVERY_MS - 30_000) return; + if (t.getTime() - lastRunAt < Math.max(0, EVERY_MS - 30_000)) return; running = true; lastRunAt = t.getTime(); diff --git a/src/server.ts b/src/server.ts index 2342678..b5f5a66 100644 --- a/src/server.ts +++ b/src/server.ts @@ -22,7 +22,11 @@ async function main(): Promise { const app = createApp({ db, config, bridge, sendQueue }, { logger: true }); log = (e) => app.log.info(e); - const stopScheduler = createResultsScheduler({ db, log: (e) => app.log.info(e) }).start(); + const stopScheduler = createResultsScheduler({ + db, + log: (e) => app.log.info(e), + ...config.results, + }).start(); const stopSweeper = createConversationSweeper({ db, settings: config.orders, diff --git a/src/testing/config.ts b/src/testing/config.ts index 548c590..4f40a3a 100644 --- a/src/testing/config.ts +++ b/src/testing/config.ts @@ -10,6 +10,7 @@ export function testConfig(overrides: Partial = {}): Config { bridgeUrl: 'http://127.0.0.1:3000', defaultCountryCode: '91', orders: { cutoffMin: 15 * 60, askUntilMin: 15 * 60 + 30, remindMin: 10, expireMin: 30 }, + results: { windowStartMin: 16 * 60, windowEndMin: 19 * 60, retryMin: 10 }, ...overrides, }; }