2.9 KiB
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)— schedulesfnto run at the target instant.- Returns a
ScheduleHandlewithcancel()(idempotent) and the originalsendAtfield for observation. - Injectable
TimersLikeseam — defaults to aglobalThis.setTimeout/clearTimeoutadapter. - Injectable
Clock— defaults toDate.now. - Past dates collapse to a zero-delay timer, matching the expectation that "send it now (or as soon as possible)".
Usage: Basic
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
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 MessageSchedulerconstructor(options?: MessageSchedulerOptions)scheduleAt(sendAt: Date, fn: () => void): ScheduleHandle
interface ScheduleHandlecancel(): void— idempotentreadonly sendAt: Date
interface MessageSchedulerOptionstimers?: TimersLikeclock?: 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.