chore: initial commit for phase07

This commit is contained in:
hibna
2026-02-22 00:25:39 +03:00
parent 5709d8bc10
commit 124e4f8921
16 changed files with 1973 additions and 23 deletions
+103
View File
@@ -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;
}