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,88 @@
# 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.