Skip to content
71 linesCodeBlameRaw
1/**
2 * When routines run, and what a routine must have: pure, so it is tested
3 * on its own (routines.ts runs them).
4 */
5import type { NewRoutine, RoutineSchedule } from "@g1t/contracts";
6
7const EVERY = ["hour", "day", "weekday", "week"] as const;
8
9/** A schedule as given, checked; a message when it isn't one. */
10export function checkSchedule(input: unknown): { ok: true; value: RoutineSchedule } | { ok: false; message: string } {
11 if (!input || typeof input !== "object") return { ok: false, message: "Say when it runs." };
12 const s = input as Record<string, unknown>;
13 const every = EVERY.find((e) => e === s.every);
14 if (!every) return { ok: false, message: "A routine runs every hour, day, weekday or week." };
15 const int = (value: unknown, min: number, max: number) => (typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : null);
16 const minute = int(s.minute ?? 0, 0, 59);
17 const hour = int(s.hour ?? 9, 0, 23);
18 const weekday = int(s.weekday ?? 1, 0, 6);
19 if (minute === null || hour === null || weekday === null) return { ok: false, message: "The minute is 0 to 59, the hour 0 to 23 and the day 0 (Sunday) to 6." };
20 return { ok: true, value: { every, minute, hour, weekday } };
21}
22
23/** When a routine next runs after `after`, in UTC. */
24export function nextRun(schedule: RoutineSchedule, after: Date): Date {
25 const next = new Date(after.getTime());
26 next.setUTCSeconds(0, 0);
27 if (schedule.every === "hour") {
28 next.setUTCMinutes(schedule.minute);
29 if (next <= after) next.setUTCHours(next.getUTCHours() + 1);
30 return next;
31 }
32 next.setUTCHours(schedule.hour, schedule.minute);
33 if (next <= after) next.setUTCDate(next.getUTCDate() + 1);
34 for (let i = 0; i < 8; i++) {
35 const day = next.getUTCDay();
36 const fits = schedule.every === "day" || (schedule.every === "weekday" ? day >= 1 && day <= 5 : day === schedule.weekday);
37 if (fits) return next;
38 next.setUTCDate(next.getUTCDate() + 1);
39 }
40 return next;
41}
42
43/** A schedule in words: "Every weekday at 09:00 UTC". */
44export function describeSchedule(schedule: RoutineSchedule): string {
45 const pad = (n: number) => String(n).padStart(2, "0");
46 const at = `${pad(schedule.hour)}:${pad(schedule.minute)} UTC`;
47 const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
48 switch (schedule.every) {
49 case "hour":
50 return `Every hour at :${pad(schedule.minute)}`;
51 case "day":
52 return `Every day at ${at}`;
53 case "weekday":
54 return `Every weekday at ${at}`;
55 case "week":
56 return `Every ${days[schedule.weekday]} at ${at}`;
57 }
58}
59
60/** A routine as given, checked: a name, instructions and a schedule. */
61export function checkRoutine(input: NewRoutine): { ok: true; value: NewRoutine & { schedule: RoutineSchedule } } | { ok: false; message: string } {
62 const name = typeof input?.name === "string" ? input.name.trim().slice(0, 80) : "";
63 const instructions = typeof input?.instructions === "string" ? input.instructions.trim().slice(0, 8000) : "";
64 if (!name) return { ok: false, message: "Give the routine a name." };
65 if (instructions.length < 10) return { ok: false, message: "Say what the routine does, in a sentence or more." };
66 if (typeof input.channel_id !== "string" || !input.channel_id) return { ok: false, message: "Choose the channel it posts in." };
67 const schedule = checkSchedule(input.schedule);
68 if (!schedule.ok) return schedule;
69 return { ok: true, value: { ...input, name, instructions, schedule: schedule.value, enabled: input.enabled !== false } };
70}
71