UNPKG

ethercalc

Version:

Multi-User Spreadsheet Server — TypeScript rewrite (Cloudflare fullstack)

84 lines (81 loc) 3.22 kB
/** * Phase 9 — cron trigger + email handlers. * * Two wire-level concerns: * * 1. `upsertCronTriggers(db, row)` — called from the `post-command` * glue when `parseSettimetrigger(cmdstr)` returns non-null. The * legacy flow (src/sc.ls:220-244) replaced the entire comma-list * for a (room, cell) pair each time. We mirror that: delete * existing rows for (room, cell), then insert one row per time. * When `times` is empty the net effect is a delete (clear). * * 2. `buildEmailSender(env)` — factory that returns a * `BindingEmailSender` when `env.EMAIL` is bound, otherwise a * `DisabledEmailSender` (no binding configured → report the honest * "not configured" state instead of a false "E-mail Sent"). Callers * never branch on the binding directly; the factory keeps the * business logic environment-agnostic. * * Pure of `Date.now()` — every helper takes values as arguments. * Pure of network — only the D1 binding and the injected sender are * touched. */ import { BindingEmailSender, DisabledEmailSender, type EmailSender, } from '../lib/email.ts'; import { withCronSchema } from '../lib/d1-schema.ts'; import type { Env } from '../env.ts'; /** * Replace the stored trigger set for (room, cell) with `times`. The * legacy behavior treats a `settimetrigger` command as "set" semantics * — previously stored times for that cell are dropped and the new * list takes their place. Empty `times` clears the cell entirely. * * Implementation: two statements, batched so a partial failure doesn't * leave the table half-updated. The DELETE + INSERT ordering matches * what the legacy HSET → HDEL flow produced at steady state. */ export async function upsertCronTriggers( db: D1Database, room: string, cell: string, times: readonly number[], ): Promise<void> { await withCronSchema(db, async () => { const del = db .prepare('DELETE FROM cron_triggers WHERE room = ?1 AND cell = ?2') .bind(room, cell); // Deduplicate times to keep the PK stable under repeat values from // the client. `Set` preserves insertion order; we only care about // uniqueness here because the index ordering is applied at scan // time by `scheduled()`. const uniqueTimes = Array.from(new Set(times)); if (uniqueTimes.length === 0) { await del.run(); return; } const insertStmt = db.prepare( 'INSERT OR IGNORE INTO cron_triggers (room, cell, fire_at) VALUES (?1, ?2, ?3)', ); const batch: D1PreparedStatement[] = [del]; for (const t of uniqueTimes) { batch.push(insertStmt.bind(room, cell, t)); } await db.batch(batch); }); } /** * Factory: return a live sender when `env.EMAIL` is bound, else a * `DisabledEmailSender` that reports email is not configured (rather * than the legacy false "E-mail Sent"). The caller's async contract is * identical regardless — both impls return `{ message: string }` and * neither rejects. */ export function buildEmailSender(env: Env): EmailSender { if (!env.EMAIL) return new DisabledEmailSender(); const from = env.EMAIL_FROM ?? 'noreply@ethercalc.invalid'; return new BindingEmailSender(env.EMAIL, from); }