Files
suaveplan/packages/foundation/timing/cron/src/next-run/next-run-fields.md
2026-05-27 23:47:53 +01:00

3.6 KiB
Raw Blame History

next-run-fields

Purpose

next-run-fields houses the low-level field-walking helpers used by the forward and backward next-run search loops in next-run.ts. Splitting the helpers into a sibling module keeps each file under the 300-line hard limit while keeping the call sites in next-run.ts declarative — the forward loop reads as snapForward → fromWallClock → verify, with the fine-grained carry/rollover logic isolated here. None of these helpers are part of the package's public API; they exist purely to support the internal next-run engine.

Features

  • climbFields(wall, fields) — single forward step across seconds → minutes → hours → days, honoring per-field carries.
  • climbFieldsBackward(wall, fields) — mirror walk used by previousRun.
  • advanceUntilMonthMatches, advanceUntilDayMatches, advanceUntilYearMatches — coarse jump helpers that snap a wall-clock forward to the next legal month / day / year for the field set.
  • nextValueAtLeast / previousValueAtMost — sorted-array lookup helpers shared by every field walker.
  • copyWall — converts an immutable WallClock to a MutableWall so the search loop can mutate without aliasing.

Usage: Basic Example

import {
    advanceUntilMonthMatches,
    climbFields,
    copyWall,
} from "@suaveplan/cron/next-run/next-run";
import { toWallClock } from "@suaveplan/cron";

const fields = {
    minutes: [0],
    hours: [9],
    daysOfMonth: Array.from({ length: 31 }, (_, i) => i + 1),
    months: [6, 12],
    daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
};

const wall = copyWall(toWallClock(Date.now(), "UTC"));
climbFields(wall, fields);
advanceUntilMonthMatches(wall, fields);
console.log(wall.month);

Usage: Advanced Example

import {
    climbFieldsBackward,
    copyWall,
} from "@suaveplan/cron/next-run/next-run";
import { toWallClock } from "@suaveplan/cron";

const fields = {
    minutes: [0, 15, 30, 45],
    hours: [9, 10, 11, 12, 13, 14, 15, 16, 17],
    daysOfMonth: Array.from({ length: 31 }, (_, i) => i + 1),
    months: Array.from({ length: 12 }, (_, i) => i + 1),
    daysOfWeek: [1, 2, 3, 4, 5],
};

const wall = copyWall(toWallClock(Date.UTC(2026, 4, 11, 12, 47), "UTC"));
climbFieldsBackward(wall, fields);
console.log(wall.hour, wall.minute); // 12, 45

API Reference

  • HORIZON_YEARS: 8, MS_PER_SECOND: 1000, MS_PER_MINUTE: 60000.
  • MutableWall — mutable counterpart of WallClock minus weekday.
  • nextValueAtLeast(sorted, current), previousValueAtMost(sorted, current).
  • daysInMonth(year, month) — calendar-correct day count.
  • copyWall(w).
  • climbFields(wall, fields), climbFieldsBackward(wall, fields).
  • advanceUntilMonthMatches, advanceUntilDayMatches, advanceUntilYearMatches, yearMatches.

Implementation Notes

The forward and backward walkers are written as small step functions so each carry case can be reasoned about independently. Day-of-month and day-of-week interaction follows Vixie OR semantics: when one field is the full range and the other is restricted, only the restricted field matters; when both are restricted, either match is sufficient. advanceUntilDayMatches walks one calendar day at a time so the OR semantics fall out naturally rather than requiring a more elaborate bitset intersection. Safety bounds in the loops cap iterations at 366 × (HORIZON_YEARS + 1) so pathological input cannot livelock the search; the outer search functions in next-run.ts then convert the resulting wall clock into an epoch instant and verify the result still matches the field set, catching DST-gap edge cases.