/** * Lets `digest-service.ts` know when each user's next digest is due. * `digest_schedule` holds, for each user and cadence they receive, the start * of the period their next digest covers; it is due once a full period has * passed. * * @module */ import type { DatabaseSync } from "node:sqlite"; const DAY_MS = 86_400_000; // A frozen copy of digest-service's periods: a migration must keep doing // what it did when it shipped, whatever the service later becomes. const PERIOD_DAYS = { weekly: 7, monthly: 30 } as const; /** Spreads `userId` over `[0, days)` days, the same way each run. */ function offsetOf(userId: string, days: number): number { let hash = 2166136261; for (const ch of userId) hash = Math.imul(hash ^ ch.charCodeAt(0), 16777619); return ((hash >>> 0) % (days * 24)) * 3_600_000; } /** * Run once by `runMigrations`. Every user present gets both cadences, with a * first period that ends at some hour of the coming week or month, picked * from their id: the digests start trickling out after the deploy instead of * all leaving a week later. Users created later are scheduled by the service * when it first sees them. */ export function up(db: DatabaseSync): void { db.exec(` CREATE TABLE IF NOT EXISTS digest_schedule ( user_id TEXT NOT NULL, cadence TEXT NOT NULL, window_start TEXT NOT NULL, PRIMARY KEY (user_id, cadence), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); `); const users = db.prepare(`SELECT id FROM users;`).all() as { id: string }[]; const insert = db.prepare( `INSERT OR IGNORE INTO digest_schedule (user_id, cadence, window_start) VALUES (?, ?, ?);`, ); const now = Date.now(); for (const { id } of users) { for (const [cadence, days] of Object.entries(PERIOD_DAYS)) { const start = now - days * DAY_MS + offsetOf(id, days); insert.run(id, cadence, new Date(start).toISOString()); } } }