Functional Weave
Code in Python

http.retry-after@1.0.0

impl/typescript.ts

2,768 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { isLeapYear } from "./dates_is_leap_year.ts";  ← from dates.is-leap-year ^1.0.0 · built alongside by fune
import { isoToUnix } from "./time_iso_to_unix.ts";  ← from time.iso-to-unix ^1.0.0 · built alongside by fune

const DAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// Nine digits is 31 years; anything longer is a broken header, not a wait.
const MAX_DIGITS = 9;

function allDigits(text: string): boolean {
  if (text.length === 0) return false;
  for (let i = 0; i < text.length; i++) {
    const c = text.charCodeAt(i);
    if (c < 48 || c > 57) return false;
  }
  return true;
}

function trimOws(text: string): string {
  // HTTP's optional whitespace is space and tab only (RFC 9110 5.6.3).
  let start = 0;
  let end = text.length;
  while (start < end && (text[start] === " " || text[start] === "\t")) start++;
  while (end > start && (text[end - 1] === " " || text[end - 1] === "\t")) end--;
  return text.slice(start, end);
}

/** An IMF-fixdate, "Sun, 06 Nov 1994 08:49:37 GMT", to Unix seconds; null if it is not one. */
function imfFixdate(t: string): number | null {
  if (t.length !== 29) return null;
  if (!DAY_NAMES.includes(t.slice(0, 3)) || t.slice(3, 5) !== ", " || t[7] !== " " || t[11] !== " " || t[16] !== " ") return null;
  if (t[19] !== ":" || t[22] !== ":" || t.slice(25) !== " GMT") return null;
  const month = MONTHS.indexOf(t.slice(8, 11));
  const dd = t.slice(5, 7), yyyy = t.slice(12, 16), hh = t.slice(17, 19), mi = t.slice(20, 22), ss = t.slice(23, 25);
  if (month < 0 || ![dd, yyyy, hh, mi, ss].every(allDigits)) return null;
  const year = Number(yyyy), day = Number(dd);
  if (year < 1 || Number(hh) > 23 || Number(mi) > 59 || Number(ss) > 59) return null;
  const monthDays = month === 1 && isLeapYear(year) ? 29 : MONTH_DAYS[month];
  if (day < 1 || day > monthDays) return null;
  const mm = String(month + 1).padStart(2, "0");
  return isoToUnix(`${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}Z`);
}

/**
 * How long a client should wait before retrying, from a Retry-After header
 * (RFC 9110 10.2.3): delay-seconds, or an HTTP date turned into seconds from
 * `now`. A date in the past is 0. Anything else is null, so the caller picks
 * its own backoff instead of trusting a garbled header.
 */
export function retryAfterSeconds(header: string | null, now: number): number | null {
  if (typeof now !== "number" || !Number.isSafeInteger(now)) throw new RangeError("now must be a whole number of seconds");
  if (typeof header !== "string") return null;
  const t = trimOws(header);
  if (allDigits(t)) return t.length <= MAX_DIGITS ? Number(t) : null;
  const at = imfFixdate(t);
  if (at === null) return null;
  return at > now ? at - now : 0;
}