New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/time

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/time - npm Package Compare versions

Comparing version
16.0.0
to
17.0.0
+18
-3
CLAUDE.md

@@ -15,3 +15,2 @@ # @ultimat3/time — agent notes

| `format.ts` | `Intl` rendering. Every function takes `locale` **and** `zone`. |
| `locale.ts` | `assertLocale` — the ONE screen a caller-supplied BCP 47 tag passes before `Intl` |
| `duration.ts` | `'2h30m'` ⇄ ms |

@@ -56,3 +55,5 @@ | `cron.ts` | barrel over the three cron modules — the only one `index.ts` re-exports |

which is true of the cache and was not an argument for letting the tag through. **Breaking at
9.x.** `assertLocale` (`locale.ts`) is the one screen and it VALIDATES AND CANONICALIZES in one
9.x.** `assertLocale` (**`@ultimat3/core`'s**, since 16.x — `locale.ts` is gone, because
`@ultimat3/money` needs the identical screen and tier 1 may not import sideways) is the one
screen and it VALIDATES AND CANONICALIZES in one
step — `Intl.getCanonicalLocales` runs the same structural check `supportedLocalesOf` throws on

@@ -64,3 +65,4 @@ and hands back the spelling the cache keys on, so there is no second question to ask. Well-formed

for a locale) and the cache must be bounded (`cachedFormatter`). **`cachedFormatter`,
`MAX_CACHED_FORMATTERS` and `canonicalLocale` are `@ultimat3/core`'s as of 2.0.0**, not this
`MAX_CACHED_FORMATTERS`, `canonicalLocale` and `assertLocale` are `@ultimat3/core`'s** — the
first three as of 2.0.0 and the screen as of 16.x, with `X_LOCALE_INVALID` moving with it — not this
package's: `@ultimat3/money` hit the identical unbounded-`Map`-on-a-header bug and tier 1 may not

@@ -147,2 +149,15 @@ import sideways, so the mechanism moved down a tier rather than being copied. An unbounded

- `m` is minutes, `ms` is milliseconds. A bare number is not a duration.
- **`toMs`'s NUMBER arm is screened, `As of 2026-08-26`** — `finiteOption('toMs', 'duration', …)`.
The string arm has always been total, because `parseDuration` refuses everything it cannot read;
the number arm passed straight through, so `toMs(Number(process.env.TTL_MS))` on an unset
variable answered `NaN` and every `wakeAt > now` built from it read false forever — a sleep that
never ends and a timeout that never fires, with no error anywhere. `??` does not guard it: `NaN`
is not nullish. `@ultimat3/notify`'s `toDurationMs` had screened its own copy of the body and
this one had not, so one duration vocabulary gave two answers to one input (#372);
`packages/notify/src/plan-bounds.test.ts` calls BOTH on the same inputs and is what keeps them
from drifting apart again.
**`finiteOption`, never `finiteCount`**: a duration here is legitimately NEGATIVE —
`parseDuration` accepts a leading `-`, and `toSeconds(-3000)` is a tested `-3` — and legitimately
fractional. A caller that needs whole non-negative milliseconds narrows on top; narrowing here
breaks the signed-duration contract `toSeconds` is built on.
- Tests must cover a spring-forward gap, a fall-back overlap and a non-hour offset zone.

@@ -149,0 +164,0 @@

+2
-2
{
"name": "@ultimat3/time",
"version": "16.0.0",
"version": "17.0.0",
"description": "UTC instants, DST-correct zone math, cron, durations and Intl formatting with an explicit timezone",

@@ -37,4 +37,4 @@ "license": "MIT",

"dependencies": {
"@ultimat3/core": "16.0.0"
"@ultimat3/core": "17.0.0"
}
}

@@ -157,3 +157,3 @@ # 🕒 @ultimat3/time

| `X_INSTANT_INVALID` | unparseable timestamp |
| `X_LOCALE_INVALID` | a tag `Intl` cannot parse (`en_US`, `''`) reached `describeCron` |
| `X_LOCALE_INVALID` | a tag `Intl` cannot parse (`en_US`, `''`) reached any entry point taking a `locale`. Declared by `@ultimat3/core` `As of 2026-08`, thrown here unchanged — `@ultimat3/time` owned it until then. It answers **400**, not 500: the `locale` stage negotiates `Accept-Language` and never throws, so a tag that reaches this code came from a path, query or action input the caller wrote |
| `X_CRON_NOT_DESCRIBABLE` | a valid 6-field cron whose seconds field `CronPhrases` has no words for |

@@ -160,0 +160,0 @@ | `X_SCHEDULE_INVALID` | a wall-clock field out of range: `slot.hour`, `slot.minute`, `slot.second`, `slot.weekday` |

@@ -7,6 +7,5 @@ /**

import { cachedFormatter } from '@ultimat3/core';
import { assertLocale, cachedFormatter } from '@ultimat3/core';
import { type CronExpression, parseCronOnce } from './cron-parse';
import { cronNotDescribable } from './errors';
import { assertLocale } from './locale';

@@ -13,0 +12,0 @@ export interface CronPhrases {

@@ -6,4 +6,4 @@ /**

import { assertLocale, finiteOption } from '@ultimat3/core';
import { durationInvalid, scheduleInvalid } from './errors';
import { assertLocale } from './locale';

@@ -65,5 +65,21 @@ export const MS = 1;

/** Parse-or-passthrough for APIs that accept either form. */
/**
* Parse-or-passthrough for APIs that accept either form.
*
* The STRING arm has always been total — `parseDuration` refuses everything it cannot read. The
* NUMBER arm was the hole: it passed straight through, so `toMs(Number(process.env.TTL_MS))` on an
* unset variable answered `NaN`, and every `wakeAt > now` built from it reads false forever — a
* sleep that never ends, a timeout that never fires, no error anywhere. `??` does not guard it,
* because `NaN` is not nullish. `@ultimat3/notify` screened its own copy of this body and this one
* did not, so one duration vocabulary gave two answers to one input.
*
* `finiteOption`, not `finiteCount`: a duration here is legitimately NEGATIVE — `parseDuration`
* accepts a leading `-` and `toSeconds(-3000)` is a tested `-3` — and legitimately fractional.
* A caller that needs whole non-negative milliseconds narrows on top of this, which is what
* `@ultimat3/notify`'s `toDurationMs` does; narrowing here would break both.
*/
export function toMs(duration: string | number): number {
return typeof duration === 'number' ? duration : parseDuration(duration);
return typeof duration === 'number'
? finiteOption('toMs', 'duration', duration)
: parseDuration(duration);
}

@@ -70,0 +86,0 @@

/**
* The X_* error codes owned by @ultimat3/time.
* DST ambiguity is a real state of the world, so it gets a code instead of a guess.
*
* `X_LOCALE_INVALID` is NOT here: it moved to `@ultimat3/core` beside `assertLocale`, because
* `@ultimat3/money` needs the same screen and tier 1 may not import sideways. A code has exactly
* one declaration — a second `registerErrorCodes` claim is `X_ERROR_CODE_DUPLICATE`.
*/

@@ -16,3 +20,2 @@

'X_SCHEDULE_INVALID',
'X_LOCALE_INVALID',
'X_CRON_NOT_DESCRIBABLE',

@@ -31,3 +34,2 @@ ] as const;

