first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# `scheduler`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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`.