@gjsify/http 0.3.12 → 0.3.14

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,228 +1,240 @@
1
+ import { IncomingMessage } from "./incoming-message.js";
2
+ import { OutgoingMessage } from "./server.js";
1
3
  import GLib from "@girs/glib-2.0";
2
4
  import Soup from "@girs/soup-3.0";
3
5
  import Gio from "@girs/gio-2.0";
4
6
  import { Buffer } from "node:buffer";
5
7
  import { URL } from "node:url";
6
8
  import { readBytesAsync } from "@gjsify/utils";
7
- import { OutgoingMessage } from "./server.js";
8
- import { IncomingMessage } from "./incoming-message.js";
9
- class ClientRequest extends OutgoingMessage {
10
- method;
11
- path;
12
- protocol;
13
- host;
14
- hostname;
15
- port;
16
- aborted = false;
17
- reusedSocket = false;
18
- maxHeadersCount = 2e3;
19
- _chunks = [];
20
- _session;
21
- _message;
22
- _cancellable;
23
- _timeout = 0;
24
- _timeoutTimer = null;
25
- _responseCallback;
26
- constructor(url, options, callback) {
27
- super();
28
- let opts;
29
- if (typeof url === "string" || url instanceof URL) {
30
- const parsed = typeof url === "string" ? new URL(url) : url;
31
- opts = {
32
- protocol: parsed.protocol,
33
- hostname: parsed.hostname,
34
- port: parsed.port ? Number(parsed.port) : void 0,
35
- path: parsed.pathname + parsed.search,
36
- ...typeof options === "object" ? options : {}
37
- };
38
- if (typeof options === "function") {
39
- callback = options;
40
- }
41
- } else {
42
- opts = url;
43
- if (typeof options === "function") {
44
- callback = options;
45
- }
46
- }
47
- this.method = (opts.method || "GET").toUpperCase();
48
- this.protocol = opts.protocol || "http:";
49
- this.hostname = opts.hostname || opts.host?.split(":")[0] || "localhost";
50
- this.port = Number(opts.port) || (this.protocol === "https:" ? 443 : 80);
51
- this.path = opts.path || "/";
52
- this.host = opts.host || `${this.hostname}:${this.port}`;
53
- this._timeout = opts.timeout || 0;
54
- if (callback) {
55
- this._responseCallback = callback;
56
- this.once("response", callback);
57
- }
58
- if (opts.headers) {
59
- for (const [key, value] of Object.entries(opts.headers)) {
60
- this.setHeader(key, value);
61
- }
62
- }
63
- if (opts.setHost !== false && !this._headers.has("host")) {
64
- const defaultPort = this.protocol === "https:" ? 443 : 80;
65
- const hostHeader = this.port === defaultPort ? this.hostname : `${this.hostname}:${this.port}`;
66
- this.setHeader("Host", hostHeader);
67
- }
68
- if (opts.auth && !this._headers.has("authorization")) {
69
- this.setHeader("Authorization", "Basic " + Buffer.from(opts.auth).toString("base64"));
70
- }
71
- if (opts.signal) {
72
- if (opts.signal.aborted) {
73
- this.abort();
74
- } else {
75
- opts.signal.addEventListener("abort", () => this.abort(), { once: true });
76
- }
77
- }
78
- const uri = GLib.Uri.parse(this._buildUrl(), GLib.UriFlags.NONE);
79
- this._session = new Soup.Session();
80
- this._message = new Soup.Message({ method: this.method, uri });
81
- this._cancellable = new Gio.Cancellable();
82
- if (this._timeout > 0) {
83
- this._session.timeout = Math.ceil(this._timeout / 1e3);
84
- this._timeoutTimer = setTimeout(() => {
85
- this._timeoutTimer = null;
86
- this.emit("timeout");
87
- }, this._timeout);
88
- }
89
- }
90
- _buildUrl() {
91
- const proto = this.protocol.endsWith(":") ? this.protocol : this.protocol + ":";
92
- const defaultPort = proto === "https:" ? 443 : 80;
93
- const portStr = this.port === defaultPort ? "" : `:${this.port}`;
94
- return `${proto}//${this.hostname}${portStr}${this.path}`;
95
- }
96
- /** Get raw header names and values as a flat array. */
97
- getRawHeaderNames() {
98
- return Array.from(this._headers.keys());
99
- }
100
- /** Flush headers — marks headers as sent. */
101
- flushHeaders() {
102
- if (!this.headersSent) {
103
- this._applyHeaders();
104
- }
105
- }
106
- /** Set timeout for the request. Emits 'timeout' if no response within msecs. */
107
- setTimeout(msecs, callback) {
108
- this._timeout = msecs;
109
- if (this._timeoutTimer) {
110
- clearTimeout(this._timeoutTimer);
111
- this._timeoutTimer = null;
112
- }
113
- if (callback) this.once("timeout", callback);
114
- if (msecs > 0) {
115
- this._session.timeout = Math.ceil(msecs / 1e3);
116
- this._timeoutTimer = setTimeout(() => {
117
- this._timeoutTimer = null;
118
- this.emit("timeout");
119
- }, msecs);
120
- }
121
- return this;
122
- }
123
- /** Abort the request. */
124
- abort() {
125
- if (this.aborted) return;
126
- this.aborted = true;
127
- if (this._timeoutTimer) {
128
- clearTimeout(this._timeoutTimer);
129
- this._timeoutTimer = null;
130
- }
131
- this._cancellable.cancel();
132
- this.emit("abort");
133
- this.destroy();
134
- }
135
- /** Writable stream _write implementation — collect body chunks. */
136
- _write(chunk, encoding, callback) {
137
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
138
- this._chunks.push(buf);
139
- callback();
140
- }
141
- /** Called when the writable stream ends — send the request. */
142
- _final(callback) {
143
- this._sendRequest().then(() => callback()).catch((err) => callback(err));
144
- }
145
- _applyHeaders() {
146
- if (this.headersSent) return;
147
- this.headersSent = true;
148
- const requestHeaders = this._message.get_request_headers();
149
- for (const [key, value] of this._headers) {
150
- if (Array.isArray(value)) {
151
- for (const v of value) {
152
- requestHeaders.append(key, v);
153
- }
154
- } else {
155
- requestHeaders.replace(key, value);
156
- }
157
- }
158
- }
159
- async _sendRequest() {
160
- this._applyHeaders();
161
- const body = Buffer.concat(this._chunks);
162
- if (body.length > 0) {
163
- const contentType = this._headers.get("content-type") || "application/octet-stream";
164
- this._message.set_request_body_from_bytes(contentType, new GLib.Bytes(body));
165
- }
166
- try {
167
- const inputStream = await new Promise((resolve, reject) => {
168
- this._session.send_async(this._message, GLib.PRIORITY_DEFAULT, this._cancellable, (_self, asyncRes) => {
169
- try {
170
- const stream = this._session.send_finish(asyncRes);
171
- resolve(stream);
172
- } catch (error) {
173
- reject(error);
174
- }
175
- });
176
- });
177
- const bodyChunks = [];
178
- try {
179
- let chunk;
180
- while ((chunk = await readBytesAsync(inputStream, 4096, GLib.PRIORITY_DEFAULT, this._cancellable)) !== null) {
181
- bodyChunks.push(Buffer.from(chunk));
182
- }
183
- } catch (readErr) {
184
- }
185
- const res = new IncomingMessage();
186
- res.statusCode = this._message.status_code;
187
- res.statusMessage = this._message.get_reason_phrase();
188
- res.httpVersion = "1.1";
189
- const responseHeaders = this._message.get_response_headers();
190
- responseHeaders.foreach((name, value) => {
191
- const lower = name.toLowerCase();
192
- res.rawHeaders.push(name, value);
193
- if (lower in res.headers) {
194
- const existing = res.headers[lower];
195
- if (Array.isArray(existing)) {
196
- existing.push(value);
197
- } else {
198
- res.headers[lower] = [existing, value];
199
- }
200
- } else {
201
- res.headers[lower] = value;
202
- }
203
- });
204
- this.finished = true;
205
- if (this._timeoutTimer) {
206
- clearTimeout(this._timeoutTimer);
207
- this._timeoutTimer = null;
208
- }
209
- this.emit("response", res);
210
- setTimeout(() => {
211
- for (const buf of bodyChunks) {
212
- res.push(buf);
213
- }
214
- res.push(null);
215
- res.complete = true;
216
- }, 0);
217
- } catch (error) {
218
- if (this.aborted) {
219
- this.emit("abort");
220
- } else {
221
- this.emit("error", error instanceof Error ? error : new Error(String(error)));
222
- }
223
- }
224
- }
225
- }
226
- export {
227
- ClientRequest
9
+
10
+ //#region src/client-request.ts
11
+ /**
12
+ * ClientRequest — Writable stream representing an outgoing HTTP request.
13
+ *
14
+ * Usage:
15
+ * const req = http.request(options, (res) => { ... });
16
+ * req.write(body);
17
+ * req.end();
18
+ */
19
+ var ClientRequest = class extends OutgoingMessage {
20
+ method;
21
+ path;
22
+ protocol;
23
+ host;
24
+ hostname;
25
+ port;
26
+ aborted = false;
27
+ reusedSocket = false;
28
+ maxHeadersCount = 2e3;
29
+ _chunks = [];
30
+ _session;
31
+ _message;
32
+ _cancellable;
33
+ _timeout = 0;
34
+ _timeoutTimer = null;
35
+ _responseCallback;
36
+ constructor(url, options, callback) {
37
+ super();
38
+ let opts;
39
+ if (typeof url === "string" || url instanceof URL) {
40
+ const parsed = typeof url === "string" ? new URL(url) : url;
41
+ opts = {
42
+ protocol: parsed.protocol,
43
+ hostname: parsed.hostname,
44
+ port: parsed.port ? Number(parsed.port) : undefined,
45
+ path: parsed.pathname + parsed.search,
46
+ ...typeof options === "object" ? options : {}
47
+ };
48
+ if (typeof options === "function") {
49
+ callback = options;
50
+ }
51
+ } else {
52
+ opts = url;
53
+ if (typeof options === "function") {
54
+ callback = options;
55
+ }
56
+ }
57
+ this.method = (opts.method || "GET").toUpperCase();
58
+ this.protocol = opts.protocol || "http:";
59
+ this.hostname = opts.hostname || opts.host?.split(":")[0] || "localhost";
60
+ this.port = Number(opts.port) || (this.protocol === "https:" ? 443 : 80);
61
+ this.path = opts.path || "/";
62
+ this.host = opts.host || `${this.hostname}:${this.port}`;
63
+ this._timeout = opts.timeout || 0;
64
+ if (callback) {
65
+ this._responseCallback = callback;
66
+ this.once("response", callback);
67
+ }
68
+ if (opts.headers) {
69
+ for (const [key, value] of Object.entries(opts.headers)) {
70
+ this.setHeader(key, value);
71
+ }
72
+ }
73
+ if (opts.setHost !== false && !this._headers.has("host")) {
74
+ const defaultPort = this.protocol === "https:" ? 443 : 80;
75
+ const hostHeader = this.port === defaultPort ? this.hostname : `${this.hostname}:${this.port}`;
76
+ this.setHeader("Host", hostHeader);
77
+ }
78
+ if (opts.auth && !this._headers.has("authorization")) {
79
+ this.setHeader("Authorization", "Basic " + Buffer.from(opts.auth).toString("base64"));
80
+ }
81
+ if (opts.signal) {
82
+ if (opts.signal.aborted) {
83
+ this.abort();
84
+ } else {
85
+ opts.signal.addEventListener("abort", () => this.abort(), { once: true });
86
+ }
87
+ }
88
+ const uri = GLib.Uri.parse(this._buildUrl(), GLib.UriFlags.NONE);
89
+ this._session = new Soup.Session();
90
+ this._message = new Soup.Message({
91
+ method: this.method,
92
+ uri
93
+ });
94
+ this._cancellable = new Gio.Cancellable();
95
+ if (this._timeout > 0) {
96
+ this._session.timeout = Math.ceil(this._timeout / 1e3);
97
+ this._timeoutTimer = setTimeout(() => {
98
+ this._timeoutTimer = null;
99
+ this.emit("timeout");
100
+ }, this._timeout);
101
+ }
102
+ }
103
+ _buildUrl() {
104
+ const proto = this.protocol.endsWith(":") ? this.protocol : this.protocol + ":";
105
+ const defaultPort = proto === "https:" ? 443 : 80;
106
+ const portStr = this.port === defaultPort ? "" : `:${this.port}`;
107
+ return `${proto}//${this.hostname}${portStr}${this.path}`;
108
+ }
109
+ /** Get raw header names and values as a flat array. */
110
+ getRawHeaderNames() {
111
+ return Array.from(this._headers.keys());
112
+ }
113
+ /** Flush headers — marks headers as sent. */
114
+ flushHeaders() {
115
+ if (!this.headersSent) {
116
+ this._applyHeaders();
117
+ }
118
+ }
119
+ /** Set timeout for the request. Emits 'timeout' if no response within msecs. */
120
+ setTimeout(msecs, callback) {
121
+ this._timeout = msecs;
122
+ if (this._timeoutTimer) {
123
+ clearTimeout(this._timeoutTimer);
124
+ this._timeoutTimer = null;
125
+ }
126
+ if (callback) this.once("timeout", callback);
127
+ if (msecs > 0) {
128
+ this._session.timeout = Math.ceil(msecs / 1e3);
129
+ this._timeoutTimer = setTimeout(() => {
130
+ this._timeoutTimer = null;
131
+ this.emit("timeout");
132
+ }, msecs);
133
+ }
134
+ return this;
135
+ }
136
+ /** Abort the request. */
137
+ abort() {
138
+ if (this.aborted) return;
139
+ this.aborted = true;
140
+ if (this._timeoutTimer) {
141
+ clearTimeout(this._timeoutTimer);
142
+ this._timeoutTimer = null;
143
+ }
144
+ this._cancellable.cancel();
145
+ this.emit("abort");
146
+ this.destroy();
147
+ }
148
+ /** Writable stream _write implementation — collect body chunks. */
149
+ _write(chunk, encoding, callback) {
150
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
151
+ this._chunks.push(buf);
152
+ callback();
153
+ }
154
+ /** Called when the writable stream ends — send the request. */
155
+ _final(callback) {
156
+ this._sendRequest().then(() => callback()).catch((err) => callback(err));
157
+ }
158
+ _applyHeaders() {
159
+ if (this.headersSent) return;
160
+ this.headersSent = true;
161
+ const requestHeaders = this._message.get_request_headers();
162
+ for (const [key, value] of this._headers) {
163
+ if (Array.isArray(value)) {
164
+ for (const v of value) {
165
+ requestHeaders.append(key, v);
166
+ }
167
+ } else {
168
+ requestHeaders.replace(key, value);
169
+ }
170
+ }
171
+ }
172
+ async _sendRequest() {
173
+ this._applyHeaders();
174
+ const body = Buffer.concat(this._chunks);
175
+ if (body.length > 0) {
176
+ const contentType = this._headers.get("content-type") || "application/octet-stream";
177
+ this._message.set_request_body_from_bytes(contentType, new GLib.Bytes(body));
178
+ }
179
+ try {
180
+ const inputStream = await new Promise((resolve, reject) => {
181
+ this._session.send_async(this._message, GLib.PRIORITY_DEFAULT, this._cancellable, (_self, asyncRes) => {
182
+ try {
183
+ const stream = this._session.send_finish(asyncRes);
184
+ resolve(stream);
185
+ } catch (error) {
186
+ reject(error);
187
+ }
188
+ });
189
+ });
190
+ const bodyChunks = [];
191
+ try {
192
+ let chunk;
193
+ while ((chunk = await readBytesAsync(inputStream, 4096, GLib.PRIORITY_DEFAULT, this._cancellable)) !== null) {
194
+ bodyChunks.push(Buffer.from(chunk));
195
+ }
196
+ } catch (readErr) {}
197
+ const res = new IncomingMessage();
198
+ res.statusCode = this._message.status_code;
199
+ res.statusMessage = this._message.get_reason_phrase();
200
+ res.httpVersion = "1.1";
201
+ const responseHeaders = this._message.get_response_headers();
202
+ responseHeaders.foreach((name, value) => {
203
+ const lower = name.toLowerCase();
204
+ res.rawHeaders.push(name, value);
205
+ if (lower in res.headers) {
206
+ const existing = res.headers[lower];
207
+ if (Array.isArray(existing)) {
208
+ existing.push(value);
209
+ } else {
210
+ res.headers[lower] = [existing, value];
211
+ }
212
+ } else {
213
+ res.headers[lower] = value;
214
+ }
215
+ });
216
+ this.finished = true;
217
+ if (this._timeoutTimer) {
218
+ clearTimeout(this._timeoutTimer);
219
+ this._timeoutTimer = null;
220
+ }
221
+ this.emit("response", res);
222
+ setTimeout(() => {
223
+ for (const buf of bodyChunks) {
224
+ res.push(buf);
225
+ }
226
+ res.push(null);
227
+ res.complete = true;
228
+ }, 0);
229
+ } catch (error) {
230
+ if (this.aborted) {
231
+ this.emit("abort");
232
+ } else {
233
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
234
+ }
235
+ }
236
+ }
228
237
  };
238
+
239
+ //#endregion
240
+ export { ClientRequest };