fix(sse): bound daily quota cooldowns around daylight-saving transitions (#13671)

Fixes the DST-gap bug in `nextDailyResetAtMs`: a reset hour that does not exist on the transition day landed one hour early (New York 02:00 came out as 01:00; Havana/Santiago midnight as 23:00 the day before). The walk across the gap is bounded to one day and uses a cached formatter.

Maintainer rework before merge (kept the idea, no default behavior change):
- Dropped the 24h clamp in `getMsUntilTomorrow` (on a 25h fall-back day 24.5h is the correct wait; clamping expired the lock 30 minutes early) and the unreachable `ms <= 0` branch, with their tests; characterization tests pin ordinary and fall-back days.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
This commit is contained in:
Dizzle
2026-09-15 20:07:42 +02:00
committed by GitHub
parent 520428c8ad
commit cad0fcc65d
4 changed files with 159 additions and 19 deletions

View File

@@ -0,0 +1 @@
- **fix(sse):** a configured daily-quota reset hour that falls inside a daylight-saving gap (New York 02:00 on spring-forward, Havana/Santiago midnight) now resolves to the first wall-clock time that exists instead of landing an hour early, sometimes on the previous day ([#13671](https://github.com/diegosouzapw/OmniRoute/pull/13671)) — thanks @maxmad64bis

View File

@@ -32,17 +32,30 @@ type ZonedParts = {
second: number;
};
// Formatter construction dominates zonedParts; the DST-gap walk below calls it
// hundreds of times, so reuse one formatter per (validated) IANA zone.
const zonedFormatters = new Map<string, Intl.DateTimeFormat>();
function zonedFormatter(timeZone: string): Intl.DateTimeFormat {
let fmt = zonedFormatters.get(timeZone);
if (!fmt) {
fmt = new Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
zonedFormatters.set(timeZone, fmt);
}
return fmt;
}
function zonedParts(ms: number, timeZone: string): ZonedParts {
const fmt = new Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
const fmt = zonedFormatter(timeZone);
const bag: Record<string, string> = {};
for (const part of fmt.formatToParts(new Date(ms))) {
if (part.type !== "literal") bag[part.type] = part.value;
@@ -71,6 +84,35 @@ function addCalendarDay(
return { year: dt.getUTCFullYear(), month: dt.getUTCMonth() + 1, day: dt.getUTCDate() };
}
/**
* Offset-iteration wall-clock → epoch conversion. `exact` is false when the
* iteration never lands on the wanted wall time, which is what a wall time
* inside a DST gap (a local time that does not exist) does.
*/
function convergeWallTime(
year: number,
month: number,
day: number,
hour: number,
minute: number,
second: number,
timeZone: string
): { ms: number; exact: boolean } {
const wanted = Date.UTC(year, month - 1, day, hour, minute, second);
let guess = wanted;
for (let i = 0; i < 4; i++) {
const p = zonedParts(guess, timeZone);
const asIfUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
const delta = asIfUtc - wanted;
if (delta === 0) return { ms: guess, exact: true };
guess -= delta;
}
return { ms: guess, exact: false };
}
/** Gap-walk bound: one full day covers every civil gap, including a skipped calendar day. */
const MAX_GAP_WALK_MINUTES = 24 * 60;
/** Convert wall-clock time in `timeZone` to epoch ms. */
function zonedLocalToUtc(
year: number,
@@ -81,16 +123,33 @@ function zonedLocalToUtc(
second: number,
timeZone: string
): number {
const wanted = Date.UTC(year, month - 1, day, hour, minute, second);
let guess = wanted;
for (let i = 0; i < 4; i++) {
const p = zonedParts(guess, timeZone);
const asIfUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
const delta = asIfUtc - wanted;
if (delta === 0) return guess;
guess -= delta;
const first = convergeWallTime(year, month, day, hour, minute, second, timeZone);
if (first.exact) return first.ms;
// DST gap (New York 02:00 on spring-forward, Havana/Santiago 00:00): the offset
// iteration settles an hour EARLY. Walk the wall clock forward minute by minute to
// the first wall time that exists; gap widths vary (30 min, 1 h), so never add a
// fixed offset.
let date = { year, month, day };
let minuteOfDay = hour * 60 + minute;
for (let step = 0; step < MAX_GAP_WALK_MINUTES; step++) {
minuteOfDay += 1;
if (minuteOfDay >= 24 * 60) {
minuteOfDay -= 24 * 60;
date = addCalendarDay(date.year, date.month, date.day);
}
const h = Math.floor(minuteOfDay / 60);
const candidate = convergeWallTime(
date.year,
date.month,
date.day,
h,
minuteOfDay % 60,
second,
timeZone
);
if (candidate.exact) return candidate.ms;
}
return guess;
return first.ms;
}
/**

View File

@@ -247,6 +247,7 @@
"tests/unit/correctness/sanitizers.property.test.ts",
"tests/unit/cursor-renewal.test.ts",
"tests/unit/custom-model-target-format.test.ts",
"tests/unit/daily-reset-dst-gap.test.ts",
"tests/unit/db-reset-module-state.test.ts",
"tests/unit/db-server-tool-executions-migration.test.ts",
"tests/unit/db/stats-dbstat-optional.test.ts",

View File

@@ -0,0 +1,79 @@
/**
* #13671 — a configured daily reset hour that does not exist on a DST
* spring-forward day must resolve to the first wall-clock time that exists,
* never an hour early (and never on the previous calendar day).
*/
import test from "node:test";
import assert from "node:assert/strict";
const { nextDailyResetAtMs } = await import("../../open-sse/services/dailyQuotaReset.ts");
const HOUR_MS = 60 * 60 * 1000;
function wallClock(timeZone: string, ms: number): string {
return new Intl.DateTimeFormat("en-CA", {
timeZone,
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(ms));
}
test("New York 02:00 on spring-forward resolves to 03:00 EDT, not 01:00 EST", () => {
// 2026-03-08: 02:00 -> 03:00 in America/New_York; 02:00 does not exist.
const nowMs = Date.parse("2026-03-08T00:30:00-05:00");
const next = nextDailyResetAtMs("America/New_York", 2, nowMs);
assert.equal(new Date(next).toISOString(), "2026-03-08T07:00:00.000Z");
assert.equal(wallClock("America/New_York", next), "2026-03-08, 03:00");
});
test("Havana midnight on spring-forward resolves to 01:00 the same day, not 23:00 the day before", () => {
// 2026-03-08: 00:00 -> 01:00 in America/Havana; midnight does not exist.
const nowMs = Date.parse("2026-03-07T20:00:00-05:00");
const next = nextDailyResetAtMs("America/Havana", 0, nowMs);
assert.equal(wallClock("America/Havana", next), "2026-03-08, 01:00");
assert.equal(next - nowMs, 4 * HOUR_MS);
});
test("Santiago midnight on spring-forward resolves to 01:00 the same day, not 23:00 the day before", () => {
// 2026-09-06: 00:00 -> 01:00 in America/Santiago; midnight does not exist.
const nowMs = Date.parse("2026-09-05T20:00:00-04:00");
const next = nextDailyResetAtMs("America/Santiago", 0, nowMs);
assert.equal(wallClock("America/Santiago", next), "2026-09-06, 01:00");
assert.equal(next - nowMs, 4 * HOUR_MS);
});
test("between the old (wrong) 23:00 and the real 01:00 the reset is still ahead", () => {
const nowMs = Date.parse("2026-03-07T23:30:00-05:00"); // Havana 23:30, before the gap
const next = nextDailyResetAtMs("America/Havana", 0, nowMs);
assert.equal(wallClock("America/Havana", next), "2026-03-08, 01:00");
assert.equal(next - nowMs, 30 * 60 * 1000);
});
test("fold hour keeps the first occurrence (characterization)", () => {
// 2026-11-01: fall back, 01:00 occurs twice; the first (EDT) occurrence wins.
const nowMs = Date.parse("2026-11-01T00:30:00-04:00");
const next = nextDailyResetAtMs("America/New_York", 1, nowMs);
assert.equal(new Date(next).toISOString(), "2026-11-01T05:00:00.000Z");
});
test("a 25h fall-back day keeps its real 24.5h magnitude (characterization, no clamp)", () => {
const nowMs = Date.parse("2026-11-01T00:30:00-04:00");
const next = nextDailyResetAtMs("America/New_York", 0, nowMs);
assert.equal(next - nowMs, 24.5 * HOUR_MS);
});
test("ordinary days are unchanged", () => {
const nowMs = Date.parse("2026-01-15T10:00:00Z");
assert.equal(
new Date(nextDailyResetAtMs("Europe/Paris", 0, nowMs)).toISOString(),
"2026-01-15T23:00:00.000Z"
);
assert.equal(
new Date(nextDailyResetAtMs("Asia/Kolkata", 0, nowMs)).toISOString(),
"2026-01-15T18:30:00.000Z"
);
});