Files
skills/docs/communication/messaging/sms/src/scheduler/scheduler.md
2026-05-29 08:20:56 +01:00

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) — 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

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 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.