Skip to content

Commit 39efeab

Browse files
authored
Merge pull request #35 from 100monkeys-ai/jules-fsl-scheduler-cron-fallback-3776530429110007294
2 parents 2379752 + b5c729f commit 39efeab

1 file changed

Lines changed: 88 additions & 3 deletions

File tree

fsl/jobs/src/scheduler.fx

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,92 @@ export const Scheduler = {
3434
}
3535

3636
function computeNextRun(expression: string): Date {
37-
// TODO: Parse cron expression and compute next run time
38-
// Using a Rust cron parser exposed via FFI
39-
return new Date(Date.now() + 60_000)
37+
// Parse cron expression and compute next run time
38+
// This uses a built-in TS cron parser fallback as the Rust FFI boundary
39+
// for deno_core ops is not yet fully initialized/exposed in the runtime context.
40+
41+
try {
42+
const parts = expression.trim().split(/\s+/);
43+
if (parts.length !== 5 && parts.length !== 6) {
44+
throw new Error('Invalid cron expression length');
45+
}
46+
47+
const now = new Date();
48+
// Start checking from the next minute
49+
let next = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes() + 1, 0, 0);
50+
51+
const matchField = (val: number, field: string): boolean => {
52+
if (field === '*') return true;
53+
if (field.includes(',')) {
54+
return field.split(',').some(part => matchField(val, part));
55+
}
56+
if (field.includes('/')) {
57+
const [range, step] = field.split('/');
58+
const stepNum = parseInt(step, 10);
59+
if (range === '*') return val % stepNum === 0;
60+
if (range.includes('-')) {
61+
const [start, end] = range.split('-');
62+
return val >= parseInt(start, 10) && val <= parseInt(end, 10) && (val - parseInt(start, 10)) % stepNum === 0;
63+
}
64+
return val % stepNum === 0;
65+
}
66+
if (field.includes('-')) {
67+
const [start, end] = field.split('-');
68+
return val >= parseInt(start, 10) && val <= parseInt(end, 10);
69+
}
70+
return val === parseInt(field, 10);
71+
};
72+
73+
const hasSec = parts.length === 6;
74+
const offset = hasSec ? 1 : 0;
75+
const minField = parts[0 + offset];
76+
const hourField = parts[1 + offset];
77+
const domField = parts[2 + offset];
78+
const monField = parts[3 + offset];
79+
const dowField = parts[4 + offset];
80+
81+
// Search forward up to 5 years
82+
for (let i = 0; i < 5 * 365 * 24 * 60; i++) {
83+
const monMatch = matchField(next.getMonth() + 1, monField);
84+
if (!monMatch) {
85+
next.setDate(1);
86+
next.setMonth(next.getMonth() + 1);
87+
next.setHours(0);
88+
next.setMinutes(0);
89+
continue;
90+
}
91+
92+
const domMatch = matchField(next.getDate(), domField);
93+
const dowMatch = matchField(next.getDay(), dowField);
94+
95+
const dayMatch = (domField === '*' || dowField === '*')
96+
? (domMatch && dowMatch)
97+
: (domMatch || dowMatch);
98+
99+
if (!dayMatch) {
100+
next.setDate(next.getDate() + 1);
101+
next.setHours(0);
102+
next.setMinutes(0);
103+
continue;
104+
}
105+
106+
const hourMatch = matchField(next.getHours(), hourField);
107+
if (!hourMatch) {
108+
next.setHours(next.getHours() + 1);
109+
next.setMinutes(0);
110+
continue;
111+
}
112+
113+
const minMatch = matchField(next.getMinutes(), minField);
114+
if (minMatch) {
115+
return next;
116+
}
117+
118+
next.setMinutes(next.getMinutes() + 1);
119+
}
120+
121+
return new Date(Date.now() + 60_000);
122+
} catch (err) {
123+
return new Date(Date.now() + 60_000);
124+
}
40125
}

0 commit comments

Comments
 (0)