|
| 1 | +import http from "node:http"; |
| 2 | +import https from "node:https"; |
1 | 3 | import type { |
2 | 4 | QaBusConversation, |
3 | 5 | QaBusEvent, |
@@ -32,27 +34,78 @@ export type { |
32 | 34 |
|
33 | 35 | type JsonResult<T> = Promise<T>; |
34 | 36 |
|
| 37 | +function buildQaBusUrl(baseUrl: string, path: string): URL { |
| 38 | + const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; |
| 39 | + return new URL(path.replace(/^\/+/, ""), normalizedBaseUrl); |
| 40 | +} |
| 41 | + |
35 | 42 | async function postJson<T>( |
36 | 43 | baseUrl: string, |
37 | 44 | path: string, |
38 | 45 | body: unknown, |
39 | 46 | signal?: AbortSignal, |
40 | 47 | ): JsonResult<T> { |
41 | | - const response = await fetch(`${baseUrl}${path}`, { |
42 | | - method: "POST", |
43 | | - headers: { |
44 | | - "content-type": "application/json", |
45 | | - }, |
46 | | - body: JSON.stringify(body), |
47 | | - signal, |
| 48 | + const url = buildQaBusUrl(baseUrl, path); |
| 49 | + const payload = JSON.stringify(body); |
| 50 | + const client = url.protocol === "https:" ? https : http; |
| 51 | + |
| 52 | + return await new Promise<T>((resolve, reject) => { |
| 53 | + const abortError = () => |
| 54 | + Object.assign(new Error("The operation was aborted"), { name: "AbortError" }); |
| 55 | + if (signal?.aborted) { |
| 56 | + reject(abortError()); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + const request = client.request( |
| 61 | + url, |
| 62 | + { |
| 63 | + method: "POST", |
| 64 | + headers: { |
| 65 | + "content-type": "application/json", |
| 66 | + "content-length": Buffer.byteLength(payload), |
| 67 | + connection: "close", |
| 68 | + }, |
| 69 | + }, |
| 70 | + (response) => { |
| 71 | + const chunks: Buffer[] = []; |
| 72 | + response.on("data", (chunk) => { |
| 73 | + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); |
| 74 | + }); |
| 75 | + response.on("end", () => { |
| 76 | + const text = Buffer.concat(chunks).toString("utf8"); |
| 77 | + let parsed: T | { error?: string }; |
| 78 | + try { |
| 79 | + parsed = text ? (JSON.parse(text) as T | { error?: string }) : ({} as T); |
| 80 | + } catch (error) { |
| 81 | + reject(error); |
| 82 | + return; |
| 83 | + } |
| 84 | + if ((response.statusCode ?? 500) < 200 || (response.statusCode ?? 500) >= 300) { |
| 85 | + const error = |
| 86 | + typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : undefined; |
| 87 | + reject(new Error(error || `qa-bus request failed: ${response.statusCode ?? 500}`)); |
| 88 | + return; |
| 89 | + } |
| 90 | + resolve(parsed as T); |
| 91 | + }); |
| 92 | + response.on("error", reject); |
| 93 | + }, |
| 94 | + ); |
| 95 | + |
| 96 | + const onAbort = () => { |
| 97 | + request.destroy(abortError()); |
| 98 | + }; |
| 99 | + signal?.addEventListener("abort", onAbort, { once: true }); |
| 100 | + request.on("error", (error) => { |
| 101 | + signal?.removeEventListener("abort", onAbort); |
| 102 | + reject(error); |
| 103 | + }); |
| 104 | + request.on("close", () => { |
| 105 | + signal?.removeEventListener("abort", onAbort); |
| 106 | + }); |
| 107 | + request.end(payload); |
48 | 108 | }); |
49 | | - const payload = (await response.json()) as T | { error?: string }; |
50 | | - if (!response.ok) { |
51 | | - const error = |
52 | | - typeof payload === "object" && payload && "error" in payload ? payload.error : undefined; |
53 | | - throw new Error(error || `qa-bus request failed: ${response.status}`); |
54 | | - } |
55 | | - return payload as T; |
56 | 109 | } |
57 | 110 |
|
58 | 111 | export function normalizeQaTarget(raw: string): string | undefined { |
@@ -218,7 +271,7 @@ export async function injectQaBusInboundMessage(params: { |
218 | 271 | } |
219 | 272 |
|
220 | 273 | export async function getQaBusState(baseUrl: string): Promise<QaBusStateSnapshot> { |
221 | | - const response = await fetch(`${baseUrl}/v1/state`); |
| 274 | + const response = await fetch(buildQaBusUrl(baseUrl, "/v1/state")); |
222 | 275 | if (!response.ok) { |
223 | 276 | throw new Error(`qa-bus request failed: ${response.status}`); |
224 | 277 | } |
|
0 commit comments