sdxc

[ Language & values ]

@sdxc/duration

Typed duration strings converted to milliseconds or seconds

npm add @sdxc/duration
pnpm add @sdxc/duration
yarn add @sdxc/duration
bun add @sdxc/duration
Installs with
@sdxc/result
Used by
auth-saas, reader

Typed duration strings converted to milliseconds or seconds.

Installation

npm add @sdxc/duration

parse() answers a Result from @sdxc/result, which installs alongside this package.

Usage

Convert a duration

import { toMs, toSeconds } from "@sdxc/duration";

toMs("5 minutes"); // 300000
toMs("30s"); // 30000
toMs(1500); // 1500, a bare number is already milliseconds

toSeconds("1 hour"); // 3600

toMs("5 minuts"); // type error: "minuts" is not a unit

Accept a duration in your own API

Declare the parameter as DurationInput and convert once inside, so callers pass either form and your function names its own unit in one place.

import type { DurationInput } from "@sdxc/duration";

import { toSeconds } from "@sdxc/duration";

interface CacheOptions {
	ttl: DurationInput;
}

async function write(cache: KVNamespace, key: string, value: string, options: CacheOptions) {
	await cache.put(key, value, { expirationTtl: toSeconds(options.ttl) });
}

await write(cache, "greeting", "hello", { ttl: "1 day" });
await write(cache, "greeting", "hello", { ttl: 86_400_000 });

Parse text that arrives at runtime

Configuration values and form fields are plain strings, so the type cannot check them. parse() does, and reports the rejected text.

import { parse } from "@sdxc/duration";
import { isFailure } from "@sdxc/result";

let result = parse(process.env.SESSION_LIFETIME ?? "");

if (isFailure(result)) throw result.error; // Invalid duration: "7 dayz"

let lifetimeMs = result.data;

API

toMs(input: DurationInput): number

Converts a duration to milliseconds; a number passes through unchanged. Text that reaches it behind a cast returns NaN, so send untrusted text through parse().

toMs("5 minutes"); // same as 5 * 60 * 1000

toSeconds(input: DurationInput): number

Converts a duration to whole seconds, rounding to the nearest second with halves rounding up (1500 is 2, 1400 is 1). Anything under half a second is 0, so pass at least "1 second" where a TTL must be non-zero.

toSeconds("1 week"); // same as Math.round((7 * 24 * 60 * 60 * 1000) / 1000)

parse(text: string): Result<number, InvalidDurationError>

Parses text into milliseconds after trimming surrounding whitespace. It accepts exactly what DurationString accepts, plus a bare amount read as milliseconds ("900000").

InvalidDurationError

The error inside a failed parse(). text holds the rejected input verbatim, and message quotes it (Invalid duration: "7 dayz") so empty or whitespace-only values stay visible in logs.

Types

DurationString

A whole amount plus a unit: spelled out after one space ("5 minutes") or abbreviated with none ("5m").

UnitLong formsShortMilliseconds
Millisecondmillisecond, millisecondsms1
Secondsecond, secondss1000
Minuteminute, minutesm60000
Hourhour, hoursh3600000
Dayday, daysd86400000
Weekweek, weeksw604800000

Amounts may be 0 or negative ("-5 minutes"). Both the type and parse() reject fractions and exponents ("1.5h", "5e3s"), leading zeros and plus signs ("05s", "+5s"), the other form's spacing ("5 m", "5minutes"), uppercase units, compound durations ("1 hour 30 minutes"), and months and years, which have no fixed length.

DurationInput

number | DurationString, the type a duration-taking API declares. A number is always milliseconds.

DurationUnit

The long unit spellings, singular and plural.

DurationUnitShort

The short aliases: ms, s, m, h, d, w.

Pattern: One Value, Several Units

Take the duration once and convert at each boundary that consumes it, in the unit that boundary counts in.

import type { DurationInput } from "@sdxc/duration";

import { toMs, toSeconds } from "@sdxc/duration";

interface SessionOptions {
	lifetime: DurationInput;
}

function sessionCookie(id: string, options: SessionOptions) {
	let maxAge = toSeconds(options.lifetime);
	let expires = new Date(Date.now() + toMs(options.lifetime));
	return `session=${id}; Max-Age=${maxAge}; Expires=${expires.toUTCString()}; HttpOnly`;
}

sessionCookie("abc123", { lifetime: "30 days" });

Pattern: Validate Configuration at Startup

Parse every duration setting where it is read, so a typo fails by name at boot instead of becoming a NaN timer later.

import type { Result } from "@sdxc/result";

import { parse } from "@sdxc/duration";
import { failure, isFailure } from "@sdxc/result";

function readDuration(name: string, fallback: string): Result<number, Error> {
	let value = process.env[name] ?? fallback;
	let result = parse(value);
	if (isFailure(result))
		return failure(new Error(`${name} is not a duration: ${result.error.text}`));
	return result;
}

let lifetime = readDuration("SESSION_LIFETIME", "30 days");
let pollInterval = readDuration("POLL_INTERVAL", "15s");

Pattern: Strings in Code, Numbers in Storage

Write durations as strings in code and configuration, and store or send the number, so persisted data reads the same whatever grammar this package accepts.

import type { DurationInput } from "@sdxc/duration";

import { toMs } from "@sdxc/duration";

interface Reminder {
	message: string;
	dueAt: number;
}

function remindIn(message: string, delay: DurationInput): Reminder {
	return { message, dueAt: Date.now() + toMs(delay) };
}

let reminder = remindIn("Renew the certificate", "2 weeks");
await storage.put("reminder", JSON.stringify(reminder));

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.