@telorun/http-client 0.1.1 → 0.1.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.
|
@@ -1,25 +1,30 @@
|
|
|
1
|
+
import { Readable } from "stream";
|
|
1
2
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
3
|
interface TeloResponse {
|
|
3
4
|
status: number;
|
|
4
5
|
headers: Record<string, string>;
|
|
5
6
|
body: unknown;
|
|
6
7
|
}
|
|
7
|
-
interface
|
|
8
|
-
client?: string;
|
|
8
|
+
interface HttpRequestInputs {
|
|
9
9
|
url: string;
|
|
10
10
|
method?: string;
|
|
11
11
|
query?: Record<string, string>;
|
|
12
12
|
headers?: Record<string, string>;
|
|
13
13
|
body?: string | Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
interface HttpRequestManifest extends HttpRequestInputs {
|
|
16
|
+
client?: string;
|
|
14
17
|
timeout?: number;
|
|
15
18
|
throwOnHttpError?: boolean;
|
|
16
19
|
retries?: number;
|
|
20
|
+
mode?: "buffer" | "stream";
|
|
21
|
+
inputs?: HttpRequestInputs;
|
|
17
22
|
}
|
|
18
23
|
declare class HttpRequestResource implements ResourceInstance {
|
|
19
24
|
private readonly manifest;
|
|
20
25
|
private readonly ctx;
|
|
21
26
|
constructor(manifest: HttpRequestManifest, ctx: ResourceContext);
|
|
22
|
-
invoke(input: any): Promise<TeloResponse>;
|
|
27
|
+
invoke(input: any): Promise<TeloResponse | Readable>;
|
|
23
28
|
}
|
|
24
29
|
export declare function register(): void;
|
|
25
30
|
export declare function create(resource: HttpRequestManifest, ctx: ResourceContext): Promise<HttpRequestResource>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PassThrough } from "stream";
|
|
1
2
|
const MAX_REDIRECTS = 5;
|
|
2
3
|
const DEFAULT_TIMEOUT = 10000;
|
|
3
4
|
function createNetworkError(code, message, url) {
|
|
@@ -37,7 +38,7 @@ function normalizeHeaders(headers) {
|
|
|
37
38
|
}
|
|
38
39
|
return result;
|
|
39
40
|
}
|
|
40
|
-
async function executeRequest(url, method, headers, body, timeout) {
|
|
41
|
+
async function executeRequest(url, method, headers, body, timeout, stream = false) {
|
|
41
42
|
const controller = new AbortController();
|
|
42
43
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
43
44
|
let currentUrl = url;
|
|
@@ -71,6 +72,34 @@ async function executeRequest(url, method, headers, body, timeout) {
|
|
|
71
72
|
response.headers.forEach((value, key) => {
|
|
72
73
|
responseHeaders[key.toLowerCase()] = value;
|
|
73
74
|
});
|
|
75
|
+
// Stream mode: pump body into a PassThrough eagerly so data flows immediately
|
|
76
|
+
if (stream) {
|
|
77
|
+
const webStream = response.body;
|
|
78
|
+
const body = new PassThrough();
|
|
79
|
+
(async () => {
|
|
80
|
+
if (!webStream) {
|
|
81
|
+
body.end();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const reader = webStream.getReader();
|
|
85
|
+
try {
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
if (done)
|
|
89
|
+
break;
|
|
90
|
+
body.push(Buffer.from(value));
|
|
91
|
+
}
|
|
92
|
+
body.end();
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
body.destroy(err);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
reader.releaseLock();
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
return { status: response.status, headers: responseHeaders, body };
|
|
102
|
+
}
|
|
74
103
|
// Deserialize body
|
|
75
104
|
const contentType = responseHeaders["content-type"] ?? "";
|
|
76
105
|
let responseBody;
|
|
@@ -91,13 +120,13 @@ async function executeRequest(url, method, headers, body, timeout) {
|
|
|
91
120
|
clearTimeout(timeoutId);
|
|
92
121
|
}
|
|
93
122
|
}
|
|
94
|
-
async function executeWithRetry(url, method, headers, body, timeout, retriesLeft) {
|
|
123
|
+
async function executeWithRetry(url, method, headers, body, timeout, retriesLeft, stream = false) {
|
|
95
124
|
try {
|
|
96
|
-
return await executeRequest(url, method, headers, body, timeout);
|
|
125
|
+
return await executeRequest(url, method, headers, body, timeout, stream);
|
|
97
126
|
}
|
|
98
127
|
catch (err) {
|
|
99
128
|
if (retriesLeft > 0 && err.error === "NetworkError") {
|
|
100
|
-
return executeWithRetry(url, method, headers, body, timeout, retriesLeft - 1);
|
|
129
|
+
return executeWithRetry(url, method, headers, body, timeout, retriesLeft - 1, stream);
|
|
101
130
|
}
|
|
102
131
|
throw err;
|
|
103
132
|
}
|
|
@@ -126,12 +155,22 @@ class HttpRequestResource {
|
|
|
126
155
|
clientHeaders = normalizeHeaders(client.headers ?? {});
|
|
127
156
|
clientTimeout = client.timeout ?? DEFAULT_TIMEOUT;
|
|
128
157
|
}
|
|
129
|
-
// Expand template fields
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
// Expand template fields from manifest.inputs using runtime input as context
|
|
159
|
+
// Manifest-level fields (url, method, etc.) serve as defaults when inputs is absent
|
|
160
|
+
const manifestInputs = {
|
|
161
|
+
url: m.url,
|
|
162
|
+
method: m.method,
|
|
163
|
+
query: m.query,
|
|
164
|
+
headers: m.headers,
|
|
165
|
+
body: m.body,
|
|
166
|
+
...m.inputs,
|
|
167
|
+
};
|
|
168
|
+
const resolved = ctx.expandValue(manifestInputs, input ?? {});
|
|
169
|
+
const rawUrl = resolved.url;
|
|
170
|
+
const method = ((resolved.method ?? "GET") || "GET").toUpperCase();
|
|
171
|
+
const requestHeaders = normalizeHeaders((resolved.headers ?? {}));
|
|
172
|
+
const query = (resolved.query ?? {});
|
|
173
|
+
const body = resolved.body;
|
|
135
174
|
const effectiveTimeout = m.timeout ?? clientTimeout;
|
|
136
175
|
const retries = m.retries ?? 0;
|
|
137
176
|
const throwOnHttpError = m.throwOnHttpError ?? false;
|
|
@@ -166,10 +205,13 @@ class HttpRequestResource {
|
|
|
166
205
|
serializedBody = String(body);
|
|
167
206
|
}
|
|
168
207
|
}
|
|
169
|
-
const response = await executeWithRetry(fullUrl, method, mergedHeaders, serializedBody, effectiveTimeout, retries);
|
|
208
|
+
const response = await executeWithRetry(fullUrl, method, mergedHeaders, serializedBody, effectiveTimeout, retries, m.mode === "stream");
|
|
170
209
|
if (throwOnHttpError && response.status >= 400) {
|
|
171
210
|
throw new Error(`HTTP ${response.status} error from ${fullUrl}`);
|
|
172
211
|
}
|
|
212
|
+
if (m.mode === "stream") {
|
|
213
|
+
return response.body;
|
|
214
|
+
}
|
|
173
215
|
return response;
|
|
174
216
|
}
|
|
175
217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/http-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./http-client": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist/**"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@telorun/sdk": "0.2.
|
|
19
|
+
"@telorun/sdk": "0.2.6"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.0.0",
|