From 847fb84eccef19c92c4bd9eb6ff0fe48d5d798c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adel=20Rodr=C3=ADguez?= Date: Wed, 18 Feb 2026 02:13:50 -0400 Subject: [PATCH] feat: replace custom duration utils with qte library --- apps/api/package.json | 1 + apps/api/src/shared/auth.ts | 7 +- apps/api/src/shared/middleware.ts | 6 +- apps/mobile/expo-env.d.ts | 2 +- bun.lock | 7 +- packages/backend/package.json | 1 + .../src/functions/shared/auth/options.ts | 7 +- packages/email/package.json | 1 + packages/email/src/client.ts | 8 +- packages/kv/package.json | 2 +- packages/kv/src/client.ts | 6 +- packages/utils/src/__tests__/duration.test.ts | 237 ----------------- packages/utils/src/constants/index.ts | 1 - packages/utils/src/constants/session.ts | 2 - packages/utils/src/duration.ts | 251 ------------------ 15 files changed, 29 insertions(+), 510 deletions(-) delete mode 100644 packages/utils/src/__tests__/duration.test.ts delete mode 100644 packages/utils/src/constants/session.ts delete mode 100644 packages/utils/src/duration.ts diff --git a/apps/api/package.json b/apps/api/package.json index 27241c9b..cbd722ab 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -33,6 +33,7 @@ "hono": "4.11.7", "hono-openapi": "^1.1.2", "hono-rate-limiter": "0.5.3", + "qte": "0.1.0", "std-env": "3.10.0", "superjson": "2.2.6" }, diff --git a/apps/api/src/shared/auth.ts b/apps/api/src/shared/auth.ts index 5bd66e52..316f3378 100644 --- a/apps/api/src/shared/auth.ts +++ b/apps/api/src/shared/auth.ts @@ -1,7 +1,8 @@ import { createAuth, databaseAdapter } from "@init/auth/server" import { admin, organization } from "@init/auth/server/plugins" import { database } from "@init/db/client" -import { APP_ID, APP_NAME, SESSION_EXPIRES_IN, SESSION_UPDATE_AGE } from "@init/utils/constants" +import { APP_ID, APP_NAME } from "@init/utils/constants" +import { seconds } from "qte" import env from "#shared/env.ts" export const auth = createAuth({ @@ -20,8 +21,8 @@ export const auth = createAuth({ plugins: [admin(), organization()], secret: env.AUTH_SECRET, session: { - expiresIn: SESSION_EXPIRES_IN, - updateAge: SESSION_UPDATE_AGE, + expiresIn: seconds("30d"), + updateAge: seconds("15d"), }, socialProviders: { github: { diff --git a/apps/api/src/shared/middleware.ts b/apps/api/src/shared/middleware.ts index eaf1cac3..7607d194 100644 --- a/apps/api/src/shared/middleware.ts +++ b/apps/api/src/shared/middleware.ts @@ -1,10 +1,10 @@ import type { DeepMerge } from "@init/utils/type" import { findIp } from "@arcjet/ip" import { kv } from "@init/kv/client" -import { type DurationInput, milliseconds } from "@init/utils/duration" import { rateLimiter } from "hono-rate-limiter" import { createMiddleware } from "hono/factory" import { HTTPException } from "hono/http-exception" +import { type TimeExpression, ms } from "qte" import type { Session } from "#shared/auth.ts" import type { AppContext } from "#shared/types.ts" @@ -32,12 +32,12 @@ export const requireSession = createMiddleware< /** * Adds basic rate limiting protection with a fixed window to the request. */ -export function withRateLimiting(interval: DurationInput, limit: number) { +export function withRateLimiting(interval: TimeExpression, limit: number) { return rateLimiter({ keyGenerator: (c) => c.var.session?.user.id ?? findIp(c.req.raw) ?? "unknown", limit, standardHeaders: "draft-7", - windowMs: milliseconds(interval), + windowMs: ms(interval), }) } diff --git a/apps/mobile/expo-env.d.ts b/apps/mobile/expo-env.d.ts index 5411fdde..bf3c1693 100644 --- a/apps/mobile/expo-env.d.ts +++ b/apps/mobile/expo-env.d.ts @@ -1,3 +1,3 @@ /// -// NOTE: This file should not be edited and should be in your git ignore \ No newline at end of file +// NOTE: This file should not be edited and should be in your git ignore diff --git a/bun.lock b/bun.lock index 5ae58213..1e452107 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,7 @@ "hono": "4.11.7", "hono-openapi": "^1.1.2", "hono-rate-limiter": "0.5.3", + "qte": "0.1.0", "std-env": "3.10.0", "superjson": "2.2.6", }, @@ -328,6 +329,7 @@ "@tanstack/react-query": "^5.90.20", "convex": "1.31.6", "convex-helpers": "0.1.111", + "qte": "0.1.0", "std-env": "3.10.0", }, "devDependencies": { @@ -372,6 +374,7 @@ "@react-email/components": "1.0.6", "@react-email/render": "2.0.4", "date-fns": "4.1.0", + "qte": "0.1.0", "resend": "6.7.0", }, "devDependencies": { @@ -409,8 +412,8 @@ "name": "@init/kv", "dependencies": { "@init/env": "workspace:*", - "@init/error": "workspace:*", "@init/utils": "workspace:*", + "qte": "0.1.0", "superjson": "2.2.6", }, "devDependencies": { @@ -4184,6 +4187,8 @@ "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + "qte": ["qte@0.1.0", "", { "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-9533onfcBoxlPloVxz1Ma2cpGH8cMJcPrMdhcNwTVENivLO33HDCXqMm0RWQzFTcVm8c9BN3khXT22e1Na0LiA=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], diff --git a/packages/backend/package.json b/packages/backend/package.json index 3f3ab647..66fa1fa9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -32,6 +32,7 @@ "@tanstack/react-query": "^5.90.20", "convex": "1.31.6", "convex-helpers": "0.1.111", + "qte": "0.1.0", "std-env": "3.10.0" }, "devDependencies": { diff --git a/packages/backend/src/functions/shared/auth/options.ts b/packages/backend/src/functions/shared/auth/options.ts index cb02e1f3..7d6012a3 100644 --- a/packages/backend/src/functions/shared/auth/options.ts +++ b/packages/backend/src/functions/shared/auth/options.ts @@ -6,7 +6,8 @@ import type { AuthOptions } from "@init/auth/server" import { createClient } from "@convex-dev/better-auth" import { convex } from "@convex-dev/better-auth/plugins" import { admin, anonymous, organization } from "@init/auth/server/plugins" -import { APP_ID, APP_NAME, SESSION_EXPIRES_IN, SESSION_UPDATE_AGE } from "@init/utils/constants" +import { APP_ID, APP_NAME } from "@init/utils/constants" +import { seconds } from "qte" import type { DataModel } from "#functions/_generated/dataModel.js" import { components } from "#functions/_generated/api.js" import authConfig from "#functions/auth.config.ts" @@ -30,7 +31,7 @@ export const authOptions = (ctx: GenericCtx) => }, plugins: [anonymous(), admin(), organization(), convex({ authConfig })], session: { - expiresIn: SESSION_EXPIRES_IN, - updateAge: SESSION_UPDATE_AGE, + expiresIn: seconds("30d"), + updateAge: seconds("15d"), }, }) satisfies AuthOptions diff --git a/packages/email/package.json b/packages/email/package.json index 3f501cae..ac53881f 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -22,6 +22,7 @@ "@react-email/components": "1.0.6", "@react-email/render": "2.0.4", "date-fns": "4.1.0", + "qte": "0.1.0", "resend": "6.7.0" }, "devDependencies": { diff --git a/packages/email/src/client.ts b/packages/email/src/client.ts index 771410c4..05b0b9c1 100644 --- a/packages/email/src/client.ts +++ b/packages/email/src/client.ts @@ -2,16 +2,16 @@ import type { ReactNode } from "react" import { resend } from "@init/env/presets" import { SendEmailError, BatchSendEmailError } from "@init/error" import { getLogger, LoggerCategory } from "@init/observability/logger" -import { type DurationInput, milliseconds } from "@init/utils/duration" import { singleton } from "@init/utils/singleton" import { render } from "@react-email/render" import { addMilliseconds } from "date-fns" +import { type TimeExpression, ms } from "qte" import { Resend } from "resend" type EmailSendParams = { emails: string[] subject: string - sendAt?: Date | DurationInput + sendAt?: Date | TimeExpression from?: string } @@ -47,7 +47,7 @@ export async function sendEmail(body: ReactNode, params: EmailSendParams) { ? undefined : sendAt instanceof Date ? sendAt.toISOString() - : addMilliseconds(new Date(), milliseconds(sendAt)).toISOString(), + : addMilliseconds(new Date(), ms(sendAt)).toISOString(), subject, to: emails, }) @@ -97,7 +97,7 @@ export async function batchEmails(payload: Array { + async set(key: string | KeyPart[], value: unknown, expiresIn?: TimeExpression): Promise { const normalizedKey = this.normalizeKey(key) await this.client.set(normalizedKey, SuperJSON.stringify(value)) diff --git a/packages/utils/src/__tests__/duration.test.ts b/packages/utils/src/__tests__/duration.test.ts deleted file mode 100644 index f2b18fdb..00000000 --- a/packages/utils/src/__tests__/duration.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { format, milliseconds, parse, parseStrict, seconds } from "#duration.ts" - -describe("parse", () => { - test("parses milliseconds", () => { - expect(parse("100ms")).toBe(100) - expect(parse("100 ms")).toBe(100) - expect(parse("100msec")).toBe(100) - expect(parse("100msecs")).toBe(100) - expect(parse("100millisecond")).toBe(100) - expect(parse("100milliseconds")).toBe(100) - }) - - test("parses seconds", () => { - expect(parse("1s")).toBe(1000) - expect(parse("1 s")).toBe(1000) - expect(parse("1sec")).toBe(1000) - expect(parse("1secs")).toBe(1000) - expect(parse("1second")).toBe(1000) - expect(parse("1seconds")).toBe(1000) - expect(parse("5s")).toBe(5000) - }) - - test("parses minutes", () => { - expect(parse("1m")).toBe(60_000) - expect(parse("1 m")).toBe(60_000) - expect(parse("1min")).toBe(60_000) - expect(parse("1mins")).toBe(60_000) - expect(parse("1minute")).toBe(60_000) - expect(parse("1minutes")).toBe(60_000) - expect(parse("2m")).toBe(120_000) - }) - - test("parses hours", () => { - expect(parse("1h")).toBe(3_600_000) - expect(parse("1 h")).toBe(3_600_000) - expect(parse("1hr")).toBe(3_600_000) - expect(parse("1hrs")).toBe(3_600_000) - expect(parse("1hour")).toBe(3_600_000) - expect(parse("1hours")).toBe(3_600_000) - expect(parse("2h")).toBe(7_200_000) - }) - - test("parses days", () => { - expect(parse("1d")).toBe(86_400_000) - expect(parse("1 d")).toBe(86_400_000) - expect(parse("1day")).toBe(86_400_000) - expect(parse("1days")).toBe(86_400_000) - expect(parse("2d")).toBe(172_800_000) - }) - - test("parses weeks", () => { - expect(parse("1w")).toBe(604_800_000) - expect(parse("1 w")).toBe(604_800_000) - expect(parse("1week")).toBe(604_800_000) - expect(parse("1weeks")).toBe(604_800_000) - expect(parse("2w")).toBe(1_209_600_000) - }) - - test("parses months", () => { - const oneMonth = (86_400_000 * 365.25) / 12 - expect(parse("1mo")).toBe(oneMonth) - expect(parse("1 mo")).toBe(oneMonth) - expect(parse("1month")).toBe(oneMonth) - expect(parse("1months")).toBe(oneMonth) - }) - - test("parses years", () => { - const oneYear = 86_400_000 * 365.25 - expect(parse("1y")).toBe(oneYear) - expect(parse("1 y")).toBe(oneYear) - expect(parse("1yr")).toBe(oneYear) - expect(parse("1yrs")).toBe(oneYear) - expect(parse("1year")).toBe(oneYear) - expect(parse("1years")).toBe(oneYear) - }) - - test("parses case-insensitive units", () => { - expect(parse("1H")).toBe(3_600_000) - expect(parse("1HOUR")).toBe(3_600_000) - expect(parse("1Hour")).toBe(3_600_000) - expect(parse("1MS")).toBe(1) - }) - - test("parses decimal values", () => { - expect(parse("1.5h")).toBe(5_400_000) - expect(parse("0.5d")).toBe(43_200_000) - expect(parse("2.5s")).toBe(2500) - }) - - test("parses negative values", () => { - expect(parse("-1s")).toBe(-1000) - expect(parse("-5m")).toBe(-300_000) - expect(parse("-100ms")).toBe(-100) - }) - - test("parses number-only strings as milliseconds", () => { - expect(parse("100")).toBe(100) - expect(parse("500")).toBe(500) - }) - - test("returns NaN for invalid strings", () => { - expect(parse("invalid")).toBeNaN() - expect(parse("abc123")).toBeNaN() - }) - - test("throws for empty string", () => { - expect(() => parse("")).toThrow() - }) - - test("throws for string exceeding 100 characters", () => { - expect(() => parse("a".repeat(101))).toThrow() - }) -}) - -describe("parseStrict", () => { - test("parses valid DurationInput values", () => { - expect(parseStrict("1h")).toBe(3_600_000) - expect(parseStrict("30m")).toBe(1_800_000) - expect(parseStrict("100ms")).toBe(100) - expect(parseStrict("1 hour")).toBe(3_600_000) - }) -}) - -describe("format", () => { - test("formats milliseconds (short)", () => { - expect(format(500)).toBe("500ms") - expect(format(0)).toBe("0ms") - expect(format(1)).toBe("1ms") - }) - - test("formats seconds (short)", () => { - expect(format(1000)).toBe("1s") - expect(format(5000)).toBe("5s") - expect(format(1500)).toBe("2s") - }) - - test("formats minutes (short)", () => { - expect(format(60_000)).toBe("1m") - expect(format(120_000)).toBe("2m") - expect(format(90_000)).toBe("2m") - }) - - test("formats hours (short)", () => { - expect(format(3_600_000)).toBe("1h") - expect(format(7_200_000)).toBe("2h") - }) - - test("formats days (short)", () => { - expect(format(86_400_000)).toBe("1d") - expect(format(172_800_000)).toBe("2d") - }) - - test("formats weeks (short)", () => { - expect(format(604_800_000)).toBe("1w") - expect(format(1_209_600_000)).toBe("2w") - }) - - test("formats months (short)", () => { - const oneMonth = (86_400_000 * 365.25) / 12 - expect(format(oneMonth)).toBe("1mo") - expect(format(oneMonth * 2)).toBe("2mo") - }) - - test("formats years (short)", () => { - const oneYear = 86_400_000 * 365.25 - expect(format(oneYear)).toBe("1y") - expect(format(oneYear * 2)).toBe("2y") - }) - - test("formats with long option", () => { - expect(format(1000, { long: true })).toBe("1 second") - expect(format(2000, { long: true })).toBe("2 seconds") - expect(format(60_000, { long: true })).toBe("1 minute") - expect(format(120_000, { long: true })).toBe("2 minutes") - expect(format(3_600_000, { long: true })).toBe("1 hour") - expect(format(7_200_000, { long: true })).toBe("2 hours") - expect(format(86_400_000, { long: true })).toBe("1 day") - expect(format(172_800_000, { long: true })).toBe("2 days") - }) - - test("formats negative values", () => { - expect(format(-1000)).toBe("-1s") - expect(format(-60_000)).toBe("-1m") - }) - - test("throws for non-number input", () => { - // @ts-expect-error testing invalid input - expect(() => format("100")).toThrow() - }) - - test("throws for non-finite numbers", () => { - expect(() => format(Number.POSITIVE_INFINITY)).toThrow() - expect(() => format(Number.NaN)).toThrow() - }) -}) - -describe("milliseconds", () => { - test("parses string to milliseconds", () => { - expect(milliseconds("1s")).toBe(1000) - expect(milliseconds("1m")).toBe(60_000) - expect(milliseconds("1h")).toBe(3_600_000) - expect(milliseconds("1d")).toBe(86_400_000) - }) - - test("formats number to string", () => { - expect(milliseconds(1000)).toBe("1s") - expect(milliseconds(60_000)).toBe("1m") - expect(milliseconds(3_600_000)).toBe("1h") - }) - - test("accepts options for formatting", () => { - expect(milliseconds(1000, { long: true })).toBe("1 second") - expect(milliseconds(60_000, { long: true })).toBe("1 minute") - }) -}) - -describe("seconds", () => { - test("parses string to seconds", () => { - expect(seconds("1s")).toBe(1) - expect(seconds("1m")).toBe(60) - expect(seconds("1h")).toBe(3600) - expect(seconds("1d")).toBe(86_400) - expect(seconds("500ms")).toBe(0.5) - }) - - test("formats seconds to string", () => { - expect(seconds(1)).toBe("1s") - expect(seconds(60)).toBe("1m") - expect(seconds(3600)).toBe("1h") - }) - - test("accepts options for formatting", () => { - expect(seconds(1, { long: true })).toBe("1 second") - expect(seconds(60, { long: true })).toBe("1 minute") - }) -}) diff --git a/packages/utils/src/constants/index.ts b/packages/utils/src/constants/index.ts index 7586ef8f..60c2b146 100644 --- a/packages/utils/src/constants/index.ts +++ b/packages/utils/src/constants/index.ts @@ -1,4 +1,3 @@ export * from "#constants/app.ts" export * from "#constants/asset.ts" export * from "#constants/breakpoint.ts" -export * from "#constants/session.ts" diff --git a/packages/utils/src/constants/session.ts b/packages/utils/src/constants/session.ts deleted file mode 100644 index 92c586e4..00000000 --- a/packages/utils/src/constants/session.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const SESSION_EXPIRES_IN = 60 * 60 * 24 * 30 // 30 days -export const SESSION_UPDATE_AGE = 60 * 60 * 24 * 15 // 15 days diff --git a/packages/utils/src/duration.ts b/packages/utils/src/duration.ts deleted file mode 100644 index 623941a9..00000000 --- a/packages/utils/src/duration.ts +++ /dev/null @@ -1,251 +0,0 @@ -// Taken from: https://github.com/vercel/ms/blob/main/src/index.ts -import { InvalidDurationFormatInputError, InvalidDurationParseInputError } from "@init/error" -import { assertUnreachable } from "#assert.ts" - -const s = 1000 -const m = s * 60 -const h = m * 60 -const d = h * 24 -const w = d * 7 -const y = d * 365.25 -const mo = y / 12 - -type Years = "years" | "year" | "yrs" | "yr" | "y" -type Months = "months" | "month" | "mo" -type Weeks = "weeks" | "week" | "w" -type Days = "days" | "day" | "d" -type Hours = "hours" | "hour" | "hrs" | "hr" | "h" -type Minutes = "minutes" | "minute" | "mins" | "min" | "m" -type Seconds = "seconds" | "second" | "secs" | "sec" | "s" -type Milliseconds = "milliseconds" | "millisecond" | "msecs" | "msec" | "ms" -type Unit = Years | Months | Weeks | Days | Hours | Minutes | Seconds | Milliseconds - -type UnitAnyCase = Capitalize | Uppercase | Unit - -export type DurationInput = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}` - -interface Options { - /** - * Set to `true` to use verbose formatting. Defaults to `false`. - */ - long?: boolean -} - -/** - * Parse or format the given value. - * - * @param value - The string or number to convert - * @param options - Options for the conversion - * @throws Error if `value` is not a non-empty string or a number - */ -export function milliseconds(value: DurationInput, options?: Options): number -export function milliseconds(value: number, options?: Options): string -export function milliseconds(value: DurationInput | number, options?: Options): number | string { - if (typeof value === "string") { - return parse(value) - } - - if (typeof value === "number") { - return format(value, options) - } - - assertUnreachable(value) -} - -/** - * Parse or format the given value in seconds. - * - * @param value - The string or number to convert - * @param options - Options for the conversion - * @throws Error if `value` is not a non-empty string or a number - */ -export function seconds(value: DurationInput, options?: Options): number -export function seconds(value: number, options?: Options): string -export function seconds(value: DurationInput | number, options?: Options): number | string { - if (typeof value === "string") { - return parse(value) / 1000 - } - - if (typeof value === "number") { - return format(value * 1000, options) - } - - assertUnreachable(value) -} - -/** - * Parse the given string and return milliseconds. - * - * @param str - A string to parse to milliseconds - * @returns The parsed value in milliseconds, or `NaN` if the string can't be - * parsed - */ -export function parse(str: string): number { - if (typeof str !== "string") { - throw new InvalidDurationFormatInputError() - } - - const length = str.length - - if (length === 0 || length > 100) { - throw new InvalidDurationParseInputError({ value: str }) - } - - const match = - /^(?-?\d*\.?\d+) *(?milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mo|years?|yrs?|y)?$/i.exec( - str - ) - - if (!match?.groups) { - return Number.NaN - } - - const { value, unit = "ms" } = match.groups - - if (!value || !unit) { - return Number.NaN - } - - const n = Number.parseFloat(value) - - const matchUnit = unit.toLowerCase() as Lowercase - - switch (matchUnit) { - case "years": - case "year": - case "yrs": - case "yr": - case "y": - return n * y - case "months": - case "month": - case "mo": - return n * mo - case "weeks": - case "week": - case "w": - return n * w - case "days": - case "day": - case "d": - return n * d - case "hours": - case "hour": - case "hrs": - case "hr": - case "h": - return n * h - case "minutes": - case "minute": - case "mins": - case "min": - case "m": - return n * m - case "seconds": - case "second": - case "secs": - case "sec": - case "s": - return n * s - case "milliseconds": - case "millisecond": - case "msecs": - case "msec": - case "ms": - return n - default: - assertUnreachable(matchUnit) - } -} - -/** - * Parse the given DurationInput and return milliseconds. - * - * @param value - A typesafe DurationInput to parse to milliseconds - * @returns The parsed value in milliseconds, or `NaN` if the string can't be - * parsed - */ -export function parseStrict(value: DurationInput): number { - return parse(value) -} - -/** - * Short format for `ms`. - */ -function fmtShort(ms: number): DurationInput { - const msAbs = Math.abs(ms) - if (msAbs >= y) { - return `${Math.round(ms / y)}y` - } - if (msAbs >= mo) { - return `${Math.round(ms / mo)}mo` - } - if (msAbs >= w) { - return `${Math.round(ms / w)}w` - } - if (msAbs >= d) { - return `${Math.round(ms / d)}d` - } - if (msAbs >= h) { - return `${Math.round(ms / h)}h` - } - if (msAbs >= m) { - return `${Math.round(ms / m)}m` - } - if (msAbs >= s) { - return `${Math.round(ms / s)}s` - } - return `${ms}ms` -} - -/** - * Long format for `ms`. - */ -function fmtLong(ms: number): DurationInput { - const msAbs = Math.abs(ms) - if (msAbs >= y) { - return plural(ms, msAbs, y, "year") - } - if (msAbs >= mo) { - return plural(ms, msAbs, mo, "month") - } - if (msAbs >= w) { - return plural(ms, msAbs, w, "week") - } - if (msAbs >= d) { - return plural(ms, msAbs, d, "day") - } - if (msAbs >= h) { - return plural(ms, msAbs, h, "hour") - } - if (msAbs >= m) { - return plural(ms, msAbs, m, "minute") - } - if (msAbs >= s) { - return plural(ms, msAbs, s, "second") - } - return `${ms} ms` -} - -/** - * Format the given integer as a string. - * - * @param ms - milliseconds - * @param options - Options for the conversion - * @returns The formatted string - */ -export function format(ms: number, options?: Options): string { - if (typeof ms !== "number" || !Number.isFinite(ms)) { - throw new InvalidDurationFormatInputError() - } - - return options?.long ? fmtLong(ms) : fmtShort(ms) -} - -/** - * Pluralization helper. - */ -function plural(ms: number, msAbs: number, n: number, name: string): DurationInput { - const isPlural = msAbs >= n * 1.5 - return `${Math.round(ms / n)} ${name}${isPlural ? "s" : ""}` as DurationInput -}