chore: initial commit for phase07
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Compute the next run time for a scheduled task.
|
||||
*/
|
||||
export function computeNextRun(
|
||||
scheduleType: string,
|
||||
scheduleData: Record<string, unknown>,
|
||||
): Date {
|
||||
const now = new Date();
|
||||
|
||||
switch (scheduleType) {
|
||||
case 'interval': {
|
||||
const minutes = Number(scheduleData.minutes) || 60;
|
||||
return new Date(now.getTime() + minutes * 60_000);
|
||||
}
|
||||
|
||||
case 'daily': {
|
||||
const hour = Number(scheduleData.hour ?? 0);
|
||||
const minute = Number(scheduleData.minute ?? 0);
|
||||
const next = new Date(now);
|
||||
next.setHours(hour, minute, 0, 0);
|
||||
if (next <= now) next.setDate(next.getDate() + 1);
|
||||
return next;
|
||||
}
|
||||
|
||||
case 'weekly': {
|
||||
const dayOfWeek = Number(scheduleData.dayOfWeek ?? 0); // 0=Sunday
|
||||
const hour = Number(scheduleData.hour ?? 0);
|
||||
const minute = Number(scheduleData.minute ?? 0);
|
||||
const next = new Date(now);
|
||||
next.setHours(hour, minute, 0, 0);
|
||||
const currentDay = next.getDay();
|
||||
let daysAhead = dayOfWeek - currentDay;
|
||||
if (daysAhead < 0 || (daysAhead === 0 && next <= now)) {
|
||||
daysAhead += 7;
|
||||
}
|
||||
next.setDate(next.getDate() + daysAhead);
|
||||
return next;
|
||||
}
|
||||
|
||||
case 'cron': {
|
||||
// Simple cron parser for: minute hour dayOfMonth month dayOfWeek
|
||||
const expression = String(scheduleData.expression || '0 * * * *');
|
||||
return parseCronNextRun(expression, now);
|
||||
}
|
||||
|
||||
default:
|
||||
return new Date(now.getTime() + 3600_000); // fallback: 1 hour
|
||||
}
|
||||
}
|
||||
|
||||
function parseCronNextRun(expression: string, from: Date): Date {
|
||||
const parts = expression.trim().split(/\s+/);
|
||||
const cronMinute = parts[0] ?? '*';
|
||||
const cronHour = parts[1] ?? '*';
|
||||
const cronDom = parts[2] ?? '*';
|
||||
const cronMonth = parts[3] ?? '*';
|
||||
const cronDow = parts[4] ?? '*';
|
||||
|
||||
// Brute force: check next 1440 minutes (24 hours)
|
||||
const candidate = new Date(from);
|
||||
candidate.setSeconds(0, 0);
|
||||
candidate.setMinutes(candidate.getMinutes() + 1);
|
||||
|
||||
for (let i = 0; i < 1440 * 31; i++) {
|
||||
if (
|
||||
matchesCronField(cronMinute, candidate.getMinutes()) &&
|
||||
matchesCronField(cronHour, candidate.getHours()) &&
|
||||
matchesCronField(cronDom, candidate.getDate()) &&
|
||||
matchesCronField(cronMonth, candidate.getMonth() + 1) &&
|
||||
matchesCronField(cronDow, candidate.getDay())
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
candidate.setMinutes(candidate.getMinutes() + 1);
|
||||
}
|
||||
|
||||
// Fallback if no match found
|
||||
return new Date(from.getTime() + 3600_000);
|
||||
}
|
||||
|
||||
function matchesCronField(field: string, value: number): boolean {
|
||||
if (field === '*') return true;
|
||||
|
||||
// Handle step values: */5
|
||||
if (field.startsWith('*/')) {
|
||||
const step = parseInt(field.slice(2), 10);
|
||||
return step > 0 && value % step === 0;
|
||||
}
|
||||
|
||||
// Handle ranges: 1-5
|
||||
if (field.includes('-')) {
|
||||
const [min, max] = field.split('-').map(Number);
|
||||
return min !== undefined && max !== undefined && value >= min && value <= max;
|
||||
}
|
||||
|
||||
// Handle lists: 1,3,5
|
||||
if (field.includes(',')) {
|
||||
return field.split(',').map(Number).includes(value);
|
||||
}
|
||||
|
||||
// Exact match
|
||||
return parseInt(field, 10) === value;
|
||||
}
|
||||
Reference in New Issue
Block a user