X_SCHEDULE_INVALID: 'a wall-clock field is out of range',
X_LOCALE_INVALID: 'not a well-formed BCP 47 tag',
X_CRON_NOT_DESCRIBABLE: 'a valid cron expression describeCron has no vocabulary for',

@@ -128,15 +130,2 @@ };

/**
* A tag `Intl` cannot parse. Distinct from i18n's `X_LOCALE_UNSUPPORTED`, which is a
* well-formed tag outside the app's supported set — this one is not a tag at all, and a raw
* `RangeError` from a formatter says nothing about which caller supplied it.
*/
export function localeInvalid(locale: string): TimeError {
return new TimeError({
code: 'X_LOCALE_INVALID',
cause: `"${locale}" is not a well-formed BCP 47 language tag`,
fix: "pass a tag like 'en', 'en-GB' or 'de-DE' — screen a header-supplied value with Intl.DateTimeFormat.supportedLocalesOf([tag]) before it reaches a formatter",
});
}
export function instantInvalid(input: string): TimeError {

@@ -143,0 +132,0 @@ return new TimeError({

@@ -7,5 +7,4 @@ /**

import { cachedFormatter } from '@ultimat3/core';
import { assertLocale, cachedFormatter } from '@ultimat3/core';
import { differenceMs, type Instant } from './instant';
import { assertLocale } from './locale';
import { isoDateInZone } from './zoned';

@@ -12,0 +11,0 @@ import { assertTimeZone, type TimeZone } from './zones';

/** Public surface of @ultimat3/time. Explicit exports only. */
// `localeInvalid` and the `X_LOCALE_INVALID` it carries are `@ultimat3/core`'s since 16.x —
// `@ultimat3/money` needs the same screen and `money -> time` is a sideways import. Re-exported
// rather than dropped so this package's public surface is unchanged, the same way
// `@ultimat3/action` and `@ultimat3/query` re-export core's client-flight names.
export { localeInvalid } from '@ultimat3/core';
export {

@@ -60,3 +65,2 @@ addBusinessDays,

instantInvalid,
localeInvalid,
TIME_ERROR_CODES,

@@ -63,0 +67,0 @@ TIME_ERROR_TITLES,

@@ -7,6 +7,5 @@ /**

import { cachedFormatter } from '@ultimat3/core';
import { assertLocale, cachedFormatter } from '@ultimat3/core';
import { timezoneInvalid } from './errors';
import type { Instant } from './instant';
import { assertLocale } from './locale';
import { canonicalTimeZone } from './zone-canonical';

@@ -13,0 +12,0 @@

// Single responsibility: the one place a caller-supplied BCP 47 tag is screened before it reaches
// an `Intl` constructor. One question, one answer (axiom 1) — `cron-describe.ts` refused a
// malformed tag from the start while seven sibling formatters handed the raw string to `Intl` and
// let a bare, uncoded `RangeError` escape several frames from the header it came out of.
import { canonicalLocale } from '@ultimat3/core';
import { localeInvalid } from './errors';
/**
* The canonical spelling of a well-formed tag, or `X_LOCALE_INVALID`.
*
* Validating and keying are one step: `Intl.getCanonicalLocales` runs exactly the structural check
* `Intl.DateTimeFormat.supportedLocalesOf` throws on — which is what `localeInvalid`'s `fix:` tells
* the caller to run — and unlike it hands back the spelling every formatter cache keys on, so
* `EN-us` and `en-US` cannot mint two entries for one locale.
*
* Well-formed but unknown to this runtime's ICU (`zz`) is NOT refused: `Intl` falls back for those,
* and a user carrying a locale the runtime has no data for must still get a rendered page.
*/
export function assertLocale(locale: string): string {
const tag = canonicalLocale(locale);
if (tag === undefined) throw localeInvalid(locale);
return tag;
}