/** * Todoist background sync scheduler (Issue #22). * * Periodically fetches Todoist tasks for every user with an active token so * that signals are fresh before the next /recommend call. The on-demand fetch * in TodoistSignalSource remains as the freshness-critical fallback — if a user * hits /recommend and the cache is stale, it re-fetches inline. * * Interval: TODOIST_SYNC_INTERVAL_MS (default 15 min). */ import { db } from '../db/index.js'; import { integrationTokens } from '../db/schema.js'; import { eq } from 'drizzle-orm'; import { todoistSource } from './todoist.js'; import { logger } from '../logger.js'; const DEFAULT_INTERVAL_MS = 15 * 60 * 1000; export function startTodoistSyncScheduler(intervalMs = DEFAULT_INTERVAL_MS): NodeJS.Timeout { async function syncAll(): Promise { let users: { userId: string }[] = []; try { users = await db .select({ userId: integrationTokens.userId }) .from(integrationTokens) .where(eq(integrationTokens.tokenStatus, 'active')); } catch (err: any) { logger.error({ err }, 'scheduler: failed to query users'); return; } if (!users.length) return; const results = await Promise.allSettled( users.map((u) => todoistSource.fetchSignals(u.userId)), ); let ok = 0; let failed = 0; for (const r of results) { if (r.status === 'fulfilled') ok++; else { failed++; logger.error({ err: r.reason }, 'scheduler: sync error'); } } logger.info({ ok, failed, total: users.length }, 'scheduler: todoist sync'); } // Run once shortly after startup, then on interval const delay = setTimeout(() => { syncAll(); setInterval(syncAll, intervalMs); }, 10_000); return delay; }