100 lines
3.9 KiB
TypeScript
100 lines
3.9 KiB
TypeScript
/**
|
|
* rSchedule cron — internal tick loops for reminders and Google Calendar sync.
|
|
*
|
|
* Runs in-process (no external scheduler); follows the same pattern as
|
|
* rCal's `startGcalSyncLoop()`. The user's original intent was to drive these
|
|
* from rMinders, but rMinders' generic action types don't yet cover our
|
|
* internal needs — keeping them here keeps the cron coupled to the module
|
|
* that owns the data. rMinders still handles user-authored automations.
|
|
*
|
|
* Tick cadence:
|
|
* - reminder sweep: every 5 minutes (checks window ±30 min around 24h mark)
|
|
* - gcal sweep: every 10 minutes per connected space
|
|
*/
|
|
|
|
import type { SyncServer } from "../../../server/local-first/sync-server";
|
|
import {
|
|
scheduleBookingsDocId,
|
|
scheduleConfigDocId,
|
|
type ScheduleBookingsDoc,
|
|
type ScheduleConfigDoc,
|
|
} from "../schemas";
|
|
import { syncGcalBusy } from "./gcal-sync";
|
|
import { sendBookingReminder } from "./emails";
|
|
|
|
const REMINDER_TICK_MS = 5 * 60 * 1000;
|
|
const GCAL_TICK_MS = 10 * 60 * 1000;
|
|
|
|
const REMINDER_WINDOW_START = 23 * 3600 * 1000; // 23h ahead
|
|
const REMINDER_WINDOW_END = 25 * 3600 * 1000; // 25h ahead
|
|
|
|
let _reminderTimer: ReturnType<typeof setInterval> | null = null;
|
|
let _gcalTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
/** Build "https://<host>" if the env hints at it — best-effort for email links. */
|
|
function hostBaseUrl(): string {
|
|
return process.env.RSPACE_BASE_URL || "https://rspace.online";
|
|
}
|
|
|
|
export function startRScheduleCron(syncServer: SyncServer): void {
|
|
if (_reminderTimer) return;
|
|
_reminderTimer = setInterval(() => void reminderSweep(syncServer).catch((e) => console.error("[rSchedule cron] reminder:", e?.message)), REMINDER_TICK_MS);
|
|
_gcalTimer = setInterval(() => void gcalSweep(syncServer).catch((e) => console.error("[rSchedule cron] gcal:", e?.message)), GCAL_TICK_MS);
|
|
// Run once on boot (small delay so syncServer.loadAllDocs has finished)
|
|
setTimeout(() => {
|
|
void reminderSweep(syncServer).catch(() => {});
|
|
void gcalSweep(syncServer).catch(() => {});
|
|
}, 15000);
|
|
console.log("[rSchedule cron] started — 5min reminder + 10min gcal sweeps");
|
|
}
|
|
|
|
export function stopRScheduleCron(): void {
|
|
if (_reminderTimer) { clearInterval(_reminderTimer); _reminderTimer = null; }
|
|
if (_gcalTimer) { clearInterval(_gcalTimer); _gcalTimer = null; }
|
|
}
|
|
|
|
// ── Sweeps ──
|
|
|
|
async function reminderSweep(syncServer: SyncServer): Promise<void> {
|
|
const now = Date.now();
|
|
for (const docId of syncServer.listDocs()) {
|
|
if (!docId.endsWith(":schedule:bookings")) continue;
|
|
const space = docId.split(":")[0];
|
|
const doc = syncServer.getDoc<ScheduleBookingsDoc>(docId);
|
|
if (!doc) continue;
|
|
const cfg = syncServer.getDoc<ScheduleConfigDoc>(scheduleConfigDocId(space));
|
|
const hostName = cfg?.settings.displayName || space;
|
|
|
|
for (const b of Object.values(doc.bookings)) {
|
|
if (b.status !== "confirmed") continue;
|
|
if (b.reminderSentAt) continue;
|
|
const delta = b.startTime - now;
|
|
if (delta < REMINDER_WINDOW_START || delta > REMINDER_WINDOW_END) continue;
|
|
|
|
const cancelUrl = `${hostBaseUrl()}/${space}/rschedule/cancel/${b.id}?token=${encodeURIComponent(b.cancelToken)}`;
|
|
const result = await sendBookingReminder({
|
|
booking: b as any,
|
|
settings: cfg?.settings || ({} as any),
|
|
hostDisplayName: hostName,
|
|
cancelUrl,
|
|
}).catch((e) => ({ ok: false, error: e?.message }));
|
|
|
|
if (result.ok) {
|
|
syncServer.changeDoc<ScheduleBookingsDoc>(docId, `reminder sent ${b.id}`, (d) => {
|
|
if (d.bookings[b.id]) d.bookings[b.id].reminderSentAt = Date.now();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function gcalSweep(syncServer: SyncServer): Promise<void> {
|
|
for (const docId of syncServer.listDocs()) {
|
|
if (!docId.endsWith(":schedule:config")) continue;
|
|
const space = docId.split(":")[0];
|
|
const cfg = syncServer.getDoc<ScheduleConfigDoc>(docId);
|
|
if (!cfg?.googleAuth?.connected) continue;
|
|
await syncGcalBusy(space, syncServer).catch(() => { /* per-space failures logged inside */ });
|
|
}
|
|
}
|