# `scheduler` _(no submodules)_ ## Purpose `scheduler` provides `MessageScheduler` — the minimal timer seam that `SmsService.scheduleAt` uses to fire a delayed delivery. The class wraps an injectable `TimersLike` so tests drive it deterministically, computes the delay from an injectable `Clock`, and returns a `ScheduleHandle` with `cancel()` so a caller can abort a scheduled message before the timer fires. The scheduler is intentionally small: it does not own retry, rate-limit, opt-out, or template logic — those layers live in `SmsService` so a scheduled message goes through the full pipeline when the timer fires. ## Features - `scheduleAt(date, fn)` — schedules `fn` to run at the target instant. - Returns a `ScheduleHandle` with `cancel()` (idempotent) and the original `sendAt` field for observation. - Injectable `TimersLike` seam — defaults to a `globalThis.setTimeout` / `clearTimeout` adapter. - Injectable `Clock` — defaults to `Date.now`. - Past dates collapse to a zero-delay timer, matching the expectation that "send it now (or as soon as possible)". ## Usage: Basic ```ts import { MessageScheduler } from "@suaveplan/sms/scheduler"; const scheduler = new MessageScheduler(); const handle = scheduler.scheduleAt( new Date(Date.now() + 60_000), () => console.log("fire!") ); handle.cancel(); // call before firing to abort ``` ## Usage: Advanced ```ts import { MessageScheduler } from "@suaveplan/sms/scheduler"; // Controlled timers + clock for deterministic tests. const pending: Array<{ cb: () => void; delayMs: number }> = []; const scheduler = new MessageScheduler({ timers: { setTimer(cb, delayMs) { pending.push({ cb, delayMs }); return pending.length; }, clearTimer() { /* no-op for tests */ }, }, clock: () => 1_000, }); const handle = scheduler.scheduleAt(new Date(2_000), () => { /* runs at virtual t=2000 */ }); // Manually fire the queued callback to simulate the timer pending[0]?.cb(); handle.cancel(); // safe to call after fire ``` ## API Reference - `class MessageScheduler` - `constructor(options?: MessageSchedulerOptions)` - `scheduleAt(sendAt: Date, fn: () => void): ScheduleHandle` - `interface ScheduleHandle` - `cancel(): void` — idempotent - `readonly sendAt: Date` - `interface MessageSchedulerOptions` - `timers?: TimersLike` - `clock?: Clock` ## Implementation Notes The scheduler tracks a `cancelled` flag inside its timer callback so a cancellation that races the timer (i.e. the caller calls `cancel` after the timer has already been pulled but before the callback runs) is still honoured. Double-cancel is a no-op. The platform adapter casts the `setTimeout` return to `number` for cross-runtime compatibility — Node returns a `Timeout` object, browsers/Bun return a number; both shapes are safe to forward back to `clearTimeout`.