feat(tz): isNearUtc helper for cross-tz fast-path guard

Pure helper returns true when the supplied IANA zone is within +-3h
of UTC at the supplied instant. Exposed from both lib/tz/format and
lib/tz/resolver so client and server callers share the same gate.
Honours DST via the existing tzOffsetMinutes path.

Used by the v1.4.38 analytics fast-path runtime guards
(bp-in-target, correlations) to fall back to live SQL for users far
from UTC, where the rollup table's UTC-midnight bucketStart slips a
calendar day relative to the per-event mood / intake stream.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 17:09:27 +02:00
1 parent dcd0b0a591
commit 33dc00fe62
3 files changed
+79

No files matched your search

+46
View File
@@ -23,6 +23,7 @@ import {
formatInUserTz,
invalidateServerDefaultTimezone,
invalidateUserTimezone,
isNearUtc,
isValidTimezone,
resolveServerDefaultTimezone,
resolveUserTimezone,
@@ -261,6 +262,51 @@ describe("userDayKey", () => {
});
});
describe("isNearUtc", () => {
// Pin a summer-side instant so DST is on for Berlin (+2) and off for
// most southern-hemisphere zones — keeps the assertions readable.
const summer = new Date("2026-07-15T12:00:00.000Z");
// Winter-side instant so Berlin is on CET (+1).
const winter = new Date("2026-01-15T12:00:00.000Z");
it("returns true for UTC", () => {
expect(isNearUtc("UTC", summer)).toBe(true);
});
it("returns true for Europe/Berlin in both summer and winter", () => {
expect(isNearUtc("Europe/Berlin", summer)).toBe(true);
expect(isNearUtc("Europe/Berlin", winter)).toBe(true);
});
it("returns true for the ±3h boundary zones", () => {
// Europe/Moscow = +3 year-round.
expect(isNearUtc("Europe/Moscow", summer)).toBe(true);
// Atlantic/Azores = -1 in winter, 0 in summer.
expect(isNearUtc("Atlantic/Azores", summer)).toBe(true);
});
it("returns false for zones more than 3 hours from UTC", () => {
// Pacific/Honolulu = -10 year-round.
expect(isNearUtc("Pacific/Honolulu", summer)).toBe(false);
// Asia/Tokyo = +9 year-round.
expect(isNearUtc("Asia/Tokyo", summer)).toBe(false);
// America/New_York = -4 in summer, -5 in winter.
expect(isNearUtc("America/New_York", summer)).toBe(false);
// Pacific/Auckland = +13 in southern summer (Jan), +12 in May.
expect(isNearUtc("Pacific/Auckland", winter)).toBe(false);
});
it("defaults to near-UTC when the zone string is invalid", () => {
// Invalid zones fall back to Europe/Berlin, which is near-UTC.
expect(isNearUtc("Mars/Olympus_Mons", summer)).toBe(true);
});
it("uses the current instant when `now` is omitted", () => {
// Pure smoke — exercises the default parameter branch.
expect(typeof isNearUtc("Europe/Berlin")).toBe("boolean");
});
});
describe("DEFAULT_TIMEZONE", () => {
it("is Europe/Berlin", () => {
expect(DEFAULT_TIMEZONE).toBe("Europe/Berlin");
+32
View File
@@ -67,6 +67,38 @@ export function detectBrowserTimezone(): string {
}
}
/**
* v1.4.38 W-A — cross-tz fast-path runtime guard.
*
* Returns `true` when the supplied IANA zone is within ±3 hours of UTC
* at the supplied instant. Used by the analytics fast-paths
* (`bp-in-target-fast-path`, `correlations-fast-path`) to gate the
* rollup read-swap: those helpers key rollup rows on UTC-midnight
* `bucketStart` and pair them with per-event streams keyed in the
* user's local timezone. For Berlin (+1/+2) the day-key on the rollup
* and the day-key on the per-event row line up; for Honolulu (-10)
* they slip by a calendar day. Three hours is a conservative pin —
* the worst-case slip inside the window is the wall-clock hour-of-day
* the user happens to log at, and three hours covers every European
* zone plus the western edge of Mid-Atlantic / eastern edge of the
* Americas.
*
* Falls back to "near-UTC" (i.e. returns `true`) when the zone string
* is invalid — the resolver layer is already defensive about junk
* values, and a `true` here means the caller takes the rollup
* fast-path which is the safer default for the canonical Berlin
* tenant.
*
* The `now` argument lets callers honour DST transitions; the offset
* of `Europe/Berlin` is +1 in January and +2 in July. Defaults to
* "now" so most callers can `isNearUtc(userTz)`.
*/
export function isNearUtc(tz: string, now: Date = new Date()): boolean {
const safeTz = isValidTimezone(tz) ? tz : DEFAULT_TIMEZONE;
const offsetMinutes = tzOffsetMinutes(now, safeTz);
return Math.abs(offsetMinutes) <= 3 * 60;
}
export type FormatInUserTzShape =
| "iso-with-offset"
| "wall-clock"
+1
View File
@@ -47,6 +47,7 @@ import {
export {
DEFAULT_TIMEZONE,
isValidTimezone,
isNearUtc,
listSupportedTimezones,
detectBrowserTimezone,
formatInUserTz,