@profullstack/r3q 0.1.0

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/src/send.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Sending a request and describing what came back.
3
+ *
4
+ * The whole exchange is kept — status, headers, timing, body — because the
5
+ * point of a terminal REST client is inspecting the response, not just getting
6
+ * an exit code.
7
+ */
8
+ import type { RequestFile } from "./collection.ts";
9
+
10
+ export interface Exchange {
11
+ status: number;
12
+ statusText: string;
13
+ headers: [string, string][];
14
+ body: string;
15
+ /** Round trip in milliseconds. */
16
+ ms: number;
17
+ bytes: number;
18
+ contentType: string;
19
+ error?: string;
20
+ }
21
+
22
+ export async function send(request: RequestFile, timeoutMs = 30_000): Promise<Exchange> {
23
+ const started = performance.now();
24
+ const empty = (error: string): Exchange => ({
25
+ status: 0, statusText: "", headers: [], body: "",
26
+ ms: performance.now() - started, bytes: 0, contentType: "", error,
27
+ });
28
+
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
31
+ try {
32
+ const response = await fetch(request.url, {
33
+ method: request.method,
34
+ headers: request.headers,
35
+ body: request.body,
36
+ signal: controller.signal,
37
+ redirect: "follow",
38
+ });
39
+ const body = await response.text();
40
+ return {
41
+ status: response.status,
42
+ statusText: response.statusText,
43
+ headers: [...response.headers.entries()].sort((a, b) => a[0].localeCompare(b[0])),
44
+ body,
45
+ ms: performance.now() - started,
46
+ bytes: new TextEncoder().encode(body).length,
47
+ contentType: response.headers.get("content-type") ?? "",
48
+ };
49
+ } catch (error) {
50
+ if (controller.signal.aborted) return empty(`timed out after ${timeoutMs}ms`);
51
+ return empty(error instanceof Error ? error.message : String(error));
52
+ } finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+
57
+ /** Pretty-print JSON, leaving anything else exactly as it arrived. */
58
+ export function formatBody(body: string, contentType: string): string {
59
+ if (!/\bjson\b/i.test(contentType)) return body;
60
+ try {
61
+ return JSON.stringify(JSON.parse(body), null, 2);
62
+ } catch {
63
+ // A content-type that lies is common enough not to be an error.
64
+ return body;
65
+ }
66
+ }
67
+
68
+ /** The colour family for a status code, as a theme key. */
69
+ export function statusKind(status: number): "success" | "accent" | "warning" | "danger" | "muted" {
70
+ if (status === 0) return "danger";
71
+ if (status < 200) return "muted";
72
+ if (status < 300) return "success";
73
+ if (status < 400) return "accent";
74
+ if (status < 500) return "warning";
75
+ return "danger";
76
+ }