89 lines
3.5 KiB
Markdown
89 lines
3.5 KiB
Markdown
# timezone
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
## Purpose
|
|
|
|
The timezone module converts between epoch-millisecond timestamps and the
|
|
wall-clock components observed in a specific IANA timezone using only the
|
|
platform-native `Intl.DateTimeFormat` — no external timezone database is
|
|
shipped or required. It powers the cron next-run algorithm by translating
|
|
the user's "every day at 02:00 New York time" into the actual moments
|
|
when that wall-clock instant occurs, including the awkward edge cases of
|
|
daylight-saving-time spring-forward gaps and fall-back overlaps.
|
|
|
|
## Features
|
|
|
|
- `toWallClock(epochMs, tz)` — decompose an epoch instant into year /
|
|
month / day / hour / minute / second / weekday in the requested zone.
|
|
- `fromWallClock(components, tz)` — the inverse; performs up to four
|
|
bounded fixed-point iterations to converge on the correct epoch even
|
|
across DST transitions.
|
|
- `detectDstTransition(before, after, tz)` — classifies a transition
|
|
between two adjacent instants as `"spring-forward"`, `"fall-back"`, or
|
|
`null` when no transition is crossed.
|
|
- `assertTimezone(tz)` — eagerly validates an IANA name so the caller can
|
|
fail fast at the API boundary.
|
|
- Per-zone formatter cache keeps Intl construction off the hot path.
|
|
|
|
## Usage: Basic Example
|
|
|
|
```typescript
|
|
import { toWallClock, fromWallClock } from "@suaveplan/cron";
|
|
|
|
const wall = toWallClock(Date.UTC(2026, 0, 1, 5, 0, 0), "America/New_York");
|
|
console.log(wall.hour); // 0 — midnight in NY
|
|
|
|
const back = fromWallClock(
|
|
{ year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
|
|
"America/New_York"
|
|
);
|
|
console.log(back === Date.UTC(2026, 0, 1, 5, 0, 0)); // true
|
|
```
|
|
|
|
## Usage: Advanced Example
|
|
|
|
```typescript
|
|
import {
|
|
detectDstTransition,
|
|
fromWallClock,
|
|
toWallClock,
|
|
} from "@suaveplan/cron";
|
|
|
|
const start = Date.UTC(2026, 2, 8, 6, 0, 0);
|
|
const end = Date.UTC(2026, 2, 8, 8, 0, 0);
|
|
const transition = detectDstTransition(start, end, "America/New_York");
|
|
console.log(transition); // "spring-forward"
|
|
|
|
const requested = { year: 2026, month: 3, day: 8, hour: 2, minute: 30, second: 0 };
|
|
const real = fromWallClock(requested, "America/New_York");
|
|
const observed = toWallClock(real, "America/New_York");
|
|
console.log(observed.hour, observed.minute); // not 02:30 — the slot was skipped
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `interface WallClock` — `{ year, month, day, hour, minute, second, weekday }`
|
|
with `weekday ∈ 0..6` (Sunday = 0, matching cron numbering).
|
|
- `toWallClock(epochMs: number, tz: string): WallClock`.
|
|
- `fromWallClock(components: Omit<WallClock, "weekday">, tz: string): number`.
|
|
- `detectDstTransition(beforeEpochMs, afterEpochMs, tz):
|
|
"spring-forward" | "fall-back" | null`.
|
|
- `assertTimezone(tz: string): void` — throws
|
|
`CronError(CRON.TIMEZONE_INVALID)` for invalid IANA names.
|
|
|
|
## Implementation Notes
|
|
|
|
`Intl.DateTimeFormat` is the only timezone authority. The package never
|
|
ships a tz database — keeping it universal and dependency-free. The
|
|
inverse `fromWallClock` works by guessing UTC, observing the wall clock
|
|
offset that produces, and applying it back. Two iterations suffice for
|
|
every IANA zone in normal time; we run up to four to handle the awkward
|
|
spring-forward case where the first correction overshoots the gap.
|
|
Inside the gap the function picks the canonical real-instant that
|
|
`Intl.DateTimeFormat` reports, and callers re-verify against their field
|
|
set so cron schedules that fall in the gap fire at the next existing
|
|
minute. Formatter construction is cached per IANA name to keep the hot
|
|
path allocation-free.
|