@zlink-systems/http-client 0.10.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/LICENSE +202 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/browser/index.mjs +818 -0
- package/dist/client.d.ts +67 -0
- package/dist/client.js +203 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/request-builder.d.ts +60 -0
- package/dist/request-builder.js +291 -0
- package/dist/runtime/browser-runtime.d.ts +12 -0
- package/dist/runtime/browser-runtime.js +123 -0
- package/dist/runtime/compression.d.ts +11 -0
- package/dist/runtime/compression.js +42 -0
- package/dist/runtime/cookie-jar.d.ts +5 -0
- package/dist/runtime/cookie-jar.js +84 -0
- package/dist/runtime/options.d.ts +22 -0
- package/dist/runtime/options.js +3 -0
- package/dist/runtime/redirect-policy.d.ts +13 -0
- package/dist/runtime/redirect-policy.js +53 -0
- package/dist/runtime/request-performer.d.ts +35 -0
- package/dist/runtime/request-performer.js +158 -0
- package/dist/runtime/response-body-reader.d.ts +19 -0
- package/dist/runtime/response-body-reader.js +89 -0
- package/dist/runtime/retry-policy.d.ts +14 -0
- package/dist/runtime/retry-policy.js +69 -0
- package/dist/runtime/runtime.d.ts +16 -0
- package/dist/runtime/runtime.js +33 -0
- package/dist/runtime/text.d.ts +5 -0
- package/dist/runtime/text.js +44 -0
- package/dist/runtime/transport-factory.d.ts +9 -0
- package/dist/runtime/transport-factory.js +39 -0
- package/dist/runtime/version.d.ts +1 -0
- package/dist/runtime/version.js +12 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.js +3 -0
- package/package.json +22 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { HttpClientRuntime } from './runtime/runtime';
|
|
2
|
+
import { ZLinkHttpRequestBuilder } from './request-builder';
|
|
3
|
+
import type { ZLinkHttpExecutionScheduler } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* ZLink-style fluent HTTP client. Wraps undici behind a builder so transport types never leak into
|
|
6
|
+
* application code. A general HTTP client; the typed-JSON path (`body(dto)` / `async<T>()`) is a
|
|
7
|
+
* convenience layer on top. Mirrors the C++ `zlink::http_client::client_t`.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ZLinkHttpClient {
|
|
10
|
+
private readonly runtimeInstance;
|
|
11
|
+
readonly executionScheduler?: ZLinkHttpExecutionScheduler | undefined;
|
|
12
|
+
constructor(runtimeInstance: HttpClientRuntime, executionScheduler?: ZLinkHttpExecutionScheduler | undefined);
|
|
13
|
+
/** @internal */
|
|
14
|
+
get runtime(): HttpClientRuntime;
|
|
15
|
+
static create(baseUrl?: string): ZLinkHttpClientBuilder;
|
|
16
|
+
get(path: string): ZLinkHttpRequestBuilder;
|
|
17
|
+
post(path: string): ZLinkHttpRequestBuilder;
|
|
18
|
+
put(path: string): ZLinkHttpRequestBuilder;
|
|
19
|
+
delete(path: string): ZLinkHttpRequestBuilder;
|
|
20
|
+
patch(path: string): ZLinkHttpRequestBuilder;
|
|
21
|
+
head(path: string): ZLinkHttpRequestBuilder;
|
|
22
|
+
options(path: string): ZLinkHttpRequestBuilder;
|
|
23
|
+
/** Releases the underlying dispatcher (connection pool). */
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/** Fluent builder for {@link ZLinkHttpClient}. Mirrors the C++ `client_builder_t`. */
|
|
27
|
+
export declare class ZLinkHttpClientBuilder {
|
|
28
|
+
private baseUrlValue;
|
|
29
|
+
private timeoutMsValue;
|
|
30
|
+
private maxResponseBodySizeValue;
|
|
31
|
+
private readonly headersValue;
|
|
32
|
+
private trustCertificateFileValue;
|
|
33
|
+
private clientCertificateValue;
|
|
34
|
+
private followRedirectsValue;
|
|
35
|
+
private retryAttemptsValue;
|
|
36
|
+
private cookiesValue;
|
|
37
|
+
private proxyValue;
|
|
38
|
+
private proxyAuthorizationValue;
|
|
39
|
+
private compressionValue;
|
|
40
|
+
private executionSchedulerValue;
|
|
41
|
+
baseUrl(value: string): this;
|
|
42
|
+
timeout(milliseconds: number): this;
|
|
43
|
+
defaultHeader(name: string, value: string): this;
|
|
44
|
+
basicAuth(user: string, password: string): this;
|
|
45
|
+
bearerToken(token: string): this;
|
|
46
|
+
maxResponseBodySize(bytes: number): this;
|
|
47
|
+
trustCertificateFile(path: string): this;
|
|
48
|
+
clientCertificateFile(certificatePath: string, keyPath: string): this;
|
|
49
|
+
followRedirects(maxRedirects?: number): this;
|
|
50
|
+
retry(attempts: number): this;
|
|
51
|
+
cookies(): this;
|
|
52
|
+
proxy(url: string): this;
|
|
53
|
+
proxyBasicAuth(user: string, password: string): this;
|
|
54
|
+
compression(): this;
|
|
55
|
+
/** Supplies the framework turn scheduler used by server-side terminators. */
|
|
56
|
+
executionScheduler(scheduler: ZLinkHttpExecutionScheduler): this;
|
|
57
|
+
/** @internal Captures a turn for one-shot requests before the client is built. */
|
|
58
|
+
captureExecutionTurn(): import("./types").ZLinkHttpExecutionTurn | undefined;
|
|
59
|
+
build(): ZLinkHttpClient;
|
|
60
|
+
get(path: string): ZLinkHttpRequestBuilder;
|
|
61
|
+
post(path: string): ZLinkHttpRequestBuilder;
|
|
62
|
+
put(path: string): ZLinkHttpRequestBuilder;
|
|
63
|
+
delete(path: string): ZLinkHttpRequestBuilder;
|
|
64
|
+
patch(path: string): ZLinkHttpRequestBuilder;
|
|
65
|
+
head(path: string): ZLinkHttpRequestBuilder;
|
|
66
|
+
options(path: string): ZLinkHttpRequestBuilder;
|
|
67
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ZLinkHttpClientBuilder = exports.ZLinkHttpClient = void 0;
|
|
5
|
+
const framework_1 = require("@zlink-systems/framework");
|
|
6
|
+
const runtime_1 = require("./runtime/runtime");
|
|
7
|
+
const text_1 = require("./runtime/text");
|
|
8
|
+
const request_builder_1 = require("./request-builder");
|
|
9
|
+
/**
|
|
10
|
+
* ZLink-style fluent HTTP client. Wraps undici behind a builder so transport types never leak into
|
|
11
|
+
* application code. A general HTTP client; the typed-JSON path (`body(dto)` / `async<T>()`) is a
|
|
12
|
+
* convenience layer on top. Mirrors the C++ `zlink::http_client::client_t`.
|
|
13
|
+
*/
|
|
14
|
+
class ZLinkHttpClient {
|
|
15
|
+
runtimeInstance;
|
|
16
|
+
executionScheduler;
|
|
17
|
+
constructor(runtimeInstance, executionScheduler) {
|
|
18
|
+
this.runtimeInstance = runtimeInstance;
|
|
19
|
+
this.executionScheduler = executionScheduler;
|
|
20
|
+
}
|
|
21
|
+
/** @internal */
|
|
22
|
+
get runtime() {
|
|
23
|
+
return this.runtimeInstance;
|
|
24
|
+
}
|
|
25
|
+
static create(baseUrl) {
|
|
26
|
+
const builder = new ZLinkHttpClientBuilder();
|
|
27
|
+
return baseUrl === undefined ? builder : builder.baseUrl(baseUrl);
|
|
28
|
+
}
|
|
29
|
+
get(path) {
|
|
30
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'GET', path);
|
|
31
|
+
}
|
|
32
|
+
post(path) {
|
|
33
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'POST', path);
|
|
34
|
+
}
|
|
35
|
+
put(path) {
|
|
36
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'PUT', path);
|
|
37
|
+
}
|
|
38
|
+
delete(path) {
|
|
39
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'DELETE', path);
|
|
40
|
+
}
|
|
41
|
+
patch(path) {
|
|
42
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'PATCH', path);
|
|
43
|
+
}
|
|
44
|
+
head(path) {
|
|
45
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'HEAD', path);
|
|
46
|
+
}
|
|
47
|
+
options(path) {
|
|
48
|
+
return (0, request_builder_1.createZLinkHttpRequestBuilder)(this, 'OPTIONS', path);
|
|
49
|
+
}
|
|
50
|
+
/** Releases the underlying dispatcher (connection pool). */
|
|
51
|
+
async close() {
|
|
52
|
+
await this.runtimeInstance.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.ZLinkHttpClient = ZLinkHttpClient;
|
|
56
|
+
/** Fluent builder for {@link ZLinkHttpClient}. Mirrors the C++ `client_builder_t`. */
|
|
57
|
+
class ZLinkHttpClientBuilder {
|
|
58
|
+
baseUrlValue = '';
|
|
59
|
+
timeoutMsValue = 3000;
|
|
60
|
+
maxResponseBodySizeValue = 16 * 1024 * 1024;
|
|
61
|
+
headersValue = {};
|
|
62
|
+
trustCertificateFileValue;
|
|
63
|
+
clientCertificateValue;
|
|
64
|
+
followRedirectsValue = 0;
|
|
65
|
+
retryAttemptsValue = 0;
|
|
66
|
+
cookiesValue = false;
|
|
67
|
+
proxyValue;
|
|
68
|
+
proxyAuthorizationValue;
|
|
69
|
+
compressionValue = false;
|
|
70
|
+
executionSchedulerValue;
|
|
71
|
+
baseUrl(value) {
|
|
72
|
+
(0, text_1.requireNonBlank)(value, 'HTTP client base_url is required');
|
|
73
|
+
this.baseUrlValue = value;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
timeout(milliseconds) {
|
|
77
|
+
(0, text_1.requirePositiveTimeout)(milliseconds);
|
|
78
|
+
this.timeoutMsValue = milliseconds;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
defaultHeader(name, value) {
|
|
82
|
+
(0, text_1.requireNonBlank)(name, 'HTTP client default header name is required');
|
|
83
|
+
this.headersValue[name.toLowerCase()] = value;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
basicAuth(user, password) {
|
|
87
|
+
(0, text_1.requireNonBlank)(user, 'HTTP client basic auth user is required');
|
|
88
|
+
this.headersValue['authorization'] = (0, text_1.basicAuthorization)(user, password);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
bearerToken(token) {
|
|
92
|
+
(0, text_1.requireNonBlank)(token, 'HTTP client bearer token is required');
|
|
93
|
+
this.headersValue['authorization'] = `Bearer ${token}`;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
maxResponseBodySize(bytes) {
|
|
97
|
+
if (!(bytes > 0)) {
|
|
98
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP client max response body size must be greater than zero');
|
|
99
|
+
}
|
|
100
|
+
this.maxResponseBodySizeValue = bytes;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
trustCertificateFile(path) {
|
|
104
|
+
(0, text_1.requireNonBlank)(path, 'HTTP client trust certificate file is required');
|
|
105
|
+
this.trustCertificateFileValue = path;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
clientCertificateFile(certificatePath, keyPath) {
|
|
109
|
+
(0, text_1.requireNonBlank)(certificatePath, 'HTTP client certificate file is required');
|
|
110
|
+
(0, text_1.requireNonBlank)(keyPath, 'HTTP client certificate key file is required');
|
|
111
|
+
this.clientCertificateValue = { certificatePath, keyPath };
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
followRedirects(maxRedirects = 5) {
|
|
115
|
+
if (!(maxRedirects > 0)) {
|
|
116
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP client follow_redirects must be greater than zero');
|
|
117
|
+
}
|
|
118
|
+
this.followRedirectsValue = maxRedirects;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
retry(attempts) {
|
|
122
|
+
if (!(attempts > 0)) {
|
|
123
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP client retry attempts must be greater than zero');
|
|
124
|
+
}
|
|
125
|
+
this.retryAttemptsValue = attempts;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
cookies() {
|
|
129
|
+
this.cookiesValue = true;
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
proxy(url) {
|
|
133
|
+
(0, text_1.requireNonBlank)(url, 'HTTP client proxy url is required');
|
|
134
|
+
if (!url.startsWith('http://')) {
|
|
135
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP client proxy url must start with http://');
|
|
136
|
+
}
|
|
137
|
+
this.proxyValue = url;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
proxyBasicAuth(user, password) {
|
|
141
|
+
(0, text_1.requireNonBlank)(user, 'HTTP client proxy auth user is required');
|
|
142
|
+
this.proxyAuthorizationValue = (0, text_1.basicAuthorization)(user, password);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
compression() {
|
|
146
|
+
this.compressionValue = true;
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/** Supplies the framework turn scheduler used by server-side terminators. */
|
|
150
|
+
executionScheduler(scheduler) {
|
|
151
|
+
this.executionSchedulerValue = scheduler;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/** @internal Captures a turn for one-shot requests before the client is built. */
|
|
155
|
+
captureExecutionTurn() {
|
|
156
|
+
return this.executionSchedulerValue?.capture();
|
|
157
|
+
}
|
|
158
|
+
build() {
|
|
159
|
+
(0, text_1.requireNonBlank)(this.baseUrlValue, 'HTTP client base_url is required');
|
|
160
|
+
(0, text_1.requirePositiveTimeout)(this.timeoutMsValue);
|
|
161
|
+
const lower = this.baseUrlValue.toLowerCase();
|
|
162
|
+
if (!lower.startsWith('http://') && !lower.startsWith('https://')) {
|
|
163
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP client base_url must start with http:// or https://');
|
|
164
|
+
}
|
|
165
|
+
const options = {
|
|
166
|
+
baseUrl: this.baseUrlValue,
|
|
167
|
+
timeoutMs: this.timeoutMsValue,
|
|
168
|
+
maxResponseBodySize: this.maxResponseBodySizeValue,
|
|
169
|
+
headers: { ...this.headersValue },
|
|
170
|
+
...(this.trustCertificateFileValue !== undefined ? { trustCertificateFile: this.trustCertificateFileValue } : {}),
|
|
171
|
+
...(this.clientCertificateValue !== undefined ? { clientCertificate: this.clientCertificateValue } : {}),
|
|
172
|
+
followRedirects: this.followRedirectsValue,
|
|
173
|
+
retryAttempts: this.retryAttemptsValue,
|
|
174
|
+
cookies: this.cookiesValue,
|
|
175
|
+
...(this.proxyValue !== undefined ? { proxy: this.proxyValue } : {}),
|
|
176
|
+
...(this.proxyAuthorizationValue !== undefined ? { proxyAuthorization: this.proxyAuthorizationValue } : {}),
|
|
177
|
+
compression: this.compressionValue,
|
|
178
|
+
};
|
|
179
|
+
return new ZLinkHttpClient(new runtime_1.HttpClientRuntime(options), this.executionSchedulerValue);
|
|
180
|
+
}
|
|
181
|
+
get(path) {
|
|
182
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'GET', path, this);
|
|
183
|
+
}
|
|
184
|
+
post(path) {
|
|
185
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'POST', path, this);
|
|
186
|
+
}
|
|
187
|
+
put(path) {
|
|
188
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'PUT', path, this);
|
|
189
|
+
}
|
|
190
|
+
delete(path) {
|
|
191
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'DELETE', path, this);
|
|
192
|
+
}
|
|
193
|
+
patch(path) {
|
|
194
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'PATCH', path, this);
|
|
195
|
+
}
|
|
196
|
+
head(path) {
|
|
197
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'HEAD', path, this);
|
|
198
|
+
}
|
|
199
|
+
options(path) {
|
|
200
|
+
return new request_builder_1.ZLinkHttpRequestBuilder(undefined, 'OPTIONS', path, this);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
exports.ZLinkHttpClientBuilder = ZLinkHttpClientBuilder;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ZLinkHttpClient, ZLinkHttpClientBuilder } from './client';
|
|
2
|
+
export { ZLinkHttpRequestBuilder } from './request-builder';
|
|
3
|
+
export type { ZLinkHttpMethod, RawHttpResponse, HttpResponse, BodyChunkProvider, DownloadSink, ZLinkHttpCallback, ZLinkHttpExecutionScheduler, ZLinkHttpExecutionTurn, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ZLinkHttpRequestBuilder = exports.ZLinkHttpClientBuilder = exports.ZLinkHttpClient = void 0;
|
|
5
|
+
var client_1 = require("./client");
|
|
6
|
+
Object.defineProperty(exports, "ZLinkHttpClient", { enumerable: true, get: function () { return client_1.ZLinkHttpClient; } });
|
|
7
|
+
Object.defineProperty(exports, "ZLinkHttpClientBuilder", { enumerable: true, get: function () { return client_1.ZLinkHttpClientBuilder; } });
|
|
8
|
+
var request_builder_1 = require("./request-builder");
|
|
9
|
+
Object.defineProperty(exports, "ZLinkHttpRequestBuilder", { enumerable: true, get: function () { return request_builder_1.ZLinkHttpRequestBuilder; } });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ZLinkHttpClient, ZLinkHttpClientBuilder } from './client';
|
|
2
|
+
import type { BodyChunkProvider, DownloadSink, HttpResponse, RawHttpResponse, ZLinkHttpCallback, ZLinkHttpExecutionScheduler, ZLinkHttpExecutionTurn, ZLinkHttpMethod } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Fluent builder for a single request. Mirrors the C++ `request_builder_t`. Submission returns a
|
|
5
|
+
* `Promise`; the event loop is never blocked while the request is in flight.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ZLinkHttpRequestBuilder {
|
|
8
|
+
private readonly method;
|
|
9
|
+
private readonly path;
|
|
10
|
+
private bodyValue;
|
|
11
|
+
private bodyProviderValue;
|
|
12
|
+
private readonly headersValue;
|
|
13
|
+
private timeoutMsValue;
|
|
14
|
+
private readonly queryValue;
|
|
15
|
+
private readonly formValue;
|
|
16
|
+
private readonly multipartValue;
|
|
17
|
+
private clientInstance;
|
|
18
|
+
private readonly clientFactory;
|
|
19
|
+
private readonly ownsClient;
|
|
20
|
+
protected readonly executionTurn: ZLinkHttpExecutionTurn | undefined;
|
|
21
|
+
protected readonly executionScheduler: ZLinkHttpExecutionScheduler | undefined;
|
|
22
|
+
private consumed;
|
|
23
|
+
constructor(client: ZLinkHttpClient | undefined, method: ZLinkHttpMethod, path: string, clientFactory?: ZLinkHttpClientBuilder);
|
|
24
|
+
private resolveClient;
|
|
25
|
+
private closeIfOwned;
|
|
26
|
+
header(name: string, value: string): this;
|
|
27
|
+
query(name: string, value: string): this;
|
|
28
|
+
timeout(milliseconds: number): this;
|
|
29
|
+
/** Sets a typed JSON body (1 arg) or a raw body with explicit content type (2 args). */
|
|
30
|
+
body<T>(value: T): this;
|
|
31
|
+
body(content: string, contentType: string): this;
|
|
32
|
+
/**
|
|
33
|
+
* Streams the request body chunk by chunk with chunked transfer-encoding; the provider returns
|
|
34
|
+
* `null` when the body is complete. Streamed requests are excluded from retry.
|
|
35
|
+
*/
|
|
36
|
+
bodyStream(provider: BodyChunkProvider, contentType: string): this;
|
|
37
|
+
form(name: string, value: string): this;
|
|
38
|
+
multipart(name: string, value: string): this;
|
|
39
|
+
multipartFile(name: string, filename: string, content: string, contentType: string): this;
|
|
40
|
+
/** Executes the request and returns the raw response while retaining the current turn. */
|
|
41
|
+
submitRaw(): Promise<RawHttpResponse>;
|
|
42
|
+
/**
|
|
43
|
+
* Streams the response body to `sink` chunk by chunk instead of buffering it; the returned
|
|
44
|
+
* response carries status and headers with an empty body (no decompression of chunks).
|
|
45
|
+
*/
|
|
46
|
+
download(sink: DownloadSink): Promise<RawHttpResponse>;
|
|
47
|
+
async<T>(): Promise<HttpResponse<T>>;
|
|
48
|
+
async<T>(callback: ZLinkHttpCallback<T>): void;
|
|
49
|
+
protected executeTyped<T>(): Promise<HttpResponse<T>>;
|
|
50
|
+
/** Returns only the decoded body for client-side scenarios that do not need the HTTP envelope. */
|
|
51
|
+
fetch<T>(): Promise<T>;
|
|
52
|
+
private completeCallback;
|
|
53
|
+
private makeRequest;
|
|
54
|
+
private resolveTarget;
|
|
55
|
+
private resolveBodyAndHeaders;
|
|
56
|
+
private countBodySources;
|
|
57
|
+
private encodeFormBody;
|
|
58
|
+
private encodeMultipartBody;
|
|
59
|
+
}
|
|
60
|
+
export declare function createZLinkHttpRequestBuilder(client: ZLinkHttpClient, method: ZLinkHttpMethod, path: string): ZLinkHttpRequestBuilder;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ZLinkHttpRequestBuilder = void 0;
|
|
5
|
+
exports.createZLinkHttpRequestBuilder = createZLinkHttpRequestBuilder;
|
|
6
|
+
const framework_1 = require("@zlink-systems/framework");
|
|
7
|
+
const text_1 = require("./runtime/text");
|
|
8
|
+
/**
|
|
9
|
+
* Fluent builder for a single request. Mirrors the C++ `request_builder_t`. Submission returns a
|
|
10
|
+
* `Promise`; the event loop is never blocked while the request is in flight.
|
|
11
|
+
*/
|
|
12
|
+
class ZLinkHttpRequestBuilder {
|
|
13
|
+
method;
|
|
14
|
+
path;
|
|
15
|
+
bodyValue;
|
|
16
|
+
bodyProviderValue;
|
|
17
|
+
headersValue = {};
|
|
18
|
+
timeoutMsValue;
|
|
19
|
+
queryValue = [];
|
|
20
|
+
formValue = [];
|
|
21
|
+
multipartValue = [];
|
|
22
|
+
clientInstance;
|
|
23
|
+
clientFactory;
|
|
24
|
+
ownsClient;
|
|
25
|
+
executionTurn;
|
|
26
|
+
executionScheduler;
|
|
27
|
+
consumed = false;
|
|
28
|
+
constructor(client, method, path, clientFactory) {
|
|
29
|
+
this.method = method;
|
|
30
|
+
this.path = path;
|
|
31
|
+
this.clientInstance = client;
|
|
32
|
+
this.clientFactory = clientFactory;
|
|
33
|
+
// A one-shot request (builder verb shortcut) owns the lazily-built client and closes it once the
|
|
34
|
+
// request completes.
|
|
35
|
+
this.ownsClient = clientFactory !== undefined;
|
|
36
|
+
this.executionScheduler = client?.executionScheduler;
|
|
37
|
+
this.executionTurn = client?.executionScheduler?.capture()
|
|
38
|
+
?? clientFactory?.captureExecutionTurn();
|
|
39
|
+
if (path.length === 0 || path[0] !== '/') {
|
|
40
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP request path must start with /');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Resolves the client to run on, building a one-shot client lazily. A one-shot request builder is
|
|
44
|
+
// single-use so its lazily-built client is closed exactly once.
|
|
45
|
+
resolveClient() {
|
|
46
|
+
if (this.ownsClient && this.consumed) {
|
|
47
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'A one-shot HTTP request can only be submitted once');
|
|
48
|
+
}
|
|
49
|
+
this.consumed = true;
|
|
50
|
+
if (this.clientInstance === undefined) {
|
|
51
|
+
if (this.clientFactory === undefined) {
|
|
52
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP request has no client');
|
|
53
|
+
}
|
|
54
|
+
this.clientInstance = this.clientFactory.build();
|
|
55
|
+
}
|
|
56
|
+
return this.clientInstance;
|
|
57
|
+
}
|
|
58
|
+
async closeIfOwned() {
|
|
59
|
+
if (this.ownsClient && this.clientInstance !== undefined) {
|
|
60
|
+
// A one-shot cleanup failure must not mask the request result.
|
|
61
|
+
await this.clientInstance.close().catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
header(name, value) {
|
|
65
|
+
(0, text_1.requireNonBlank)(name, 'HTTP request header name is required');
|
|
66
|
+
this.headersValue[name.toLowerCase()] = value;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
query(name, value) {
|
|
70
|
+
(0, text_1.requireNonBlank)(name, 'HTTP request query name is required');
|
|
71
|
+
this.queryValue.push([name, value]);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
timeout(milliseconds) {
|
|
75
|
+
(0, text_1.requirePositiveTimeout)(milliseconds);
|
|
76
|
+
this.timeoutMsValue = milliseconds;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
body(value, contentType) {
|
|
80
|
+
if (contentType !== undefined) {
|
|
81
|
+
if (typeof value !== 'string') {
|
|
82
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP request raw body content is required');
|
|
83
|
+
}
|
|
84
|
+
(0, text_1.requireNonBlank)(contentType, 'HTTP request body content type is required');
|
|
85
|
+
this.bodyValue = value;
|
|
86
|
+
this.headersValue['content-type'] = contentType;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
this.bodyValue = JSON.stringify(value);
|
|
90
|
+
this.headersValue['content-type'] ??= 'application/json';
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Streams the request body chunk by chunk with chunked transfer-encoding; the provider returns
|
|
95
|
+
* `null` when the body is complete. Streamed requests are excluded from retry.
|
|
96
|
+
*/
|
|
97
|
+
bodyStream(provider, contentType) {
|
|
98
|
+
if (typeof provider !== 'function') {
|
|
99
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP request body stream provider is required');
|
|
100
|
+
}
|
|
101
|
+
(0, text_1.requireNonBlank)(contentType, 'HTTP request body content type is required');
|
|
102
|
+
this.bodyProviderValue = provider;
|
|
103
|
+
this.headersValue['content-type'] = contentType;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
form(name, value) {
|
|
107
|
+
(0, text_1.requireNonBlank)(name, 'HTTP request form field name is required');
|
|
108
|
+
this.formValue.push([name, value]);
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
multipart(name, value) {
|
|
112
|
+
(0, text_1.requireNonBlank)(name, 'HTTP request multipart field name is required');
|
|
113
|
+
this.multipartValue.push({ name, filename: '', content: value, contentType: '' });
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
multipartFile(name, filename, content, contentType) {
|
|
117
|
+
(0, text_1.requireNonBlank)(name, 'HTTP request multipart field name is required');
|
|
118
|
+
(0, text_1.requireNonBlank)(filename, 'HTTP request multipart filename is required');
|
|
119
|
+
(0, text_1.requireNonBlank)(contentType, 'HTTP request multipart content type is required');
|
|
120
|
+
this.multipartValue.push({ name, filename, content, contentType });
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
/** Executes the request and returns the raw response while retaining the current turn. */
|
|
124
|
+
async submitRaw() {
|
|
125
|
+
const request = this.makeRequest(undefined);
|
|
126
|
+
const client = this.resolveClient();
|
|
127
|
+
try {
|
|
128
|
+
return await client.runtime.executeAsync(request);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await this.closeIfOwned();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Streams the response body to `sink` chunk by chunk instead of buffering it; the returned
|
|
136
|
+
* response carries status and headers with an empty body (no decompression of chunks).
|
|
137
|
+
*/
|
|
138
|
+
async download(sink) {
|
|
139
|
+
if (typeof sink !== 'function') {
|
|
140
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP request download sink is required');
|
|
141
|
+
}
|
|
142
|
+
const request = this.makeRequest(sink);
|
|
143
|
+
const client = this.resolveClient();
|
|
144
|
+
try {
|
|
145
|
+
return await client.runtime.executeAsync(request);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await this.closeIfOwned();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async(callback) {
|
|
152
|
+
const pending = this.executeTyped();
|
|
153
|
+
if (callback === undefined) {
|
|
154
|
+
return pending;
|
|
155
|
+
}
|
|
156
|
+
void pending.then((response) => this.completeCallback(() => callback(undefined, response)), (error) => this.completeCallback(() => callback(error, undefined)));
|
|
157
|
+
}
|
|
158
|
+
async executeTyped() {
|
|
159
|
+
const raw = await this.submitRaw();
|
|
160
|
+
if (raw.status >= 400) {
|
|
161
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.InternalFailure, `HTTP request failed with status ${raw.status}`);
|
|
162
|
+
}
|
|
163
|
+
let body;
|
|
164
|
+
if (raw.body.length === 0) {
|
|
165
|
+
body = null;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
try {
|
|
169
|
+
body = JSON.parse(raw.body, safeJsonReviver);
|
|
170
|
+
}
|
|
171
|
+
catch (cause) {
|
|
172
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, cause instanceof Error ? cause.message : 'HTTP response body decode failed', cause);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { status: raw.status, headers: raw.headers, body, rawBody: raw.body };
|
|
176
|
+
}
|
|
177
|
+
/** Returns only the decoded body for client-side scenarios that do not need the HTTP envelope. */
|
|
178
|
+
async fetch() {
|
|
179
|
+
return (await this.executeTyped()).body;
|
|
180
|
+
}
|
|
181
|
+
completeCallback(callback) {
|
|
182
|
+
if (this.executionTurn === undefined) {
|
|
183
|
+
callback();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.executionTurn.post(callback);
|
|
187
|
+
}
|
|
188
|
+
makeRequest(sink) {
|
|
189
|
+
const { body, headers } = this.resolveBodyAndHeaders();
|
|
190
|
+
return {
|
|
191
|
+
method: this.method,
|
|
192
|
+
target: this.resolveTarget(),
|
|
193
|
+
...(body !== undefined ? { body } : {}),
|
|
194
|
+
...(this.bodyProviderValue !== undefined ? { bodyProvider: this.bodyProviderValue } : {}),
|
|
195
|
+
headers,
|
|
196
|
+
...(this.timeoutMsValue !== undefined ? { timeoutMs: this.timeoutMsValue } : {}),
|
|
197
|
+
...(sink !== undefined ? { sink } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
resolveTarget() {
|
|
201
|
+
if (this.queryValue.length === 0) {
|
|
202
|
+
return this.path;
|
|
203
|
+
}
|
|
204
|
+
let target = this.path;
|
|
205
|
+
let separator = this.path.includes('?') ? '&' : '?';
|
|
206
|
+
for (const [name, value] of this.queryValue) {
|
|
207
|
+
target += `${separator}${(0, text_1.percentEncode)(name)}=${(0, text_1.percentEncode)(value)}`;
|
|
208
|
+
separator = '&';
|
|
209
|
+
}
|
|
210
|
+
return target;
|
|
211
|
+
}
|
|
212
|
+
resolveBodyAndHeaders() {
|
|
213
|
+
if (this.countBodySources() > 1) {
|
|
214
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP request accepts a single body source: body, body_stream, form, or multipart');
|
|
215
|
+
}
|
|
216
|
+
const headers = { ...this.headersValue };
|
|
217
|
+
if (this.bodyValue !== undefined) {
|
|
218
|
+
return { body: this.bodyValue, headers };
|
|
219
|
+
}
|
|
220
|
+
if (this.formValue.length > 0) {
|
|
221
|
+
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
222
|
+
return { body: this.encodeFormBody(), headers };
|
|
223
|
+
}
|
|
224
|
+
if (this.multipartValue.length > 0) {
|
|
225
|
+
const boundary = (0, text_1.makeMultipartBoundary)();
|
|
226
|
+
headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
|
|
227
|
+
return { body: this.encodeMultipartBody(boundary), headers };
|
|
228
|
+
}
|
|
229
|
+
return { body: undefined, headers };
|
|
230
|
+
}
|
|
231
|
+
countBodySources() {
|
|
232
|
+
return ((this.bodyValue !== undefined ? 1 : 0) +
|
|
233
|
+
(this.bodyProviderValue !== undefined ? 1 : 0) +
|
|
234
|
+
(this.formValue.length > 0 ? 1 : 0) +
|
|
235
|
+
(this.multipartValue.length > 0 ? 1 : 0));
|
|
236
|
+
}
|
|
237
|
+
encodeFormBody() {
|
|
238
|
+
return this.formValue
|
|
239
|
+
.map(([name, value]) => `${(0, text_1.percentEncode)(name)}=${(0, text_1.percentEncode)(value)}`)
|
|
240
|
+
.join('&');
|
|
241
|
+
}
|
|
242
|
+
encodeMultipartBody(boundary) {
|
|
243
|
+
let encoded = '';
|
|
244
|
+
for (const part of this.multipartValue) {
|
|
245
|
+
encoded += `--${boundary}\r\n`;
|
|
246
|
+
encoded += `Content-Disposition: form-data; name="${part.name}"`;
|
|
247
|
+
if (part.filename.length > 0) {
|
|
248
|
+
encoded += `; filename="${part.filename}"`;
|
|
249
|
+
}
|
|
250
|
+
encoded += '\r\n';
|
|
251
|
+
if (part.contentType.length > 0) {
|
|
252
|
+
encoded += `Content-Type: ${part.contentType}\r\n`;
|
|
253
|
+
}
|
|
254
|
+
encoded += '\r\n';
|
|
255
|
+
encoded += part.content;
|
|
256
|
+
encoded += '\r\n';
|
|
257
|
+
}
|
|
258
|
+
encoded += `--${boundary}--\r\n`;
|
|
259
|
+
return encoded;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
exports.ZLinkHttpRequestBuilder = ZLinkHttpRequestBuilder;
|
|
263
|
+
class ZLinkFrameworkHttpRequestBuilder extends ZLinkHttpRequestBuilder {
|
|
264
|
+
/** Starts a server-side one-way request and ignores its response body. */
|
|
265
|
+
async submit() {
|
|
266
|
+
if (this.executionScheduler === undefined) {
|
|
267
|
+
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP submit requires a framework server client');
|
|
268
|
+
}
|
|
269
|
+
await this.submitRaw();
|
|
270
|
+
}
|
|
271
|
+
/** Executes a typed request while yielding the current Spot turn. */
|
|
272
|
+
yield() {
|
|
273
|
+
if (this.executionTurn === undefined) {
|
|
274
|
+
return Promise.reject(new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP yield requires a framework Spot turn'));
|
|
275
|
+
}
|
|
276
|
+
return this.executionTurn.yieldPromise(this.executeTyped());
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function createZLinkHttpRequestBuilder(client, method, path) {
|
|
280
|
+
return client.executionScheduler === undefined
|
|
281
|
+
? new ZLinkHttpRequestBuilder(client, method, path)
|
|
282
|
+
: new ZLinkFrameworkHttpRequestBuilder(client, method, path);
|
|
283
|
+
}
|
|
284
|
+
// Prototype-pollution guard, mirroring the framework's stream JSON codec.
|
|
285
|
+
const prototypeKeys = new Set(['__proto__', 'constructor', 'prototype']);
|
|
286
|
+
function safeJsonReviver(key, value) {
|
|
287
|
+
if (prototypeKeys.has(key)) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HttpClientOptions } from './options';
|
|
2
|
+
import type { HttpRequestSpec, RawResult } from './request-performer';
|
|
3
|
+
/** Browser transport for the same public client surface used by the Node runtime. */
|
|
4
|
+
export declare class HttpClientRuntime {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private readonly retryPolicy;
|
|
7
|
+
constructor(options: HttpClientOptions);
|
|
8
|
+
executeAsync(spec: HttpRequestSpec): Promise<RawResult>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
private perform;
|
|
11
|
+
private buildHeaders;
|
|
12
|
+
}
|