@softure-ai/core 0.2.1 → 0.2.2
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 +14 -0
- package/README.md +22 -2
- package/dist/http-json.d.ts +24 -6
- package/dist/http-json.d.ts.map +1 -1
- package/dist/http-json.js +23 -5
- package/dist/http-json.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +1 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +1 -0
- package/dist/messages/en.js.map +1 -1
- package/package.json +1 -1
- package/src/http-json.ts +60 -15
- package/src/index.ts +4 -0
- package/src/messages/en.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.2
|
|
8
|
+
|
|
9
|
+
- `requestJson` writes (#635): `method` (`"GET"` by default, `"POST" | "PUT" | "PATCH" | "DELETE"`), `body` sent as
|
|
10
|
+
JSON with `Content-Type: application/json` unless the caller sets its own, and `idempotencyKey` sent as
|
|
11
|
+
`Idempotency-Key`. A write answered with an empty body (204, or 200 with nothing) returns `ok(undefined)`, so a
|
|
12
|
+
write with a schema returns `Ok<T | undefined>`; a non-empty answer still goes through the schema. A GET takes no
|
|
13
|
+
body, and its empty answer stays `core.http_unexpected_answer`. New types `HttpJsonMethod`, `HttpJsonWriteMethod`,
|
|
14
|
+
`RequestJsonReadOptions`, `RequestJsonWriteOptions`.
|
|
15
|
+
- New code `core.http_conflict` for 409 and 412 (a stale version or sha), for every method, with English copy in
|
|
16
|
+
`coreMessages` (`errors.http_conflict`).
|
|
17
|
+
- Migrate: a caller that switched on `core.http_unexpected_answer` to spot a 409 or 412 switches on
|
|
18
|
+
`core.http_conflict` instead; an exhaustive `switch` over `HttpJsonFailureCode` gets the new case. An app with
|
|
19
|
+
its own write helper replaces it with `requestJson({ method, body })` and handles `ok(undefined)` for 204s.
|
|
20
|
+
|
|
7
21
|
## 0.2.1
|
|
8
22
|
|
|
9
23
|
- New (#599): calendar periods and local wall-clock date-times next to calendar days.
|
package/README.md
CHANGED
|
@@ -305,8 +305,8 @@ 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, fetch?, headers?, timeoutMs?, schema? })`
|
|
309
|
-
and returns `Ok<value>` or an `HttpJsonFailure` (`{ ok: false, error, status }`). It sets the details a hand-written
|
|
308
|
+
**External JSON APIs.** `requestJson({ url, method?, body?, idempotencyKey?, fetch?, headers?, timeoutMs?, schema? })`
|
|
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
|
|
|
312
312
|
- a timeout, `AbortSignal.timeout(timeoutMs)` (default 10 000 ms), body included;
|
|
@@ -319,6 +319,7 @@ helper tends to miss:
|
|
|
319
319
|
|---|---|
|
|
320
320
|
| `core.http_unreachable` | network error, timeout, a body that breaks off |
|
|
321
321
|
| `core.http_refused` | 3xx, an opaque redirect, 401, 403 |
|
|
322
|
+
| `core.http_conflict` | 409, 412 (a stale version or sha: re-read, then retry) |
|
|
322
323
|
| `core.http_rate_limited` | 429 |
|
|
323
324
|
| `core.http_unavailable` | 5xx |
|
|
324
325
|
| `core.http_unexpected_answer` | any other non-2xx status, a body that is not JSON or fails the schema |
|
|
@@ -344,6 +345,25 @@ if (!result.ok) {
|
|
|
344
345
|
}
|
|
345
346
|
```
|
|
346
347
|
|
|
348
|
+
Writes take `method: "POST" | "PUT" | "PATCH" | "DELETE"` and a `body`, sent as JSON with
|
|
349
|
+
`Content-Type: application/json` unless `headers` sets its own; a value JSON cannot represent (a cycle, a BigInt)
|
|
350
|
+
throws before anything is sent. `idempotencyKey` is sent as `Idempotency-Key`. A write answered with an empty body
|
|
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
|
+
answer still goes through the schema. A GET carries no body, and an empty answer to it stays an unexpected answer.
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
const saved = await requestJson({
|
|
356
|
+
url: `https://api.example.com/tasks/${task.id}`,
|
|
357
|
+
method: "PUT",
|
|
358
|
+
body: { title: task.title, version: task.version },
|
|
359
|
+
headers: { Authorization: `Bearer ${env.TASKS_API_KEY}` },
|
|
360
|
+
schema: z.object({ version: z.number() }),
|
|
361
|
+
});
|
|
362
|
+
if (!saved.ok && saved.error === "core.http_conflict") {
|
|
363
|
+
// someone else changed the task: read it again and retry the write
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
347
367
|
**Time.** Server code receives a `Clock` (`ModuleContext.clock`) instead of calling `new Date()`.
|
|
348
368
|
`systemClock` is the real one; `createTestClock(start)` has `advance(ms)` and `set(date)`.
|
|
349
369
|
A whole test run can also move to another day without code changes: `TEST_TODAY=YYYY-MM-DD` with the
|
package/dist/http-json.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
import { type Err, type Ok } from "./result.js";
|
|
3
|
-
export type HttpJsonFailureCode = "core.http_unreachable" | "core.http_refused" | "core.http_rate_limited" | "core.http_unavailable" | "core.http_unexpected_answer";
|
|
3
|
+
export type HttpJsonFailureCode = "core.http_unreachable" | "core.http_refused" | "core.http_rate_limited" | "core.http_unavailable" | "core.http_conflict" | "core.http_unexpected_answer";
|
|
4
4
|
/** A failed `requestJson`: the code and the HTTP status (`null` when no response arrived). Nothing else. */
|
|
5
5
|
export type HttpJsonFailure = Err<HttpJsonFailureCode> & {
|
|
6
6
|
readonly status: number | null;
|
|
7
7
|
};
|
|
8
|
+
export type HttpJsonWriteMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
9
|
+
export type HttpJsonMethod = "GET" | HttpJsonWriteMethod;
|
|
8
10
|
export type FetchFunction = (input: string | URL, init: RequestInit) => Promise<Response>;
|
|
9
11
|
export interface RequestJsonOptions {
|
|
10
12
|
readonly url: string | URL;
|
|
@@ -17,13 +19,29 @@ export interface RequestJsonOptions {
|
|
|
17
19
|
export interface RequestJsonSchemaOptions<S extends z.ZodType> extends RequestJsonOptions {
|
|
18
20
|
readonly schema: S;
|
|
19
21
|
}
|
|
22
|
+
/** A GET: no body, no idempotency key (a GET is idempotent already). */
|
|
23
|
+
export interface RequestJsonReadOptions extends RequestJsonOptions {
|
|
24
|
+
readonly method?: "GET";
|
|
25
|
+
readonly body?: never;
|
|
26
|
+
readonly idempotencyKey?: never;
|
|
27
|
+
}
|
|
28
|
+
/** A write: the body is sent as JSON, and an answer with no body is `ok(undefined)`. */
|
|
29
|
+
export interface RequestJsonWriteOptions extends RequestJsonOptions {
|
|
30
|
+
readonly method: HttpJsonWriteMethod;
|
|
31
|
+
/** Serialised with `JSON.stringify`; `undefined` sends no body. A value JSON cannot represent throws. */
|
|
32
|
+
readonly body?: unknown;
|
|
33
|
+
/** Sent as `Idempotency-Key`, for providers that deduplicate retried writes by it. */
|
|
34
|
+
readonly idempotencyKey?: string;
|
|
35
|
+
}
|
|
20
36
|
/**
|
|
21
|
-
*
|
|
22
|
-
* it is the schema's output.
|
|
23
|
-
*
|
|
37
|
+
* Call `url` (a GET unless `method` says otherwise) and read its JSON body. Without a schema the value is `unknown`,
|
|
38
|
+
* for the caller to narrow; with one it is the schema's output. A write answered with an empty body (a 204) is
|
|
39
|
+
* `ok(undefined)`, schema or not. 3xx, 401 and 403 are refused, 409 and 412 a conflict, 429 rate limited, 5xx
|
|
40
|
+
* unavailable, and any other non-2xx status or a body that is not JSON or fails the schema an unexpected answer.
|
|
24
41
|
*/
|
|
25
|
-
export declare function requestJson<S extends z.ZodType>(options: RequestJsonSchemaOptions<S>): Promise<Ok<z.output<S>> | HttpJsonFailure>;
|
|
26
|
-
export declare function requestJson(options:
|
|
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>;
|
|
27
45
|
/**
|
|
28
46
|
* The query part of a URL (without `?`) from a flat record, in insertion order. `null` and `undefined` are left
|
|
29
47
|
* out, so optional parameters can be passed as they are.
|
package/dist/http-json.d.ts.map
CHANGED
|
@@ -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,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,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;AAID
|
|
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"}
|
package/dist/http-json.js
CHANGED
|
@@ -2,6 +2,10 @@ import { err, ok } from "./result.js";
|
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
3
|
export async function requestJson(options) {
|
|
4
4
|
const fetchFunction = options.fetch ?? globalThis.fetch;
|
|
5
|
+
const method = options.method ?? "GET";
|
|
6
|
+
const isWrite = method !== "GET";
|
|
7
|
+
// Outside the try on purpose: a body JSON cannot represent is the caller's bug, not an unreachable service.
|
|
8
|
+
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
5
9
|
let response;
|
|
6
10
|
try {
|
|
7
11
|
// Inside the try: an invalid header value throws a message that quotes it.
|
|
@@ -9,9 +13,16 @@ export async function requestJson(options) {
|
|
|
9
13
|
if (!headers.has("accept")) {
|
|
10
14
|
headers.set("accept", "application/json");
|
|
11
15
|
}
|
|
16
|
+
if (body !== undefined && !headers.has("content-type")) {
|
|
17
|
+
headers.set("content-type", "application/json");
|
|
18
|
+
}
|
|
19
|
+
if (options.idempotencyKey !== undefined) {
|
|
20
|
+
headers.set("idempotency-key", options.idempotencyKey);
|
|
21
|
+
}
|
|
12
22
|
response = await fetchFunction(options.url, {
|
|
13
|
-
method
|
|
23
|
+
method,
|
|
14
24
|
headers,
|
|
25
|
+
body,
|
|
15
26
|
redirect: "manual",
|
|
16
27
|
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
17
28
|
});
|
|
@@ -34,18 +45,22 @@ export async function requestJson(options) {
|
|
|
34
45
|
// The body broke off, usually the timeout firing mid-body.
|
|
35
46
|
return httpFailure("core.http_unreachable", response.status);
|
|
36
47
|
}
|
|
37
|
-
|
|
48
|
+
if (isWrite && text.trim() === "") {
|
|
49
|
+
// A 204, or a write that answers 200 with nothing: the write succeeded and there is nothing to read.
|
|
50
|
+
return ok(undefined);
|
|
51
|
+
}
|
|
52
|
+
let answer;
|
|
38
53
|
try {
|
|
39
|
-
|
|
54
|
+
answer = JSON.parse(text);
|
|
40
55
|
}
|
|
41
56
|
catch {
|
|
42
57
|
return httpFailure("core.http_unexpected_answer", response.status);
|
|
43
58
|
}
|
|
44
59
|
if (options.schema === undefined) {
|
|
45
|
-
return ok(
|
|
60
|
+
return ok(answer);
|
|
46
61
|
}
|
|
47
62
|
// The schema's issues can quote the body's values, so only the code leaves.
|
|
48
|
-
const parsed = options.schema.safeParse(
|
|
63
|
+
const parsed = options.schema.safeParse(answer);
|
|
49
64
|
return parsed.success ? ok(parsed.data) : httpFailure("core.http_unexpected_answer", response.status);
|
|
50
65
|
}
|
|
51
66
|
/**
|
|
@@ -66,6 +81,9 @@ function getStatusFailure(response) {
|
|
|
66
81
|
if (response.type === "opaqueredirect" || (status >= 300 && status < 400) || status === 401 || status === 403) {
|
|
67
82
|
return httpFailure("core.http_refused", status);
|
|
68
83
|
}
|
|
84
|
+
if (status === 409 || status === 412) {
|
|
85
|
+
return httpFailure("core.http_conflict", status);
|
|
86
|
+
}
|
|
69
87
|
if (status === 429) {
|
|
70
88
|
return httpFailure("core.http_rate_limited", status);
|
|
71
89
|
}
|
package/dist/http-json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-json.js","sourceRoot":"","sources":["../src/http-json.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAqB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"http-json.js","sourceRoot":"","sources":["../src/http-json.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAqB,MAAM,aAAa,CAAC;AA+CzD,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAiBlC,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAA6F;IAE7F,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACxD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC;IACvC,MAAM,OAAO,GAAG,MAAM,KAAK,KAAK,CAAC;IACjC,4GAA4G;IAC5G,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnF,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,2EAA2E;QAC3E,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YACvD,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAClD,CAAC;QACD,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;QACzD,CAAC;QACD,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE;YAC1C,MAAM;YACN,OAAO;YACP,IAAI;YACJ,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAAC;SACrE,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,6FAA6F;QAC7F,OAAO,WAAW,CAAC,uBAAuB,EAAE,IAAI,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,aAAa,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,0FAA0F;QAC1F,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrD,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,2DAA2D;QAC3D,OAAO,WAAW,CAAC,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/D,CAAC;IAED,IAAI,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAClC,qGAAqG;QACrG,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,WAAW,CAAC,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IACD,4EAA4E;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAA8E;IAC1G,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,gBAAgB,CAAC,QAAkB;IAC1C,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC;IAC5B,IAAI,QAAQ,CAAC,IAAI,KAAK,gBAAgB,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QAC9G,OAAO,WAAW,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACrC,OAAO,WAAW,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,WAAW,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAClB,OAAO,WAAW,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAClC,OAAO,WAAW,CAAC,6BAA6B,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,IAAyB,EAAE,MAAqB;IACnE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;AAClC,CAAC"}
|
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 RequestJsonOptions, type RequestJsonSchemaOptions, } from "./http-json.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";
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,KAAK,UAAU,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,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,EACf,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
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,
|
|
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"}
|
package/dist/messages/en.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;;CAWd,CAAC"}
|
package/dist/messages/en.js
CHANGED
|
@@ -6,6 +6,7 @@ export const en = {
|
|
|
6
6
|
http_refused: "The service refused the request.",
|
|
7
7
|
http_rate_limited: "The service is receiving too many requests. Please try again later.",
|
|
8
8
|
http_unavailable: "The service is unavailable right now. Please try again later.",
|
|
9
|
+
http_conflict: "The data changed in the meantime. Please reload it and try again.",
|
|
9
10
|
http_unexpected_answer: "The service sent an answer that could not be read.",
|
|
10
11
|
},
|
|
11
12
|
};
|
package/dist/messages/en.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,MAAM,EAAE;QACN,eAAe,EAAE,sEAAsE;QACvF,UAAU,EAAE,yCAAyC;QACrD,gBAAgB,EAAE,2DAA2D;QAC7E,YAAY,EAAE,kCAAkC;QAChD,iBAAiB,EAAE,qEAAqE;QACxF,gBAAgB,EAAE,+DAA+D;QACjF,sBAAsB,EAAE,oDAAoD;KAC7E;CACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,MAAM,EAAE;QACN,eAAe,EAAE,sEAAsE;QACvF,UAAU,EAAE,yCAAyC;QACrD,gBAAgB,EAAE,2DAA2D;QAC7E,YAAY,EAAE,kCAAkC;QAChD,iBAAiB,EAAE,qEAAqE;QACxF,gBAAgB,EAAE,+DAA+D;QACjF,aAAa,EAAE,mEAAmE;QAClF,sBAAsB,EAAE,oDAAoD;KAC7E;CACF,CAAC"}
|
package/package.json
CHANGED
package/src/http-json.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// A JSON
|
|
2
|
-
// followed (a key in a header or the URL never reaches another host), and every
|
|
3
|
-
// error's message is never read, since it can quote the URL and some APIs put
|
|
4
|
-
// `unknown` unless a zod schema narrows it.
|
|
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
|
+
// get right: a timeout, no redirect followed (a key in a header or the URL never reaches another host), and every
|
|
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.
|
|
5
5
|
import type { z } from "zod";
|
|
6
6
|
import { err, ok, type Err, type Ok } from "./result.js";
|
|
7
7
|
|
|
@@ -10,11 +10,15 @@ export type HttpJsonFailureCode =
|
|
|
10
10
|
| "core.http_refused"
|
|
11
11
|
| "core.http_rate_limited"
|
|
12
12
|
| "core.http_unavailable"
|
|
13
|
+
| "core.http_conflict"
|
|
13
14
|
| "core.http_unexpected_answer";
|
|
14
15
|
|
|
15
16
|
/** A failed `requestJson`: the code and the HTTP status (`null` when no response arrived). Nothing else. */
|
|
16
17
|
export type HttpJsonFailure = Err<HttpJsonFailureCode> & { readonly status: number | null };
|
|
17
18
|
|
|
19
|
+
export type HttpJsonWriteMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
20
|
+
export type HttpJsonMethod = "GET" | HttpJsonWriteMethod;
|
|
21
|
+
|
|
18
22
|
export type FetchFunction = (input: string | URL, init: RequestInit) => Promise<Response>;
|
|
19
23
|
|
|
20
24
|
export interface RequestJsonOptions {
|
|
@@ -30,21 +34,47 @@ export interface RequestJsonSchemaOptions<S extends z.ZodType> extends RequestJs
|
|
|
30
34
|
readonly schema: S;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
/** A GET: no body, no idempotency key (a GET is idempotent already). */
|
|
38
|
+
export interface RequestJsonReadOptions extends RequestJsonOptions {
|
|
39
|
+
readonly method?: "GET";
|
|
40
|
+
readonly body?: never;
|
|
41
|
+
readonly idempotencyKey?: never;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A write: the body is sent as JSON, and an answer with no body is `ok(undefined)`. */
|
|
45
|
+
export interface RequestJsonWriteOptions extends RequestJsonOptions {
|
|
46
|
+
readonly method: HttpJsonWriteMethod;
|
|
47
|
+
/** Serialised with `JSON.stringify`; `undefined` sends no body. A value JSON cannot represent throws. */
|
|
48
|
+
readonly body?: unknown;
|
|
49
|
+
/** Sent as `Idempotency-Key`, for providers that deduplicate retried writes by it. */
|
|
50
|
+
readonly idempotencyKey?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
34
54
|
|
|
35
55
|
/**
|
|
36
|
-
*
|
|
37
|
-
* it is the schema's output.
|
|
38
|
-
*
|
|
56
|
+
* Call `url` (a GET unless `method` says otherwise) and read its JSON body. Without a schema the value is `unknown`,
|
|
57
|
+
* for the caller to narrow; with one it is the schema's output. A write answered with an empty body (a 204) is
|
|
58
|
+
* `ok(undefined)`, schema or not. 3xx, 401 and 403 are refused, 409 and 412 a conflict, 429 rate limited, 5xx
|
|
59
|
+
* unavailable, and any other non-2xx status or a body that is not JSON or fails the schema an unexpected answer.
|
|
39
60
|
*/
|
|
40
61
|
export function requestJson<S extends z.ZodType>(
|
|
41
|
-
options: RequestJsonSchemaOptions<S
|
|
62
|
+
options: RequestJsonSchemaOptions<S> & RequestJsonReadOptions,
|
|
42
63
|
): Promise<Ok<z.output<S>> | HttpJsonFailure>;
|
|
43
|
-
export function requestJson
|
|
64
|
+
export function requestJson<S extends z.ZodType>(
|
|
65
|
+
options: RequestJsonSchemaOptions<S> & RequestJsonWriteOptions,
|
|
66
|
+
): Promise<Ok<z.output<S> | undefined> | HttpJsonFailure>;
|
|
67
|
+
export function requestJson(
|
|
68
|
+
options: RequestJsonReadOptions | RequestJsonWriteOptions,
|
|
69
|
+
): Promise<Ok<unknown> | HttpJsonFailure>;
|
|
44
70
|
export async function requestJson(
|
|
45
|
-
options:
|
|
71
|
+
options: (RequestJsonReadOptions | RequestJsonWriteOptions) & { readonly schema?: z.ZodType },
|
|
46
72
|
): Promise<Ok<unknown> | HttpJsonFailure> {
|
|
47
73
|
const fetchFunction = options.fetch ?? globalThis.fetch;
|
|
74
|
+
const method = options.method ?? "GET";
|
|
75
|
+
const isWrite = method !== "GET";
|
|
76
|
+
// Outside the try on purpose: a body JSON cannot represent is the caller's bug, not an unreachable service.
|
|
77
|
+
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
48
78
|
|
|
49
79
|
let response: Response;
|
|
50
80
|
try {
|
|
@@ -53,9 +83,16 @@ export async function requestJson(
|
|
|
53
83
|
if (!headers.has("accept")) {
|
|
54
84
|
headers.set("accept", "application/json");
|
|
55
85
|
}
|
|
86
|
+
if (body !== undefined && !headers.has("content-type")) {
|
|
87
|
+
headers.set("content-type", "application/json");
|
|
88
|
+
}
|
|
89
|
+
if (options.idempotencyKey !== undefined) {
|
|
90
|
+
headers.set("idempotency-key", options.idempotencyKey);
|
|
91
|
+
}
|
|
56
92
|
response = await fetchFunction(options.url, {
|
|
57
|
-
method
|
|
93
|
+
method,
|
|
58
94
|
headers,
|
|
95
|
+
body,
|
|
59
96
|
redirect: "manual",
|
|
60
97
|
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
61
98
|
});
|
|
@@ -79,18 +116,23 @@ export async function requestJson(
|
|
|
79
116
|
return httpFailure("core.http_unreachable", response.status);
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
|
|
119
|
+
if (isWrite && text.trim() === "") {
|
|
120
|
+
// A 204, or a write that answers 200 with nothing: the write succeeded and there is nothing to read.
|
|
121
|
+
return ok(undefined);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let answer: unknown;
|
|
83
125
|
try {
|
|
84
|
-
|
|
126
|
+
answer = JSON.parse(text);
|
|
85
127
|
} catch {
|
|
86
128
|
return httpFailure("core.http_unexpected_answer", response.status);
|
|
87
129
|
}
|
|
88
130
|
|
|
89
131
|
if (options.schema === undefined) {
|
|
90
|
-
return ok(
|
|
132
|
+
return ok(answer);
|
|
91
133
|
}
|
|
92
134
|
// The schema's issues can quote the body's values, so only the code leaves.
|
|
93
|
-
const parsed = options.schema.safeParse(
|
|
135
|
+
const parsed = options.schema.safeParse(answer);
|
|
94
136
|
return parsed.success ? ok(parsed.data) : httpFailure("core.http_unexpected_answer", response.status);
|
|
95
137
|
}
|
|
96
138
|
|
|
@@ -113,6 +155,9 @@ function getStatusFailure(response: Response): HttpJsonFailure | null {
|
|
|
113
155
|
if (response.type === "opaqueredirect" || (status >= 300 && status < 400) || status === 401 || status === 403) {
|
|
114
156
|
return httpFailure("core.http_refused", status);
|
|
115
157
|
}
|
|
158
|
+
if (status === 409 || status === 412) {
|
|
159
|
+
return httpFailure("core.http_conflict", status);
|
|
160
|
+
}
|
|
116
161
|
if (status === 429) {
|
|
117
162
|
return httpFailure("core.http_rate_limited", status);
|
|
118
163
|
}
|
package/src/index.ts
CHANGED
|
@@ -41,8 +41,12 @@ export {
|
|
|
41
41
|
type FetchFunction,
|
|
42
42
|
type HttpJsonFailure,
|
|
43
43
|
type HttpJsonFailureCode,
|
|
44
|
+
type HttpJsonMethod,
|
|
45
|
+
type HttpJsonWriteMethod,
|
|
44
46
|
type RequestJsonOptions,
|
|
47
|
+
type RequestJsonReadOptions,
|
|
45
48
|
type RequestJsonSchemaOptions,
|
|
49
|
+
type RequestJsonWriteOptions,
|
|
46
50
|
} from "./http-json.js";
|
|
47
51
|
export { memoizeByContent, type MemoizeByContentOptions, type MemoizedFunction } from "./memoize.js";
|
|
48
52
|
export { createTestClock, systemClock, type Clock, type TestClock } from "./clock.js";
|
package/src/messages/en.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const en = {
|
|
|
6
6
|
http_refused: "The service refused the request.",
|
|
7
7
|
http_rate_limited: "The service is receiving too many requests. Please try again later.",
|
|
8
8
|
http_unavailable: "The service is unavailable right now. Please try again later.",
|
|
9
|
+
http_conflict: "The data changed in the meantime. Please reload it and try again.",
|
|
9
10
|
http_unexpected_answer: "The service sent an answer that could not be read.",
|
|
10
11
|
},
|
|
11
12
|
};
|