import { Options } from 'rrule'; /** * Public RRStack types, options, and JSON shapes. * * Design notes * - Unit-aware domain: no EPOCH_* constants exported; domain bounds are handled * internally according to {@link UnixTimeUnit}. * - Keep module small and testable (SRP). */ /** Instant status classification for a timestamp. */ type InstantStatus = 'active' | 'blackout'; /** Effect type for rules (includes event for zero-duration instants). */ type EffectType = InstantStatus | 'event'; /** Default effect classification for an RRStack. */ type DefaultEffect = InstantStatus | 'auto'; /** Range classification across `[from, to)`. */ type RangeStatus = InstantStatus | 'partial'; /** Time unit for inputs/outputs and internal computation. */ type UnixTimeUnit = 'ms' | 's'; /** * Branded IANA timezone id after runtime validation. Use * {@link RRStack.asTimeZoneId | RRStack.asTimeZoneId} to construct one from a string. */ type TimeZoneId = string & { __brand: 'TimeZoneId'; }; /** * Human-readable RRULE frequency (lower-case) for recurring rules. * Mapped internally to rrule's numeric Frequency enum during compilation. */ type FrequencyStr = 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly'; /** * Structured duration parts for UI-friendly, lossless round-tripping. * - All fields are non-negative integers. * - At least one field must be \> 0 (duration must be strictly positive). * - Calendar vs exact: * • \{ days: 1 \} → calendar day (can be 23/25 hours across DST), * • \{ hours: 24 \} → exact 24 hours. */ interface DurationParts { years?: number; months?: number; weeks?: number; days?: number; hours?: number; minutes?: number; seconds?: number; } /** * JSON shape for rule options: * - Derived from rrule Options with `dtstart`/`until`/`tzid` removed. * - `freq` is optional: * • present → recurring rule (RRULE-based), * • absent → continuous span rule (no recurrence). * - Adds `starts`/`ends` in the configured {@link UnixTimeUnit} for domain clamping. * - When `freq` is present, RRStack maps it to rrule's numeric enum internally. */ type RuleOptionsJson = Partial> & { /** * Optional frequency. When omitted, the rule is a continuous span and must * omit `duration`. */ freq?: FrequencyStr; starts?: number; ends?: number; }; /** A single rule in the cascade. */ interface RuleJson { /** `'active' | 'blackout' | 'event'` — effect applied at covered instants. */ effect: EffectType; /** Structured duration for recurring rules; must be omitted for span rules. */ duration?: DurationParts; /** Subset of rrule options (see {@link RuleOptionsJson}). */ options: RuleOptionsJson; /** Optional label for diagnostics/UI. */ label?: string; } /** * Constructor input and serialized output (round-trippable). * - `version` is optional on input and ignored by the constructor. * - {@link RRStack.toJson | toJson()} always writes the current package version. */ interface RRStackOptions { /** Optional version string; ignored by the constructor. */ version?: string; /** IANA timezone id (validated at runtime). */ timezone: string; /** Time unit ('ms' | 's'). Defaults to 'ms'. */ timeUnit?: UnixTimeUnit; /** Baseline effect for uncovered instants. Defaults to 'auto'. */ defaultEffect?: DefaultEffect; /** Rule list. Defaults to empty. */ rules?: RuleJson[]; } /** * Normalized options stored on the instance (frozen). * - `timeUnit` is required. * - `rules` is a readonly array. * - `timezone` is a branded, validated string. */ interface RRStackOptionsNormalized extends Omit { timeUnit: UnixTimeUnit; rules: readonly RuleJson[]; timezone: TimeZoneId; defaultEffect: DefaultEffect; } /** * Notices emitted by {@link RRStack.update | RRStack.update()} to describe * version/unit handling outcomes. Returned in order and also delivered via * {@link UpdatePolicy.onNotice | onNotice} when provided. * * Example: log all notices while accepting newer versions with a warning * ```ts * const notices = stack.update(incomingJson, { * onVersionDown: 'warn', * onNotice: (n) => { * console.info('[rrstack.notice]', n.kind, n.action); * }, * }); * // `notices` contains the same entries, in the same order * ``` * * @public * Notices emitted by RRStack.update() to describe version/unit handling outcomes. */ type Notice = { kind: 'versionUp'; level: 'error' | 'warn' | 'info'; from: string | null; to: string; action: 'upgrade' | 'rejected' | 'ingestAsCurrent'; message?: string; } | { kind: 'versionDown'; level: 'error' | 'warn' | 'info'; from: string | null; to: string; action: 'rejected' | 'ingestAsCurrent'; message?: string; } | { kind: 'versionInvalid'; level: 'error' | 'warn' | 'info'; raw: unknown; to: string; action: 'rejected' | 'ingestAsCurrent'; message?: string; } | { kind: 'timeUnitChange'; level: 'error' | 'warn' | 'info'; from: UnixTimeUnit; to: UnixTimeUnit; action: 'convertedExisting' | 'acceptedIncomingRules' | 'rejected'; convertedRuleCount?: number; replacedRuleCount?: number; message?: string; }; /** * Policy switches for {@link RRStack.update | RRStack.update()}. Defaults: * - onVersionUp: 'off'; onVersionDown: 'error'; onVersionInvalid: 'error'; * - onTimeUnitChange: 'warn'. * * Example: accept newer versions with a warning and surface time‑unit changes * ```ts * stack.update(incoming, { * onVersionDown: 'warn', * onTimeUnitChange: 'warn', * onNotice: (n) => { * // route to your logger/telemetry * logger.info({ notice: n }); * }, * }); * ``` * * React passthrough: * When supplied via `useRRStack({ policy })`, this policy is applied to both: * - prop ingestion (`json` → engine), and * - staged UI commits (timezone/rules/timeUnit). * * @public */ interface UpdatePolicy { onVersionUp?: 'error' | 'warn' | 'off'; onVersionDown?: 'error' | 'warn' | 'off'; onVersionInvalid?: 'error' | 'warn' | 'off'; onTimeUnitChange?: 'error' | 'warn' | 'off'; onNotice?: (n: Notice) => void; } type FrequencyAdjectiveLabels = Record; type FrequencyNounLabels = Record; interface FrequencyLexicon { adjective: FrequencyAdjectiveLabels; noun: FrequencyNounLabels; pluralize?: (noun: string, n: number) => string; } interface RuleDescriptorBase { kind: 'span' | 'recur' | 'event' | 'oneTimeEvent'; effect: 'active' | 'blackout' | 'event'; tz: string; unit: UnixTimeUnit; clamps?: { starts?: number; ends?: number; }; } interface WeekdayPos { /** 1..7 (Mon..Sun) */ weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7; /** ±1..±5; -1 === last */ nth?: number; } interface RuleDescriptorRecur extends RuleDescriptorBase { kind: 'recur'; freq: FrequencyStr; interval: number; duration: DurationParts; by: { months?: number[]; monthDays?: number[]; yearDays?: number[]; weekNos?: number[]; weekdays?: WeekdayPos[]; hours?: number[]; minutes?: number[]; seconds?: number[]; setpos?: number[]; wkst?: 1 | 2 | 3 | 4 | 5 | 6 | 7; }; count?: number; until?: number; } interface RuleDescriptorSpan extends RuleDescriptorBase { kind: 'span'; } interface RuleDescriptorEvent extends RuleDescriptorBase { kind: 'event'; freq: FrequencyStr; interval: number; by: RuleDescriptorRecur['by']; count?: number; until?: number; } interface RuleDescriptorOneTimeEvent extends RuleDescriptorBase { kind: 'oneTimeEvent'; at: number; } type RuleDescriptor = RuleDescriptorRecur | RuleDescriptorSpan | RuleDescriptorEvent | RuleDescriptorOneTimeEvent; type DescribeTranslator = (desc: RuleDescriptor, cfg?: DescribeConfig) => string; /** * Unified configuration for rule descriptions. * Translators own the entire sentence (effect, duration, cadence, bounds, tz). */ interface DescribeConfig { /** Translator chooser (default: 'strict-en'). */ translator?: 'strict-en' | DescribeTranslator; /** Show "(timezone