/** * Local cache of today's calendar schedule. * Synced periodically from HF backend, checked locally for due slots. */ import type { CalendarSlotResponse } from "./types.js"; export class ScheduleCache { private slots: Map = new Map(); private lastSyncAt: Date | null = null; private cachedDate: string | null = null; // YYYY-MM-DD /** * Replace the cache with a fresh schedule from backend. */ sync(date: string, slots: CalendarSlotResponse[]): void { // If date changed, clear old data if (this.cachedDate !== date) { this.slots.clear(); } this.cachedDate = date; // Merge: update existing slots, add new ones const incomingIds = new Set(); for (const slot of slots) { const id = this.getSlotId(slot); incomingIds.add(id); this.slots.set(id, slot); } // Remove slots that no longer exist on backend (cancelled etc.) for (const id of this.slots.keys()) { if (!incomingIds.has(id)) { this.slots.delete(id); } } this.lastSyncAt = new Date(); } /** * Get slots that are due (scheduled_at <= now) and still pending. */ getDueSlots(now: Date): CalendarSlotResponse[] { const results: CalendarSlotResponse[] = []; for (const slot of this.slots.values()) { if (slot.status !== "not_started" && slot.status !== "deferred") continue; if (!slot.scheduled_at) continue; const scheduledAt = this.parseScheduledTime(slot.scheduled_at); if (scheduledAt && scheduledAt <= now) { results.push(slot); } } // Sort by priority descending results.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); return results; } /** * Update a slot locally (e.g., after status change). */ updateSlot(slotId: string, update: Partial): void { const existing = this.slots.get(slotId); if (existing) { this.slots.set(slotId, { ...existing, ...update }); } } /** * Remove a slot from cache. */ removeSlot(slotId: string): void { this.slots.delete(slotId); } /** * Get all cached slots. */ getAll(): CalendarSlotResponse[] { return Array.from(this.slots.values()); } /** * Get cache metadata. */ getStatus(): { slotCount: number; lastSyncAt: string | null; cachedDate: string | null } { return { slotCount: this.slots.size, lastSyncAt: this.lastSyncAt?.toISOString() ?? null, cachedDate: this.cachedDate, }; } private getSlotId(slot: CalendarSlotResponse): string { return slot.virtual_id ?? String(slot.id); } private parseScheduledTime(scheduledAt: string): Date | null { // scheduled_at can be "HH:MM:SS" (time only) or full ISO if (/^\d{2}:\d{2}(:\d{2})?$/.test(scheduledAt)) { // Time-only: combine with cached date if (!this.cachedDate) return null; return new Date(`${this.cachedDate}T${scheduledAt}Z`); } const d = new Date(scheduledAt); return isNaN(d.getTime()) ? null : d; } }