@softure-ai/core 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done"). Versions before the first one below are described in their GitHub Releases (`core@x.y.z`).
6
6
 
7
+ ## 0.2.3
8
+
9
+ - `requestJson` reads response headers (#652), with no breaking change to calls that do not use the new option.
10
+ - `readHeaders: readonly string[]`: response headers to return, by name (any letter case), an explicit allow-list.
11
+ With it the value is `{ body, headers }` (each requested name a key, `null` when absent) and a failure that had a
12
+ response carries `headers`. A name that is not a valid header name throws a `RangeError` before the request.
13
+ - `retryAfterSeconds: number | null` on every `core.http_rate_limited` and `core.http_unavailable` failure, parsed
14
+ from `Retry-After` (seconds, or an HTTP date measured against the new `clock` option), 0 to 86 400.
15
+ `HttpJsonFailure` is now a union narrowed by `error`, and a rate limited or unavailable failure's `status` is a
16
+ `number`.
17
+ - `parseLinkHeader(value)` returns the `next`, `prev`, `first` and `last` URLs of a `Link` header.
18
+ - New types `HttpJsonAnswer`, `HttpJsonResponseHeaders`, `HttpJsonRetryableCode`, `LinkHeaderLinks`,
19
+ `RequestJsonReadHeadersOptions`.
20
+ - Migrate: nothing is required. A test that deep-compares a 429 or 5xx failure adds `retryAfterSeconds: null` (or
21
+ the parsed value). An app that dropped to raw `fetch` for `Retry-After`, a `Link` header or rate-limit headers
22
+ goes back to `requestJson` with `readHeaders`, and passes `retryAfterSeconds` to `retryJobLater`.
23
+
7
24
  ## 0.2.2
8
25
 
9
26
  - `requestJson` writes (#635): `method` (`"GET"` by default, `"POST" | "PUT" | "PATCH" | "DELETE"`), `body` sent as
package/README.md CHANGED
@@ -305,7 +305,7 @@ if (plan.endedAt !== null) throw new PublicError("This plan has ended; pick anot
305
305
  `isPublicError` recognises one from another copy of core too (a registry symbol, not `instanceof`).
306
306
  `safeError` itself still answers `core.unexpected` for it, since callers switch on its two codes.
307
307
 
308
- **External JSON APIs.** `requestJson({ url, method?, body?, idempotencyKey?, fetch?, headers?, timeoutMs?, schema? })`
308
+ **External JSON APIs.** `requestJson({ url, method?, body?, idempotencyKey?, fetch?, headers?, timeoutMs?, schema?, readHeaders?, clock? })`
309
309
  calls a third-party JSON API (a GET by default) and returns `Ok<value>` or an `HttpJsonFailure` (`{ ok: false, error, status }`). It sets the details a hand-written
310
310
  helper tends to miss:
311
311
 
@@ -351,6 +351,35 @@ throws before anything is sent. `idempotencyKey` is sent as `Idempotency-Key`. A
351
351
  (a 204, or a 200 with nothing) is `ok(undefined)`, schema or not, so a write's value is `T | undefined`; a non-empty
352
352
  answer still goes through the schema. A GET carries no body, and an empty answer to it stays an unexpected answer.
353
353
 
354
+ **Response headers.** A failure with `core.http_rate_limited` (429) or `core.http_unavailable` (5xx) always carries
355
+ `retryAfterSeconds`: the `Retry-After` header as seconds (delay-seconds, or an HTTP date measured against `clock`,
356
+ default `systemClock`, rounded up), between 0 and 86 400, or `null` when the header is missing or malformed. It goes
357
+ straight into `retryJobLater` of `@softure-ai/jobs`. Narrowing on `error` types it; other failures do not carry it.
358
+
359
+ Any other header is read only when named in `readHeaders` (any letter case; an explicit allow-list, so nothing secret
360
+ leaves by accident). The value then becomes `{ body, headers }`, with each requested name as a key and `null` for a
361
+ header the answer lacks; a failure that had a response carries the same `headers`. Without `readHeaders` the value is
362
+ the body, as before. A name that is not a valid header name throws a `RangeError` before anything is sent.
363
+ `parseLinkHeader(value)` reads a `Link` header (RFC 8288) into `{ next?, prev?, first?, last? }`, the URLs as
364
+ written (`previous` counts as `prev`; the first link of a relation wins; `null` or garbage gives `{}`):
365
+
366
+ ```ts
367
+ import { parseLinkHeader, requestJson } from "@softure-ai/core";
368
+
369
+ const page = await requestJson({
370
+ url,
371
+ headers: { Authorization: `Bearer ${env.REPORTS_API_KEY}` },
372
+ schema: z.array(rowSchema),
373
+ readHeaders: ["link", "x-ratelimit-remaining"],
374
+ });
375
+ if (!page.ok) {
376
+ if (page.error === "core.http_rate_limited") return retryJobLater("app.reports_busy", page.retryAfterSeconds ?? 60);
377
+ return page;
378
+ }
379
+ const { next } = parseLinkHeader(page.value.headers.link);
380
+ // page.value.body: the rows; next: the URL of the next page, or undefined on the last one
381
+ ```
382
+
354
383
  ```ts
355
384
  const saved = await requestJson({
356
385
  url: `https://api.example.com/tasks/${task.id}`,
@@ -1,9 +1,28 @@
1
1
  import type { z } from "zod";
2
+ import { type Clock } from "./clock.js";
2
3
  import { type Err, type Ok } from "./result.js";
3
4
  export type HttpJsonFailureCode = "core.http_unreachable" | "core.http_refused" | "core.http_rate_limited" | "core.http_unavailable" | "core.http_conflict" | "core.http_unexpected_answer";
4
- /** A failed `requestJson`: the code and the HTTP status (`null` when no response arrived). Nothing else. */
5
- export type HttpJsonFailure = Err<HttpJsonFailureCode> & {
5
+ /** The failure codes whose answer may say when to try again (`Retry-After`). */
6
+ export type HttpJsonRetryableCode = "core.http_rate_limited" | "core.http_unavailable";
7
+ /** The headers named in `readHeaders`, keyed as the caller wrote them; `null` when the answer does not carry one. */
8
+ export type HttpJsonResponseHeaders<H extends string = string> = Readonly<Record<H, string | null>>;
9
+ /**
10
+ * A failed `requestJson`: the code and the HTTP status (`null` when no response arrived). A rate limited or
11
+ * unavailable answer adds `retryAfterSeconds`, and a response read with `readHeaders` adds `headers`. Nothing else.
12
+ */
13
+ export type HttpJsonFailure = (Err<Exclude<HttpJsonFailureCode, HttpJsonRetryableCode>> & {
6
14
  readonly status: number | null;
15
+ readonly headers?: HttpJsonResponseHeaders;
16
+ }) | (Err<HttpJsonRetryableCode> & {
17
+ readonly status: number;
18
+ /** Parsed from `Retry-After` (seconds or an HTTP date), 0 to 86 400; `null` when missing or malformed. */
19
+ readonly retryAfterSeconds: number | null;
20
+ readonly headers?: HttpJsonResponseHeaders;
21
+ });
22
+ /** The value of a call made with `readHeaders`: the body as without it, and the requested headers. */
23
+ export type HttpJsonAnswer<T, H extends string> = {
24
+ readonly body: T;
25
+ readonly headers: HttpJsonResponseHeaders<H>;
7
26
  };
8
27
  export type HttpJsonWriteMethod = "POST" | "PUT" | "PATCH" | "DELETE";
9
28
  export type HttpJsonMethod = "GET" | HttpJsonWriteMethod;
@@ -15,6 +34,16 @@ export interface RequestJsonOptions {
15
34
  readonly headers?: HeadersInit;
16
35
  /** Milliseconds before the request (body included) gives up. Defaults to 10 000. */
17
36
  readonly timeoutMs?: number;
37
+ /** Measures a `Retry-After` HTTP date. Defaults to `systemClock`. */
38
+ readonly clock?: Clock;
39
+ }
40
+ /** Response headers to return, by name (any letter case). Only these are read. */
41
+ export interface RequestJsonReadHeadersOptions<H extends string> {
42
+ readonly readHeaders: readonly H[];
43
+ }
44
+ /** The calls without `readHeaders`, whose value is the body itself. */
45
+ interface WithoutReadHeaders {
46
+ readonly readHeaders?: undefined;
18
47
  }
19
48
  export interface RequestJsonSchemaOptions<S extends z.ZodType> extends RequestJsonOptions {
20
49
  readonly schema: S;
@@ -38,13 +67,31 @@ export interface RequestJsonWriteOptions extends RequestJsonOptions {
38
67
  * for the caller to narrow; with one it is the schema's output. A write answered with an empty body (a 204) is
39
68
  * `ok(undefined)`, schema or not. 3xx, 401 and 403 are refused, 409 and 412 a conflict, 429 rate limited, 5xx
40
69
  * unavailable, and any other non-2xx status or a body that is not JSON or fails the schema an unexpected answer.
70
+ * With `readHeaders` the value is `{ body, headers }`, and a failure that had a response carries `headers` too.
71
+ */
72
+ export declare function requestJson<S extends z.ZodType, const H extends string>(options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions & RequestJsonReadHeadersOptions<H>): Promise<Ok<HttpJsonAnswer<z.output<S>, H>> | HttpJsonFailure>;
73
+ export declare function requestJson<S extends z.ZodType, const H extends string>(options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions & RequestJsonReadHeadersOptions<H>): Promise<Ok<HttpJsonAnswer<z.output<S> | undefined, H>> | HttpJsonFailure>;
74
+ export declare function requestJson<const H extends string>(options: (RequestJsonReadOptions | RequestJsonWriteOptions) & RequestJsonReadHeadersOptions<H>): Promise<Ok<HttpJsonAnswer<unknown, H>> | HttpJsonFailure>;
75
+ export declare function requestJson<S extends z.ZodType>(options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions & WithoutReadHeaders): Promise<Ok<z.output<S>> | HttpJsonFailure>;
76
+ export declare function requestJson<S extends z.ZodType>(options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions & WithoutReadHeaders): Promise<Ok<z.output<S> | undefined> | HttpJsonFailure>;
77
+ export declare function requestJson(options: (RequestJsonReadOptions | RequestJsonWriteOptions) & WithoutReadHeaders): Promise<Ok<unknown> | HttpJsonFailure>;
78
+ /** The URLs of the pagination relations in a `Link` header (RFC 8288), as written; missing ones are left out. */
79
+ export interface LinkHeaderLinks {
80
+ readonly next?: string;
81
+ readonly prev?: string;
82
+ readonly first?: string;
83
+ readonly last?: string;
84
+ }
85
+ /**
86
+ * Read `next`, `prev` (or `previous`), `first` and `last` from a `Link` header such as
87
+ * `<https://api.example.com/items?page=2>; rel="next"`. The first link of a relation wins; `null`, an empty value or
88
+ * one that cannot be read gives `{}`.
41
89
  */
42
- export declare function requestJson<S extends z.ZodType>(options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions): Promise<Ok<z.output<S>> | HttpJsonFailure>;
43
- export declare function requestJson<S extends z.ZodType>(options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions): Promise<Ok<z.output<S> | undefined> | HttpJsonFailure>;
44
- export declare function requestJson(options: RequestJsonReadOptions | RequestJsonWriteOptions): Promise<Ok<unknown> | HttpJsonFailure>;
90
+ export declare function parseLinkHeader(value: string | null | undefined): LinkHeaderLinks;
45
91
  /**
46
92
  * The query part of a URL (without `?`) from a flat record, in insertion order. `null` and `undefined` are left
47
93
  * out, so optional parameters can be passed as they are.
48
94
  */
49
95
  export declare function toQueryString(params: Readonly<Record<string, string | number | boolean | null | undefined>>): string;
96
+ export {};
50
97
  //# sourceMappingURL=http-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-json.d.ts","sourceRoot":"","sources":["../src/http-json.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,mBAAmB,GAC3B,uBAAuB,GACvB,mBAAmB,GACnB,wBAAwB,GACxB,uBAAuB,GACvB,oBAAoB,GACpB,6BAA6B,CAAC;AAElC,4GAA4G;AAC5G,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC,mBAAmB,CAAC,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE5F,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,mBAAmB,CAAC;AAEzD,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE1F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAE,SAAQ,kBAAkB;IACvF,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;CACpB;AAED,wEAAwE;AACxE,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC;CACjC;AAED,wFAAwF;AACxF,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IACjE,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,yGAAyG;IACzG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAC7C,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG,sBAAsB,GAC5D,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC;AAC9C,wBAAgB,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAC7C,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG,uBAAuB,GAC7D,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,eAAe,CAAC,CAAC;AAC1D,wBAAgB,WAAW,CACzB,OAAO,EAAE,sBAAsB,GAAG,uBAAuB,GACxD,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC;AAsE1C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,GAAG,MAAM,CAQpH"}
1
+ {"version":3,"file":"http-json.d.ts","sourceRoot":"","sources":["../src/http-json.ts"],"names":[],"mappings":"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"}
package/dist/http-json.js CHANGED
@@ -1,6 +1,12 @@
1
+ import { systemClock } from "./clock.js";
1
2
  import { err, ok } from "./result.js";
2
3
  const DEFAULT_TIMEOUT_MS = 10_000;
4
+ const MAX_RETRY_AFTER_SECONDS = 86_400;
5
+ // An RFC 9110 token: what a header name may be made of.
6
+ const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
3
7
  export async function requestJson(options) {
8
+ // Before anything is sent: a write must not go out when its answer cannot be read.
9
+ assertHeaderNames(options.readHeaders);
4
10
  const fetchFunction = options.fetch ?? globalThis.fetch;
5
11
  const method = options.method ?? "GET";
6
12
  const isWrite = method !== "GET";
@@ -31,11 +37,14 @@ export async function requestJson(options) {
31
37
  // Network error or timeout. The error's text can quote the URL, so it is dropped on purpose.
32
38
  return httpFailure("core.http_unreachable", null);
33
39
  }
34
- const statusFailure = getStatusFailure(response);
35
- if (statusFailure !== null) {
40
+ const headers = options.readHeaders === undefined ? undefined : readResponseHeaders(response, options.readHeaders);
41
+ const fail = (code) => withHeaders(responseFailure(code, response, options.clock ?? systemClock), headers);
42
+ const succeed = (body) => ok(headers === undefined ? body : { body, headers });
43
+ const statusFailureCode = getStatusFailureCode(response);
44
+ if (statusFailureCode !== null) {
36
45
  // Release the connection; a body that cannot be cancelled changes nothing for the caller.
37
46
  await response.body?.cancel().catch(() => undefined);
38
- return statusFailure;
47
+ return fail(statusFailureCode);
39
48
  }
40
49
  let text;
41
50
  try {
@@ -43,25 +52,69 @@ export async function requestJson(options) {
43
52
  }
44
53
  catch {
45
54
  // The body broke off, usually the timeout firing mid-body.
46
- return httpFailure("core.http_unreachable", response.status);
55
+ return fail("core.http_unreachable");
47
56
  }
48
57
  if (isWrite && text.trim() === "") {
49
58
  // A 204, or a write that answers 200 with nothing: the write succeeded and there is nothing to read.
50
- return ok(undefined);
59
+ return succeed(undefined);
51
60
  }
52
61
  let answer;
53
62
  try {
54
63
  answer = JSON.parse(text);
55
64
  }
56
65
  catch {
57
- return httpFailure("core.http_unexpected_answer", response.status);
66
+ return fail("core.http_unexpected_answer");
58
67
  }
59
68
  if (options.schema === undefined) {
60
- return ok(answer);
69
+ return succeed(answer);
61
70
  }
62
71
  // The schema's issues can quote the body's values, so only the code leaves.
63
72
  const parsed = options.schema.safeParse(answer);
64
- return parsed.success ? ok(parsed.data) : httpFailure("core.http_unexpected_answer", response.status);
73
+ return parsed.success ? succeed(parsed.data) : fail("core.http_unexpected_answer");
74
+ }
75
+ const REL_PATTERN = /(?:^|;)\s*rel\s*=\s*(?:"([^"]*)"|([^\s;,"]+))/i;
76
+ const LINK_RELATIONS = {
77
+ next: "next",
78
+ prev: "prev",
79
+ previous: "prev",
80
+ first: "first",
81
+ last: "last",
82
+ };
83
+ /**
84
+ * Read `next`, `prev` (or `previous`), `first` and `last` from a `Link` header such as
85
+ * `<https://api.example.com/items?page=2>; rel="next"`. The first link of a relation wins; `null`, an empty value or
86
+ * one that cannot be read gives `{}`.
87
+ */
88
+ export function parseLinkHeader(value) {
89
+ const links = {};
90
+ if (value === null || value === undefined) {
91
+ return links;
92
+ }
93
+ // One link: the URL in angle brackets and its parameters up to the next `<`. Splitting by commas would break a URL
94
+ // that contains one, and a regular expression would rescan the rest of a hostile value from every `<`.
95
+ let position = 0;
96
+ while (position < value.length) {
97
+ const open = value.indexOf("<", position);
98
+ const close = open === -1 ? -1 : value.indexOf(">", open + 1);
99
+ if (close === -1) {
100
+ break;
101
+ }
102
+ const nextOpen = value.indexOf("<", close + 1);
103
+ position = nextOpen === -1 ? value.length : nextOpen;
104
+ const url = value.slice(open + 1, close).trim();
105
+ const rel = REL_PATTERN.exec(value.slice(close + 1, position));
106
+ const relationTypes = rel?.[1] ?? rel?.[2];
107
+ if (url === "" || relationTypes === undefined) {
108
+ continue;
109
+ }
110
+ for (const relationType of relationTypes.trim().toLowerCase().split(/\s+/)) {
111
+ const relation = LINK_RELATIONS[relationType];
112
+ if (relation !== undefined && links[relation] === undefined) {
113
+ links[relation] = url;
114
+ }
115
+ }
116
+ }
117
+ return links;
65
118
  }
66
119
  /**
67
120
  * The query part of a URL (without `?`) from a flat record, in insertion order. `null` and `undefined` are left
@@ -76,26 +129,67 @@ export function toQueryString(params) {
76
129
  }
77
130
  return search.toString();
78
131
  }
79
- function getStatusFailure(response) {
132
+ function getStatusFailureCode(response) {
80
133
  const { status } = response;
81
134
  if (response.type === "opaqueredirect" || (status >= 300 && status < 400) || status === 401 || status === 403) {
82
- return httpFailure("core.http_refused", status);
135
+ return "core.http_refused";
83
136
  }
84
137
  if (status === 409 || status === 412) {
85
- return httpFailure("core.http_conflict", status);
138
+ return "core.http_conflict";
86
139
  }
87
140
  if (status === 429) {
88
- return httpFailure("core.http_rate_limited", status);
141
+ return "core.http_rate_limited";
89
142
  }
90
143
  if (status >= 500) {
91
- return httpFailure("core.http_unavailable", status);
144
+ return "core.http_unavailable";
92
145
  }
93
146
  if (status < 200 || status >= 300) {
94
- return httpFailure("core.http_unexpected_answer", status);
147
+ return "core.http_unexpected_answer";
95
148
  }
96
149
  return null;
97
150
  }
98
151
  function httpFailure(code, status) {
99
152
  return { ...err(code), status };
100
153
  }
154
+ function responseFailure(code, response, clock) {
155
+ if (code === "core.http_rate_limited" || code === "core.http_unavailable") {
156
+ const retryAfterSeconds = getRetryAfterSeconds(response.headers.get("retry-after"), clock.now());
157
+ return { ...err(code), status: response.status, retryAfterSeconds };
158
+ }
159
+ return httpFailure(code, response.status);
160
+ }
161
+ function withHeaders(failure, headers) {
162
+ return headers === undefined ? failure : { ...failure, headers };
163
+ }
164
+ function assertHeaderNames(names) {
165
+ for (const name of names ?? []) {
166
+ if (!HEADER_NAME_PATTERN.test(name)) {
167
+ throw new RangeError(`requestJson: readHeaders holds an invalid header name ${JSON.stringify(name)}`);
168
+ }
169
+ }
170
+ }
171
+ function readResponseHeaders(response, names) {
172
+ return Object.fromEntries(names.map((name) => [name, response.headers.get(name)]));
173
+ }
174
+ /**
175
+ * Seconds to wait from a `Retry-After` value (RFC 9110): delay-seconds, or an HTTP date measured against `now` and
176
+ * rounded up. A date in the past is 0, anything above a day is a day, and a malformed value is `null`.
177
+ */
178
+ function getRetryAfterSeconds(value, now) {
179
+ const trimmed = value?.trim() ?? "";
180
+ if (/^\d+$/.test(trimmed)) {
181
+ return Math.min(Number(trimmed), MAX_RETRY_AFTER_SECONDS);
182
+ }
183
+ // Every HTTP date names its day and month; without a letter the value is no date (and `Date.parse` guesses).
184
+ if (!/^[A-Za-z]/.test(trimmed)) {
185
+ return null;
186
+ }
187
+ // HTTP dates are GMT; the obsolete asctime form says so nowhere, and `Date.parse` would read it as local time.
188
+ const at = Date.parse(/(GMT|UTC)$/i.test(trimmed) ? trimmed : `${trimmed} GMT`);
189
+ if (Number.isNaN(at)) {
190
+ return null;
191
+ }
192
+ const seconds = Math.ceil((at - now.getTime()) / 1000);
193
+ return Math.min(Math.max(seconds, 0), MAX_RETRY_AFTER_SECONDS);
194
+ }
101
195
  //# sourceMappingURL=http-json.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-json.js","sourceRoot":"","sources":["../src/http-json.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http-json.js","sourceRoot":"","sources":["../src/http-json.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { calendarPeriodSchema, countPeriodDays, doPeriodsOverlap, isDayInPeriod,
3
3
  export { getLocalDay, isLocalDateTime, localDateTimeSchema, type LocalDateTime } from "./local-date-time.js";
4
4
  export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
5
5
  export { DEFAULT_DURATION_MESSAGES, formatCalendarDay, formatCalendarMonth, formatDuration, formatMoney, formatPercent, isCalendarMonth, type CalendarDayStyle, type CalendarMonthStyle, type DurationMessages, type FormatDurationOptions, type FormatMoneyOptions, type FormatPercentOptions, } from "./format.js";
6
- export { requestJson, toQueryString, type FetchFunction, type HttpJsonFailure, type HttpJsonFailureCode, type HttpJsonMethod, type HttpJsonWriteMethod, type RequestJsonOptions, type RequestJsonReadOptions, type RequestJsonSchemaOptions, type RequestJsonWriteOptions, } from "./http-json.js";
6
+ export { parseLinkHeader, requestJson, toQueryString, type FetchFunction, type HttpJsonAnswer, type HttpJsonFailure, type HttpJsonFailureCode, type HttpJsonMethod, type HttpJsonResponseHeaders, type HttpJsonRetryableCode, type HttpJsonWriteMethod, type LinkHeaderLinks, type RequestJsonOptions, type RequestJsonReadHeadersOptions, type RequestJsonReadOptions, type RequestJsonSchemaOptions, type RequestJsonWriteOptions, } from "./http-json.js";
7
7
  export { memoizeByContent, type MemoizeByContentOptions, type MemoizedFunction } from "./memoize.js";
8
8
  export { createTestClock, systemClock, type Clock, type TestClock } from "./clock.js";
9
9
  export { formatMessage, getMessage, DEFAULT_LOCALE, isLocale, mergeMessages, pickLocalizedText, selectMessages, selectPlural, type DeepPartial, type Dictionaries, type Locale, type LocalizedText, type MessageOverrides, type MessageTree, type PluralForms, } from "./i18n.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { calendarPeriodSchema, countPeriodDays, doPeriodsOverlap, isDayInPeriod,
5
5
  export { getLocalDay, isLocalDateTime, localDateTimeSchema } from "./local-date-time.js";
6
6
  export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
7
7
  export { DEFAULT_DURATION_MESSAGES, formatCalendarDay, formatCalendarMonth, formatDuration, formatMoney, formatPercent, isCalendarMonth, } from "./format.js";
8
- export { requestJson, toQueryString, } from "./http-json.js";
8
+ export { parseLinkHeader, requestJson, toQueryString, } from "./http-json.js";
9
9
  export { memoizeByContent } from "./memoize.js";
10
10
  export { createTestClock, systemClock } from "./clock.js";
11
11
  export { formatMessage, getMessage, DEFAULT_LOCALE, isLocale, mergeMessages, pickLocalizedText, selectMessages, selectPlural, } from "./i18n.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,aAAa,GAEd,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAsB,MAAM,sBAAsB,CAAC;AAC7G,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,aAAa,EACb,eAAe,GAOhB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,aAAa,GAUd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAuD,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,WAAW,EAA8B,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,YAAY,GAQb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAkD,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAsB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,GAQ1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,GAUb,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GAIjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAuC,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAyC,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAmC,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,aAAa,GAEd,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAsB,MAAM,sBAAsB,CAAC;AAC7G,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,aAAa,EACb,eAAe,GAOhB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,WAAW,EACX,aAAa,GAed,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAuD,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,WAAW,EAA8B,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,YAAY,GAQb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAkD,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAsB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,GAQ1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,GAUb,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GAIjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAuC,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAyC,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAmC,MAAM,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softure-ai/core",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "The SOFTURE AI module contract: app config, module definitions, Result, Clock and messages.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/http-json.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  // A JSON call to a third-party API (a GET, or a write with a JSON body), with the details every app's own copy has to
2
2
  // get right: a timeout, no redirect followed (a key in a header or the URL never reaches another host), and every
3
3
  // failure returned as a code. A caught error's message is never read, since it can quote the URL and some APIs put
4
- // the key there. The body stays `unknown` unless a zod schema narrows it.
4
+ // the key there. The body stays `unknown` unless a zod schema narrows it. Response headers are read only by name from
5
+ // the caller's allow-list, so nothing secret is surfaced by accident.
5
6
  import type { z } from "zod";
7
+ import { systemClock, type Clock } from "./clock.js";
6
8
  import { err, ok, type Err, type Ok } from "./result.js";
7
9
 
8
10
  export type HttpJsonFailureCode =
@@ -13,8 +15,30 @@ export type HttpJsonFailureCode =
13
15
  | "core.http_conflict"
14
16
  | "core.http_unexpected_answer";
15
17
 
16
- /** A failed `requestJson`: the code and the HTTP status (`null` when no response arrived). Nothing else. */
17
- export type HttpJsonFailure = Err<HttpJsonFailureCode> & { readonly status: number | null };
18
+ /** The failure codes whose answer may say when to try again (`Retry-After`). */
19
+ export type HttpJsonRetryableCode = "core.http_rate_limited" | "core.http_unavailable";
20
+
21
+ /** The headers named in `readHeaders`, keyed as the caller wrote them; `null` when the answer does not carry one. */
22
+ export type HttpJsonResponseHeaders<H extends string = string> = Readonly<Record<H, string | null>>;
23
+
24
+ /**
25
+ * A failed `requestJson`: the code and the HTTP status (`null` when no response arrived). A rate limited or
26
+ * unavailable answer adds `retryAfterSeconds`, and a response read with `readHeaders` adds `headers`. Nothing else.
27
+ */
28
+ export type HttpJsonFailure =
29
+ | (Err<Exclude<HttpJsonFailureCode, HttpJsonRetryableCode>> & {
30
+ readonly status: number | null;
31
+ readonly headers?: HttpJsonResponseHeaders;
32
+ })
33
+ | (Err<HttpJsonRetryableCode> & {
34
+ readonly status: number;
35
+ /** Parsed from `Retry-After` (seconds or an HTTP date), 0 to 86 400; `null` when missing or malformed. */
36
+ readonly retryAfterSeconds: number | null;
37
+ readonly headers?: HttpJsonResponseHeaders;
38
+ });
39
+
40
+ /** The value of a call made with `readHeaders`: the body as without it, and the requested headers. */
41
+ export type HttpJsonAnswer<T, H extends string> = { readonly body: T; readonly headers: HttpJsonResponseHeaders<H> };
18
42
 
19
43
  export type HttpJsonWriteMethod = "POST" | "PUT" | "PATCH" | "DELETE";
20
44
  export type HttpJsonMethod = "GET" | HttpJsonWriteMethod;
@@ -28,6 +52,18 @@ export interface RequestJsonOptions {
28
52
  readonly headers?: HeadersInit;
29
53
  /** Milliseconds before the request (body included) gives up. Defaults to 10 000. */
30
54
  readonly timeoutMs?: number;
55
+ /** Measures a `Retry-After` HTTP date. Defaults to `systemClock`. */
56
+ readonly clock?: Clock;
57
+ }
58
+
59
+ /** Response headers to return, by name (any letter case). Only these are read. */
60
+ export interface RequestJsonReadHeadersOptions<H extends string> {
61
+ readonly readHeaders: readonly H[];
62
+ }
63
+
64
+ /** The calls without `readHeaders`, whose value is the body itself. */
65
+ interface WithoutReadHeaders {
66
+ readonly readHeaders?: undefined;
31
67
  }
32
68
 
33
69
  export interface RequestJsonSchemaOptions<S extends z.ZodType> extends RequestJsonOptions {
@@ -51,25 +87,43 @@ export interface RequestJsonWriteOptions extends RequestJsonOptions {
51
87
  }
52
88
 
53
89
  const DEFAULT_TIMEOUT_MS = 10_000;
90
+ const MAX_RETRY_AFTER_SECONDS = 86_400;
91
+ // An RFC 9110 token: what a header name may be made of.
92
+ const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
54
93
 
55
94
  /**
56
95
  * Call `url` (a GET unless `method` says otherwise) and read its JSON body. Without a schema the value is `unknown`,
57
96
  * for the caller to narrow; with one it is the schema's output. A write answered with an empty body (a 204) is
58
97
  * `ok(undefined)`, schema or not. 3xx, 401 and 403 are refused, 409 and 412 a conflict, 429 rate limited, 5xx
59
98
  * unavailable, and any other non-2xx status or a body that is not JSON or fails the schema an unexpected answer.
99
+ * With `readHeaders` the value is `{ body, headers }`, and a failure that had a response carries `headers` too.
60
100
  */
101
+ export function requestJson<S extends z.ZodType, const H extends string>(
102
+ options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions & RequestJsonReadHeadersOptions<H>,
103
+ ): Promise<Ok<HttpJsonAnswer<z.output<S>, H>> | HttpJsonFailure>;
104
+ export function requestJson<S extends z.ZodType, const H extends string>(
105
+ options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions & RequestJsonReadHeadersOptions<H>,
106
+ ): Promise<Ok<HttpJsonAnswer<z.output<S> | undefined, H>> | HttpJsonFailure>;
107
+ export function requestJson<const H extends string>(
108
+ options: (RequestJsonReadOptions | RequestJsonWriteOptions) & RequestJsonReadHeadersOptions<H>,
109
+ ): Promise<Ok<HttpJsonAnswer<unknown, H>> | HttpJsonFailure>;
61
110
  export function requestJson<S extends z.ZodType>(
62
- options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions,
111
+ options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions & WithoutReadHeaders,
63
112
  ): Promise<Ok<z.output<S>> | HttpJsonFailure>;
64
113
  export function requestJson<S extends z.ZodType>(
65
- options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions,
114
+ options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions & WithoutReadHeaders,
66
115
  ): Promise<Ok<z.output<S> | undefined> | HttpJsonFailure>;
67
116
  export function requestJson(
68
- options: RequestJsonReadOptions | RequestJsonWriteOptions,
117
+ options: (RequestJsonReadOptions | RequestJsonWriteOptions) & WithoutReadHeaders,
69
118
  ): Promise<Ok<unknown> | HttpJsonFailure>;
70
119
  export async function requestJson(
71
- options: (RequestJsonReadOptions | RequestJsonWriteOptions) & { readonly schema?: z.ZodType },
120
+ options: (RequestJsonReadOptions | RequestJsonWriteOptions) & {
121
+ readonly schema?: z.ZodType;
122
+ readonly readHeaders?: readonly string[];
123
+ },
72
124
  ): Promise<Ok<unknown> | HttpJsonFailure> {
125
+ // Before anything is sent: a write must not go out when its answer cannot be read.
126
+ assertHeaderNames(options.readHeaders);
73
127
  const fetchFunction = options.fetch ?? globalThis.fetch;
74
128
  const method = options.method ?? "GET";
75
129
  const isWrite = method !== "GET";
@@ -101,11 +155,16 @@ export async function requestJson(
101
155
  return httpFailure("core.http_unreachable", null);
102
156
  }
103
157
 
104
- const statusFailure = getStatusFailure(response);
105
- if (statusFailure !== null) {
158
+ const headers = options.readHeaders === undefined ? undefined : readResponseHeaders(response, options.readHeaders);
159
+ const fail = (code: HttpJsonFailureCode): HttpJsonFailure =>
160
+ withHeaders(responseFailure(code, response, options.clock ?? systemClock), headers);
161
+ const succeed = (body: unknown): Ok<unknown> => ok(headers === undefined ? body : { body, headers });
162
+
163
+ const statusFailureCode = getStatusFailureCode(response);
164
+ if (statusFailureCode !== null) {
106
165
  // Release the connection; a body that cannot be cancelled changes nothing for the caller.
107
166
  await response.body?.cancel().catch(() => undefined);
108
- return statusFailure;
167
+ return fail(statusFailureCode);
109
168
  }
110
169
 
111
170
  let text: string;
@@ -113,27 +172,83 @@ export async function requestJson(
113
172
  text = await response.text();
114
173
  } catch {
115
174
  // The body broke off, usually the timeout firing mid-body.
116
- return httpFailure("core.http_unreachable", response.status);
175
+ return fail("core.http_unreachable");
117
176
  }
118
177
 
119
178
  if (isWrite && text.trim() === "") {
120
179
  // A 204, or a write that answers 200 with nothing: the write succeeded and there is nothing to read.
121
- return ok(undefined);
180
+ return succeed(undefined);
122
181
  }
123
182
 
124
183
  let answer: unknown;
125
184
  try {
126
185
  answer = JSON.parse(text);
127
186
  } catch {
128
- return httpFailure("core.http_unexpected_answer", response.status);
187
+ return fail("core.http_unexpected_answer");
129
188
  }
130
189
 
131
190
  if (options.schema === undefined) {
132
- return ok(answer);
191
+ return succeed(answer);
133
192
  }
134
193
  // The schema's issues can quote the body's values, so only the code leaves.
135
194
  const parsed = options.schema.safeParse(answer);
136
- return parsed.success ? ok(parsed.data) : httpFailure("core.http_unexpected_answer", response.status);
195
+ return parsed.success ? succeed(parsed.data) : fail("core.http_unexpected_answer");
196
+ }
197
+
198
+ /** The URLs of the pagination relations in a `Link` header (RFC 8288), as written; missing ones are left out. */
199
+ export interface LinkHeaderLinks {
200
+ readonly next?: string;
201
+ readonly prev?: string;
202
+ readonly first?: string;
203
+ readonly last?: string;
204
+ }
205
+
206
+ type LinkRelation = keyof LinkHeaderLinks;
207
+
208
+ const REL_PATTERN = /(?:^|;)\s*rel\s*=\s*(?:"([^"]*)"|([^\s;,"]+))/i;
209
+ const LINK_RELATIONS: Readonly<Record<string, LinkRelation>> = {
210
+ next: "next",
211
+ prev: "prev",
212
+ previous: "prev",
213
+ first: "first",
214
+ last: "last",
215
+ };
216
+
217
+ /**
218
+ * Read `next`, `prev` (or `previous`), `first` and `last` from a `Link` header such as
219
+ * `<https://api.example.com/items?page=2>; rel="next"`. The first link of a relation wins; `null`, an empty value or
220
+ * one that cannot be read gives `{}`.
221
+ */
222
+ export function parseLinkHeader(value: string | null | undefined): LinkHeaderLinks {
223
+ const links: Partial<Record<LinkRelation, string>> = {};
224
+ if (value === null || value === undefined) {
225
+ return links;
226
+ }
227
+ // One link: the URL in angle brackets and its parameters up to the next `<`. Splitting by commas would break a URL
228
+ // that contains one, and a regular expression would rescan the rest of a hostile value from every `<`.
229
+ let position = 0;
230
+ while (position < value.length) {
231
+ const open = value.indexOf("<", position);
232
+ const close = open === -1 ? -1 : value.indexOf(">", open + 1);
233
+ if (close === -1) {
234
+ break;
235
+ }
236
+ const nextOpen = value.indexOf("<", close + 1);
237
+ position = nextOpen === -1 ? value.length : nextOpen;
238
+ const url = value.slice(open + 1, close).trim();
239
+ const rel = REL_PATTERN.exec(value.slice(close + 1, position));
240
+ const relationTypes = rel?.[1] ?? rel?.[2];
241
+ if (url === "" || relationTypes === undefined) {
242
+ continue;
243
+ }
244
+ for (const relationType of relationTypes.trim().toLowerCase().split(/\s+/)) {
245
+ const relation = LINK_RELATIONS[relationType];
246
+ if (relation !== undefined && links[relation] === undefined) {
247
+ links[relation] = url;
248
+ }
249
+ }
250
+ }
251
+ return links;
137
252
  }
138
253
 
139
254
  /**
@@ -150,26 +265,72 @@ export function toQueryString(params: Readonly<Record<string, string | number |
150
265
  return search.toString();
151
266
  }
152
267
 
153
- function getStatusFailure(response: Response): HttpJsonFailure | null {
268
+ function getStatusFailureCode(response: Response): HttpJsonFailureCode | null {
154
269
  const { status } = response;
155
270
  if (response.type === "opaqueredirect" || (status >= 300 && status < 400) || status === 401 || status === 403) {
156
- return httpFailure("core.http_refused", status);
271
+ return "core.http_refused";
157
272
  }
158
273
  if (status === 409 || status === 412) {
159
- return httpFailure("core.http_conflict", status);
274
+ return "core.http_conflict";
160
275
  }
161
276
  if (status === 429) {
162
- return httpFailure("core.http_rate_limited", status);
277
+ return "core.http_rate_limited";
163
278
  }
164
279
  if (status >= 500) {
165
- return httpFailure("core.http_unavailable", status);
280
+ return "core.http_unavailable";
166
281
  }
167
282
  if (status < 200 || status >= 300) {
168
- return httpFailure("core.http_unexpected_answer", status);
283
+ return "core.http_unexpected_answer";
169
284
  }
170
285
  return null;
171
286
  }
172
287
 
173
- function httpFailure(code: HttpJsonFailureCode, status: number | null): HttpJsonFailure {
288
+ function httpFailure(code: Exclude<HttpJsonFailureCode, HttpJsonRetryableCode>, status: number | null): HttpJsonFailure {
174
289
  return { ...err(code), status };
175
290
  }
291
+
292
+ function responseFailure(code: HttpJsonFailureCode, response: Response, clock: Clock): HttpJsonFailure {
293
+ if (code === "core.http_rate_limited" || code === "core.http_unavailable") {
294
+ const retryAfterSeconds = getRetryAfterSeconds(response.headers.get("retry-after"), clock.now());
295
+ return { ...err(code), status: response.status, retryAfterSeconds };
296
+ }
297
+ return httpFailure(code, response.status);
298
+ }
299
+
300
+ function withHeaders(failure: HttpJsonFailure, headers: HttpJsonResponseHeaders | undefined): HttpJsonFailure {
301
+ return headers === undefined ? failure : { ...failure, headers };
302
+ }
303
+
304
+ function assertHeaderNames(names: readonly string[] | undefined): void {
305
+ for (const name of names ?? []) {
306
+ if (!HEADER_NAME_PATTERN.test(name)) {
307
+ throw new RangeError(`requestJson: readHeaders holds an invalid header name ${JSON.stringify(name)}`);
308
+ }
309
+ }
310
+ }
311
+
312
+ function readResponseHeaders(response: Response, names: readonly string[]): HttpJsonResponseHeaders {
313
+ return Object.fromEntries(names.map((name) => [name, response.headers.get(name)]));
314
+ }
315
+
316
+ /**
317
+ * Seconds to wait from a `Retry-After` value (RFC 9110): delay-seconds, or an HTTP date measured against `now` and
318
+ * rounded up. A date in the past is 0, anything above a day is a day, and a malformed value is `null`.
319
+ */
320
+ function getRetryAfterSeconds(value: string | null, now: Date): number | null {
321
+ const trimmed = value?.trim() ?? "";
322
+ if (/^\d+$/.test(trimmed)) {
323
+ return Math.min(Number(trimmed), MAX_RETRY_AFTER_SECONDS);
324
+ }
325
+ // Every HTTP date names its day and month; without a letter the value is no date (and `Date.parse` guesses).
326
+ if (!/^[A-Za-z]/.test(trimmed)) {
327
+ return null;
328
+ }
329
+ // HTTP dates are GMT; the obsolete asctime form says so nowhere, and `Date.parse` would read it as local time.
330
+ const at = Date.parse(/(GMT|UTC)$/i.test(trimmed) ? trimmed : `${trimmed} GMT`);
331
+ if (Number.isNaN(at)) {
332
+ return null;
333
+ }
334
+ const seconds = Math.ceil((at - now.getTime()) / 1000);
335
+ return Math.min(Math.max(seconds, 0), MAX_RETRY_AFTER_SECONDS);
336
+ }
package/src/index.ts CHANGED
@@ -36,14 +36,20 @@ export {
36
36
  type FormatPercentOptions,
37
37
  } from "./format.js";
38
38
  export {
39
+ parseLinkHeader,
39
40
  requestJson,
40
41
  toQueryString,
41
42
  type FetchFunction,
43
+ type HttpJsonAnswer,
42
44
  type HttpJsonFailure,
43
45
  type HttpJsonFailureCode,
44
46
  type HttpJsonMethod,
47
+ type HttpJsonResponseHeaders,
48
+ type HttpJsonRetryableCode,
45
49
  type HttpJsonWriteMethod,
50
+ type LinkHeaderLinks,
46
51
  type RequestJsonOptions,
52
+ type RequestJsonReadHeadersOptions,
47
53
  type RequestJsonReadOptions,
48
54
  type RequestJsonSchemaOptions,
49
55
  type RequestJsonWriteOptions,