@gjsify/http 0.0.3 → 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/README.md +30 -1
- package/lib/esm/client-request.js +228 -0
- package/lib/esm/constants.js +105 -0
- package/lib/esm/incoming-message.js +59 -0
- package/lib/esm/index.js +112 -2
- package/lib/esm/server.js +371 -0
- package/lib/types/client-request.d.ts +65 -0
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/incoming-message.d.ts +25 -0
- package/lib/types/index.d.ts +102 -0
- package/lib/types/server.d.ts +102 -0
- package/package.json +24 -18
- package/src/client-request.ts +307 -0
- package/src/client.spec.ts +538 -0
- package/src/constants.ts +33 -0
- package/src/extended.spec.ts +620 -0
- package/src/incoming-message.ts +71 -0
- package/src/index.spec.ts +1359 -67
- package/src/index.ts +164 -20
- package/src/server.ts +489 -0
- package/src/streaming.spec.ts +588 -0
- package/src/test.mts +6 -1
- package/src/timeout.spec.ts +668 -0
- package/src/upgrade.spec.ts +256 -0
- package/tsconfig.json +23 -10
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/index.js +0 -18
- package/test.gjs.js +0 -34832
- package/test.gjs.mjs +0 -34786
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -1278
- package/test.node.mjs +0 -358
- package/tsconfig.types.json +0 -8
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import Soup from "@girs/soup-3.0";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
import { Socket as NetSocket } from "@gjsify/net/socket";
|
|
6
|
+
import { deferEmit, ensureMainLoop } from "@gjsify/utils";
|
|
7
|
+
import { STATUS_CODES } from "./constants.js";
|
|
8
|
+
import { IncomingMessage } from "./incoming-message.js";
|
|
9
|
+
class OutgoingMessage extends Writable {
|
|
10
|
+
headersSent = false;
|
|
11
|
+
sendDate = true;
|
|
12
|
+
finished = false;
|
|
13
|
+
socket = null;
|
|
14
|
+
_headers = /* @__PURE__ */ new Map();
|
|
15
|
+
/** Set a header. */
|
|
16
|
+
setHeader(name, value) {
|
|
17
|
+
this._headers.set(name.toLowerCase(), typeof value === "number" ? String(value) : value);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
/** Get a header. */
|
|
21
|
+
getHeader(name) {
|
|
22
|
+
return this._headers.get(name.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
/** Remove a header. */
|
|
25
|
+
removeHeader(name) {
|
|
26
|
+
this._headers.delete(name.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
/** Check if a header has been set. */
|
|
29
|
+
hasHeader(name) {
|
|
30
|
+
return this._headers.has(name.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
/** Get all header names. */
|
|
33
|
+
getHeaderNames() {
|
|
34
|
+
return Array.from(this._headers.keys());
|
|
35
|
+
}
|
|
36
|
+
/** Get all headers as an object. */
|
|
37
|
+
getHeaders() {
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const [key, value] of this._headers) {
|
|
40
|
+
result[key] = value;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/** Append a header value instead of replacing. */
|
|
45
|
+
appendHeader(name, value) {
|
|
46
|
+
const lower = name.toLowerCase();
|
|
47
|
+
const existing = this._headers.get(lower);
|
|
48
|
+
if (existing === void 0) {
|
|
49
|
+
this._headers.set(lower, value);
|
|
50
|
+
} else if (Array.isArray(existing)) {
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
existing.push(...value);
|
|
53
|
+
} else {
|
|
54
|
+
existing.push(value);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
this._headers.set(lower, [existing, ...value]);
|
|
59
|
+
} else {
|
|
60
|
+
this._headers.set(lower, [existing, value]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
/** Flush headers (no-op in base class). */
|
|
66
|
+
flushHeaders() {
|
|
67
|
+
this.headersSent = true;
|
|
68
|
+
}
|
|
69
|
+
_write(_chunk, _encoding, callback) {
|
|
70
|
+
callback();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class ServerResponse extends OutgoingMessage {
|
|
74
|
+
statusCode = 200;
|
|
75
|
+
statusMessage = "";
|
|
76
|
+
_streaming = false;
|
|
77
|
+
_soupMsg;
|
|
78
|
+
_timeoutTimer = null;
|
|
79
|
+
constructor(soupMsg) {
|
|
80
|
+
super();
|
|
81
|
+
this._soupMsg = soupMsg;
|
|
82
|
+
}
|
|
83
|
+
/** Set a timeout for the response. Emits 'timeout' if response not sent within msecs. */
|
|
84
|
+
setTimeout(msecs, callback) {
|
|
85
|
+
if (this._timeoutTimer) {
|
|
86
|
+
clearTimeout(this._timeoutTimer);
|
|
87
|
+
this._timeoutTimer = null;
|
|
88
|
+
}
|
|
89
|
+
if (callback) this.once("timeout", callback);
|
|
90
|
+
if (msecs > 0) {
|
|
91
|
+
this._timeoutTimer = setTimeout(() => {
|
|
92
|
+
this._timeoutTimer = null;
|
|
93
|
+
this.emit("timeout");
|
|
94
|
+
}, msecs);
|
|
95
|
+
}
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
/** Write the status line and headers. */
|
|
99
|
+
writeHead(statusCode, statusMessage, headers) {
|
|
100
|
+
this.statusCode = statusCode;
|
|
101
|
+
if (typeof statusMessage === "object") {
|
|
102
|
+
headers = statusMessage;
|
|
103
|
+
statusMessage = void 0;
|
|
104
|
+
}
|
|
105
|
+
this.statusMessage = statusMessage || STATUS_CODES[statusCode] || "";
|
|
106
|
+
if (headers) {
|
|
107
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
108
|
+
this.setHeader(key, value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
/** Send a 100 Continue response. */
|
|
114
|
+
writeContinue(callback) {
|
|
115
|
+
if (callback) Promise.resolve().then(callback);
|
|
116
|
+
}
|
|
117
|
+
/** Send a 102 Processing response (WebDAV). */
|
|
118
|
+
writeProcessing(callback) {
|
|
119
|
+
if (callback) Promise.resolve().then(callback);
|
|
120
|
+
}
|
|
121
|
+
/** Flush headers (send them immediately). */
|
|
122
|
+
flushHeaders() {
|
|
123
|
+
if (!this.headersSent) {
|
|
124
|
+
this.headersSent = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Add trailing headers for chunked transfer encoding. */
|
|
128
|
+
addTrailers(headers) {
|
|
129
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
130
|
+
this._headers.set("trailer-" + key.toLowerCase(), value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Send status + headers to the client via Soup and switch to streaming (chunked) mode.
|
|
135
|
+
* Called on the first write() — subsequent writes append chunks and unpause.
|
|
136
|
+
*/
|
|
137
|
+
_startStreaming() {
|
|
138
|
+
if (this._streaming) return;
|
|
139
|
+
this._streaming = true;
|
|
140
|
+
this.headersSent = true;
|
|
141
|
+
if (this._timeoutTimer) {
|
|
142
|
+
clearTimeout(this._timeoutTimer);
|
|
143
|
+
this._timeoutTimer = null;
|
|
144
|
+
}
|
|
145
|
+
this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
|
|
146
|
+
const responseHeaders = this._soupMsg.get_response_headers();
|
|
147
|
+
responseHeaders.set_encoding(Soup.Encoding.CHUNKED);
|
|
148
|
+
if (!this._headers.has("connection")) {
|
|
149
|
+
responseHeaders.replace("Connection", "close");
|
|
150
|
+
}
|
|
151
|
+
for (const [key, value] of this._headers) {
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
for (const v of value) {
|
|
154
|
+
responseHeaders.append(key, v);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
responseHeaders.replace(key, value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Writable stream _write — sends headers on first call, then appends + flushes each chunk. */
|
|
162
|
+
_write(chunk, encoding, callback) {
|
|
163
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
164
|
+
this._startStreaming();
|
|
165
|
+
const responseBody = this._soupMsg.get_response_body();
|
|
166
|
+
responseBody.append(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
167
|
+
this._soupMsg.unpause();
|
|
168
|
+
callback();
|
|
169
|
+
}
|
|
170
|
+
/** Called by Writable.end() — completes the body (streaming) or sends batch response (no-body). */
|
|
171
|
+
_final(callback) {
|
|
172
|
+
if (this._streaming) {
|
|
173
|
+
const responseBody = this._soupMsg.get_response_body();
|
|
174
|
+
responseBody.complete();
|
|
175
|
+
this._soupMsg.unpause();
|
|
176
|
+
} else {
|
|
177
|
+
this._sendBatchResponse();
|
|
178
|
+
}
|
|
179
|
+
this.finished = true;
|
|
180
|
+
callback();
|
|
181
|
+
}
|
|
182
|
+
/** Batch response — sends status + headers + empty/no body in one shot (for responses without write()). */
|
|
183
|
+
_sendBatchResponse() {
|
|
184
|
+
if (this.headersSent) return;
|
|
185
|
+
this.headersSent = true;
|
|
186
|
+
if (this._timeoutTimer) {
|
|
187
|
+
clearTimeout(this._timeoutTimer);
|
|
188
|
+
this._timeoutTimer = null;
|
|
189
|
+
}
|
|
190
|
+
this._soupMsg.set_status(this.statusCode, this.statusMessage || null);
|
|
191
|
+
const responseHeaders = this._soupMsg.get_response_headers();
|
|
192
|
+
if (!this._headers.has("connection")) {
|
|
193
|
+
responseHeaders.replace("Connection", "close");
|
|
194
|
+
}
|
|
195
|
+
for (const [key, value] of this._headers) {
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
for (const v of value) {
|
|
198
|
+
responseHeaders.append(key, v);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
responseHeaders.replace(key, value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const contentType = this._headers.get("content-type") || "text/plain";
|
|
205
|
+
this._soupMsg.set_response(contentType, Soup.MemoryUse.COPY, new Uint8Array(0));
|
|
206
|
+
}
|
|
207
|
+
/** Write status + headers + body in one call (convenience). */
|
|
208
|
+
end(chunk, encoding, callback) {
|
|
209
|
+
if (typeof chunk === "function") {
|
|
210
|
+
callback = chunk;
|
|
211
|
+
chunk = void 0;
|
|
212
|
+
} else if (typeof encoding === "function") {
|
|
213
|
+
callback = encoding;
|
|
214
|
+
encoding = void 0;
|
|
215
|
+
}
|
|
216
|
+
if (chunk != null) {
|
|
217
|
+
this.write(chunk, encoding);
|
|
218
|
+
}
|
|
219
|
+
super.end(callback);
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const _activeServers = /* @__PURE__ */ new Set();
|
|
224
|
+
class Server extends EventEmitter {
|
|
225
|
+
listening = false;
|
|
226
|
+
maxHeadersCount = 2e3;
|
|
227
|
+
timeout = 0;
|
|
228
|
+
keepAliveTimeout = 5e3;
|
|
229
|
+
headersTimeout = 6e4;
|
|
230
|
+
requestTimeout = 3e5;
|
|
231
|
+
_soupServer = null;
|
|
232
|
+
_address = null;
|
|
233
|
+
constructor(optionsOrListener, requestListener) {
|
|
234
|
+
super();
|
|
235
|
+
const listener = typeof optionsOrListener === "function" ? optionsOrListener : requestListener;
|
|
236
|
+
if (listener) {
|
|
237
|
+
this.on("request", listener);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
listen(...args) {
|
|
241
|
+
let port = 0;
|
|
242
|
+
let hostname = "0.0.0.0";
|
|
243
|
+
let callback;
|
|
244
|
+
for (const arg of args) {
|
|
245
|
+
if (typeof arg === "number") port = arg;
|
|
246
|
+
else if (typeof arg === "string") hostname = arg;
|
|
247
|
+
else if (typeof arg === "function") callback = arg;
|
|
248
|
+
}
|
|
249
|
+
if (callback) this.once("listening", callback);
|
|
250
|
+
try {
|
|
251
|
+
this._soupServer = new Soup.Server({});
|
|
252
|
+
this._soupServer.add_handler(null, (server, msg, path) => {
|
|
253
|
+
this._handleRequest(msg, path);
|
|
254
|
+
});
|
|
255
|
+
this._soupServer.listen_local(port, Soup.ServerListenOptions.IPV4_ONLY);
|
|
256
|
+
ensureMainLoop();
|
|
257
|
+
const listeners = this._soupServer.get_listeners();
|
|
258
|
+
let actualPort = port;
|
|
259
|
+
if (listeners && listeners.length > 0) {
|
|
260
|
+
const addr = listeners[0].get_local_address();
|
|
261
|
+
if (addr && typeof addr.get_port === "function") {
|
|
262
|
+
actualPort = addr.get_port();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.listening = true;
|
|
266
|
+
this._address = { port: actualPort, family: "IPv4", address: hostname };
|
|
267
|
+
_activeServers.add(this);
|
|
268
|
+
deferEmit(this, "listening");
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
271
|
+
if (this.listenerCount("error") === 0) {
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
deferEmit(this, "error", error);
|
|
275
|
+
}
|
|
276
|
+
return this;
|
|
277
|
+
}
|
|
278
|
+
_handleRequest(soupMsg, path) {
|
|
279
|
+
const req = new IncomingMessage();
|
|
280
|
+
const res = new ServerResponse(soupMsg);
|
|
281
|
+
req.method = soupMsg.get_method();
|
|
282
|
+
req.url = soupMsg.get_uri().get_path();
|
|
283
|
+
const query = soupMsg.get_uri().get_query();
|
|
284
|
+
if (query) req.url += "?" + query;
|
|
285
|
+
req.httpVersion = "1.1";
|
|
286
|
+
const requestHeaders = soupMsg.get_request_headers();
|
|
287
|
+
requestHeaders.foreach((name, value) => {
|
|
288
|
+
const lower = name.toLowerCase();
|
|
289
|
+
req.rawHeaders.push(name, value);
|
|
290
|
+
if (lower in req.headers) {
|
|
291
|
+
const existing = req.headers[lower];
|
|
292
|
+
if (Array.isArray(existing)) {
|
|
293
|
+
existing.push(value);
|
|
294
|
+
} else {
|
|
295
|
+
req.headers[lower] = [existing, value];
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
req.headers[lower] = value;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const connectionHeader = (req.headers["connection"] || "").toLowerCase();
|
|
302
|
+
const upgradeHeader = (req.headers["upgrade"] || "").toLowerCase();
|
|
303
|
+
if (connectionHeader.includes("upgrade") && upgradeHeader && this.listenerCount("upgrade") > 0) {
|
|
304
|
+
let ioStream = null;
|
|
305
|
+
try {
|
|
306
|
+
ioStream = soupMsg.steal_connection();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
}
|
|
309
|
+
if (ioStream) {
|
|
310
|
+
const socket = new NetSocket();
|
|
311
|
+
socket._setupFromIOStream(ioStream);
|
|
312
|
+
this.emit("upgrade", req, socket, Buffer.alloc(0));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const body = soupMsg.get_request_body();
|
|
317
|
+
if (body && body.data && body.data.length > 0) {
|
|
318
|
+
req._pushBody(body.data);
|
|
319
|
+
} else {
|
|
320
|
+
req._pushBody(null);
|
|
321
|
+
}
|
|
322
|
+
soupMsg.pause();
|
|
323
|
+
res.on("finish", () => {
|
|
324
|
+
soupMsg.unpause();
|
|
325
|
+
});
|
|
326
|
+
this.emit("request", req, res);
|
|
327
|
+
}
|
|
328
|
+
address() {
|
|
329
|
+
return this._address;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Register a WebSocket handler on this server (GJS only).
|
|
333
|
+
* Delegates to Soup.Server.add_websocket_handler().
|
|
334
|
+
* @param path URL path to handle WebSocket upgrades (e.g., '/ws')
|
|
335
|
+
* @param callback Called for each new WebSocket connection with the Soup.WebsocketConnection
|
|
336
|
+
*/
|
|
337
|
+
addWebSocketHandler(path, callback) {
|
|
338
|
+
if (!this._soupServer) {
|
|
339
|
+
throw new Error("Server must be listening before adding WebSocket handlers. Call listen() first.");
|
|
340
|
+
}
|
|
341
|
+
this._soupServer.add_websocket_handler(
|
|
342
|
+
path,
|
|
343
|
+
null,
|
|
344
|
+
null,
|
|
345
|
+
(_srv, _msg, _path, connection) => {
|
|
346
|
+
callback(connection);
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
close(callback) {
|
|
351
|
+
if (callback) this.once("close", callback);
|
|
352
|
+
if (this._soupServer) {
|
|
353
|
+
this._soupServer.disconnect();
|
|
354
|
+
this._soupServer = null;
|
|
355
|
+
}
|
|
356
|
+
this.listening = false;
|
|
357
|
+
_activeServers.delete(this);
|
|
358
|
+
deferEmit(this, "close");
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
setTimeout(msecs, callback) {
|
|
362
|
+
this.timeout = msecs;
|
|
363
|
+
if (callback) this.on("timeout", callback);
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
export {
|
|
368
|
+
OutgoingMessage,
|
|
369
|
+
Server,
|
|
370
|
+
ServerResponse
|
|
371
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { URL } from 'node:url';
|
|
2
|
+
import { OutgoingMessage } from './server.js';
|
|
3
|
+
import { IncomingMessage } from './incoming-message.js';
|
|
4
|
+
export interface ClientRequestOptions {
|
|
5
|
+
protocol?: string;
|
|
6
|
+
hostname?: string;
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number | string;
|
|
9
|
+
path?: string;
|
|
10
|
+
method?: string;
|
|
11
|
+
headers?: Record<string, string | number | string[]>;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
agent?: any;
|
|
14
|
+
setHost?: boolean;
|
|
15
|
+
/** Basic authentication string in the format 'user:password'. */
|
|
16
|
+
auth?: string;
|
|
17
|
+
/** Local address to bind the request from. */
|
|
18
|
+
localAddress?: string;
|
|
19
|
+
/** IP address family (4 or 6). */
|
|
20
|
+
family?: 4 | 6 | 0;
|
|
21
|
+
/** Signal to abort the request. */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* ClientRequest — Writable stream representing an outgoing HTTP request.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* const req = http.request(options, (res) => { ... });
|
|
29
|
+
* req.write(body);
|
|
30
|
+
* req.end();
|
|
31
|
+
*/
|
|
32
|
+
export declare class ClientRequest extends OutgoingMessage {
|
|
33
|
+
method: string;
|
|
34
|
+
path: string;
|
|
35
|
+
protocol: string;
|
|
36
|
+
host: string;
|
|
37
|
+
hostname: string;
|
|
38
|
+
port: number;
|
|
39
|
+
aborted: boolean;
|
|
40
|
+
reusedSocket: boolean;
|
|
41
|
+
maxHeadersCount: number;
|
|
42
|
+
private _chunks;
|
|
43
|
+
private _session;
|
|
44
|
+
private _message;
|
|
45
|
+
private _cancellable;
|
|
46
|
+
private _timeout;
|
|
47
|
+
private _timeoutTimer;
|
|
48
|
+
private _responseCallback?;
|
|
49
|
+
constructor(url: string | URL | ClientRequestOptions, options?: ClientRequestOptions | ((res: IncomingMessage) => void), callback?: (res: IncomingMessage) => void);
|
|
50
|
+
private _buildUrl;
|
|
51
|
+
/** Get raw header names and values as a flat array. */
|
|
52
|
+
getRawHeaderNames(): string[];
|
|
53
|
+
/** Flush headers — marks headers as sent. */
|
|
54
|
+
flushHeaders(): void;
|
|
55
|
+
/** Set timeout for the request. Emits 'timeout' if no response within msecs. */
|
|
56
|
+
setTimeout(msecs: number, callback?: () => void): this;
|
|
57
|
+
/** Abort the request. */
|
|
58
|
+
abort(): void;
|
|
59
|
+
/** Writable stream _write implementation — collect body chunks. */
|
|
60
|
+
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
|
61
|
+
/** Called when the writable stream ends — send the request. */
|
|
62
|
+
_final(callback: (error?: Error | null) => void): void;
|
|
63
|
+
private _applyHeaders;
|
|
64
|
+
private _sendRequest;
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* IncomingMessage — Readable stream for HTTP request (server-side) or response (client-side).
|
|
4
|
+
*/
|
|
5
|
+
export declare class IncomingMessage extends Readable {
|
|
6
|
+
httpVersion: string;
|
|
7
|
+
httpVersionMajor: number;
|
|
8
|
+
httpVersionMinor: number;
|
|
9
|
+
headers: Record<string, string | string[]>;
|
|
10
|
+
rawHeaders: string[];
|
|
11
|
+
method?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
statusMessage?: string;
|
|
15
|
+
complete: boolean;
|
|
16
|
+
socket: any;
|
|
17
|
+
aborted: boolean;
|
|
18
|
+
private _timeoutTimer;
|
|
19
|
+
constructor();
|
|
20
|
+
_read(_size: number): void;
|
|
21
|
+
/** Finish the readable stream with the body data (used by server-side handler). */
|
|
22
|
+
_pushBody(body: Uint8Array | null): void;
|
|
23
|
+
setTimeout(msecs: number, callback?: () => void): this;
|
|
24
|
+
destroy(error?: Error): this;
|
|
25
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export { STATUS_CODES, METHODS } from './constants.js';
|
|
2
|
+
export { IncomingMessage } from './incoming-message.js';
|
|
3
|
+
export { OutgoingMessage, Server, ServerResponse } from './server.js';
|
|
4
|
+
export { ClientRequest } from './client-request.js';
|
|
5
|
+
import { IncomingMessage } from './incoming-message.js';
|
|
6
|
+
import { OutgoingMessage, Server, ServerResponse } from './server.js';
|
|
7
|
+
import { ClientRequest } from './client-request.js';
|
|
8
|
+
import type { ClientRequestOptions } from './client-request.js';
|
|
9
|
+
import { URL } from 'node:url';
|
|
10
|
+
/**
|
|
11
|
+
* Performs the low-level validations on the provided `name` that are done when `res.setHeader(name, value)` is called.
|
|
12
|
+
* @since v14.3.0
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateHeaderName(name: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Performs the low-level validations on the provided `value` that are done when `res.setHeader(name, value)` is called.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateHeaderValue(name: string, value: any): void;
|
|
19
|
+
export interface AgentOptions {
|
|
20
|
+
keepAlive?: boolean;
|
|
21
|
+
keepAliveMsecs?: number;
|
|
22
|
+
maxSockets?: number;
|
|
23
|
+
maxTotalSockets?: number;
|
|
24
|
+
maxFreeSockets?: number;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
scheduling?: 'fifo' | 'lifo';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Agent class for connection pooling.
|
|
30
|
+
* Soup.Session handles actual TCP connection pooling internally.
|
|
31
|
+
* This class provides the Node.js-compatible API surface for frameworks.
|
|
32
|
+
*/
|
|
33
|
+
export declare class Agent {
|
|
34
|
+
defaultPort: number;
|
|
35
|
+
protocol: string;
|
|
36
|
+
maxSockets: number;
|
|
37
|
+
maxTotalSockets: number;
|
|
38
|
+
maxFreeSockets: number;
|
|
39
|
+
keepAliveMsecs: number;
|
|
40
|
+
keepAlive: boolean;
|
|
41
|
+
scheduling: 'fifo' | 'lifo';
|
|
42
|
+
/** Pending requests per host (compatibility — Soup manages internally). */
|
|
43
|
+
readonly requests: Record<string, unknown[]>;
|
|
44
|
+
/** Active sockets per host (compatibility — Soup manages internally). */
|
|
45
|
+
readonly sockets: Record<string, unknown[]>;
|
|
46
|
+
/** Idle sockets per host (compatibility — Soup manages internally). */
|
|
47
|
+
readonly freeSockets: Record<string, unknown[]>;
|
|
48
|
+
constructor(options?: AgentOptions);
|
|
49
|
+
/** Destroy the agent and close idle connections. */
|
|
50
|
+
destroy(): void;
|
|
51
|
+
/** Return a connection pool key for the given options. */
|
|
52
|
+
getName(options: {
|
|
53
|
+
host?: string;
|
|
54
|
+
port?: number;
|
|
55
|
+
localAddress?: string;
|
|
56
|
+
family?: number;
|
|
57
|
+
}): string;
|
|
58
|
+
}
|
|
59
|
+
export declare const globalAgent: Agent;
|
|
60
|
+
/**
|
|
61
|
+
* Create an HTTP server.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createServer(options?: Record<string, unknown> | ((req: IncomingMessage, res: ServerResponse) => void), requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server;
|
|
64
|
+
/**
|
|
65
|
+
* Make an HTTP request.
|
|
66
|
+
*
|
|
67
|
+
* @param url URL string, URL object, or request options
|
|
68
|
+
* @param options Request options (if url is string/URL)
|
|
69
|
+
* @param callback Response callback
|
|
70
|
+
*/
|
|
71
|
+
export declare function request(url: string | URL | ClientRequestOptions, options?: ClientRequestOptions | ((res: IncomingMessage) => void), callback?: (res: IncomingMessage) => void): ClientRequest;
|
|
72
|
+
/**
|
|
73
|
+
* Make an HTTP GET request (convenience wrapper that calls req.end() automatically).
|
|
74
|
+
*/
|
|
75
|
+
export declare function get(url: string | URL | ClientRequestOptions, options?: ClientRequestOptions | ((res: IncomingMessage) => void), callback?: (res: IncomingMessage) => void): ClientRequest;
|
|
76
|
+
/** Max header size in bytes. */
|
|
77
|
+
export declare const maxHeaderSize = 16384;
|
|
78
|
+
/**
|
|
79
|
+
* Set the maximum number of idle HTTP parsers. Soup.Session handles
|
|
80
|
+
* connection pooling internally, so this is a no-op for compatibility.
|
|
81
|
+
* @since v18.8.0
|
|
82
|
+
*/
|
|
83
|
+
export declare function setMaxIdleHTTPParsers(_max: number): void;
|
|
84
|
+
declare const _default: {
|
|
85
|
+
STATUS_CODES: Record<number, string>;
|
|
86
|
+
METHODS: string[];
|
|
87
|
+
Server: typeof Server;
|
|
88
|
+
IncomingMessage: typeof IncomingMessage;
|
|
89
|
+
OutgoingMessage: typeof OutgoingMessage;
|
|
90
|
+
ServerResponse: typeof ServerResponse;
|
|
91
|
+
ClientRequest: typeof ClientRequest;
|
|
92
|
+
Agent: typeof Agent;
|
|
93
|
+
globalAgent: Agent;
|
|
94
|
+
createServer: typeof createServer;
|
|
95
|
+
request: typeof request;
|
|
96
|
+
get: typeof get;
|
|
97
|
+
validateHeaderName: typeof validateHeaderName;
|
|
98
|
+
validateHeaderValue: typeof validateHeaderValue;
|
|
99
|
+
maxHeaderSize: number;
|
|
100
|
+
setMaxIdleHTTPParsers: typeof setMaxIdleHTTPParsers;
|
|
101
|
+
};
|
|
102
|
+
export default _default;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import Soup from '@girs/soup-3.0';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { Writable } from 'node:stream';
|
|
4
|
+
import { IncomingMessage } from './incoming-message.js';
|
|
5
|
+
/**
|
|
6
|
+
* OutgoingMessage — Base class for ServerResponse and ClientRequest.
|
|
7
|
+
* Reference: Node.js lib/_http_outgoing.js
|
|
8
|
+
*/
|
|
9
|
+
export declare class OutgoingMessage extends Writable {
|
|
10
|
+
headersSent: boolean;
|
|
11
|
+
sendDate: boolean;
|
|
12
|
+
finished: boolean;
|
|
13
|
+
socket: import('net').Socket | null;
|
|
14
|
+
protected _headers: Map<string, string | string[]>;
|
|
15
|
+
/** Set a header. */
|
|
16
|
+
setHeader(name: string, value: string | number | string[]): this;
|
|
17
|
+
/** Get a header. */
|
|
18
|
+
getHeader(name: string): string | string[] | undefined;
|
|
19
|
+
/** Remove a header. */
|
|
20
|
+
removeHeader(name: string): void;
|
|
21
|
+
/** Check if a header has been set. */
|
|
22
|
+
hasHeader(name: string): boolean;
|
|
23
|
+
/** Get all header names. */
|
|
24
|
+
getHeaderNames(): string[];
|
|
25
|
+
/** Get all headers as an object. */
|
|
26
|
+
getHeaders(): Record<string, string | string[]>;
|
|
27
|
+
/** Append a header value instead of replacing. */
|
|
28
|
+
appendHeader(name: string, value: string | string[]): this;
|
|
29
|
+
/** Flush headers (no-op in base class). */
|
|
30
|
+
flushHeaders(): void;
|
|
31
|
+
_write(_chunk: any, _encoding: string, callback: (error?: Error | null) => void): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* ServerResponse — Writable stream representing an HTTP response.
|
|
35
|
+
* Extends OutgoingMessage for shared header management.
|
|
36
|
+
*/
|
|
37
|
+
export declare class ServerResponse extends OutgoingMessage {
|
|
38
|
+
statusCode: number;
|
|
39
|
+
statusMessage: string;
|
|
40
|
+
private _streaming;
|
|
41
|
+
private _soupMsg;
|
|
42
|
+
private _timeoutTimer;
|
|
43
|
+
constructor(soupMsg: Soup.ServerMessage);
|
|
44
|
+
/** Set a timeout for the response. Emits 'timeout' if response not sent within msecs. */
|
|
45
|
+
setTimeout(msecs: number, callback?: () => void): this;
|
|
46
|
+
/** Write the status line and headers. */
|
|
47
|
+
writeHead(statusCode: number, statusMessage?: string | Record<string, string | string[]>, headers?: Record<string, string | string[]>): this;
|
|
48
|
+
/** Send a 100 Continue response. */
|
|
49
|
+
writeContinue(callback?: () => void): void;
|
|
50
|
+
/** Send a 102 Processing response (WebDAV). */
|
|
51
|
+
writeProcessing(callback?: () => void): void;
|
|
52
|
+
/** Flush headers (send them immediately). */
|
|
53
|
+
flushHeaders(): void;
|
|
54
|
+
/** Add trailing headers for chunked transfer encoding. */
|
|
55
|
+
addTrailers(headers: Record<string, string>): void;
|
|
56
|
+
/**
|
|
57
|
+
* Send status + headers to the client via Soup and switch to streaming (chunked) mode.
|
|
58
|
+
* Called on the first write() — subsequent writes append chunks and unpause.
|
|
59
|
+
*/
|
|
60
|
+
private _startStreaming;
|
|
61
|
+
/** Writable stream _write — sends headers on first call, then appends + flushes each chunk. */
|
|
62
|
+
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
|
63
|
+
/** Called by Writable.end() — completes the body (streaming) or sends batch response (no-body). */
|
|
64
|
+
_final(callback: (error?: Error | null) => void): void;
|
|
65
|
+
/** Batch response — sends status + headers + empty/no body in one shot (for responses without write()). */
|
|
66
|
+
private _sendBatchResponse;
|
|
67
|
+
/** Write status + headers + body in one call (convenience). */
|
|
68
|
+
end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void): this;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* HTTP Server wrapping Soup.Server.
|
|
72
|
+
*/
|
|
73
|
+
export declare class Server extends EventEmitter {
|
|
74
|
+
listening: boolean;
|
|
75
|
+
maxHeadersCount: number;
|
|
76
|
+
timeout: number;
|
|
77
|
+
keepAliveTimeout: number;
|
|
78
|
+
headersTimeout: number;
|
|
79
|
+
requestTimeout: number;
|
|
80
|
+
private _soupServer;
|
|
81
|
+
private _address;
|
|
82
|
+
constructor(requestListener?: ((req: IncomingMessage, res: ServerResponse) => void) | Record<string, unknown>);
|
|
83
|
+
constructor(options: Record<string, unknown>, requestListener?: (req: IncomingMessage, res: ServerResponse) => void);
|
|
84
|
+
listen(port?: number, hostname?: string, backlog?: number, callback?: () => void): this;
|
|
85
|
+
listen(port?: number, hostname?: string, callback?: () => void): this;
|
|
86
|
+
listen(port?: number, callback?: () => void): this;
|
|
87
|
+
private _handleRequest;
|
|
88
|
+
address(): {
|
|
89
|
+
port: number;
|
|
90
|
+
family: string;
|
|
91
|
+
address: string;
|
|
92
|
+
} | null;
|
|
93
|
+
/**
|
|
94
|
+
* Register a WebSocket handler on this server (GJS only).
|
|
95
|
+
* Delegates to Soup.Server.add_websocket_handler().
|
|
96
|
+
* @param path URL path to handle WebSocket upgrades (e.g., '/ws')
|
|
97
|
+
* @param callback Called for each new WebSocket connection with the Soup.WebsocketConnection
|
|
98
|
+
*/
|
|
99
|
+
addWebSocketHandler(path: string, callback: (connection: unknown) => void): void;
|
|
100
|
+
close(callback?: (err?: Error) => void): this;
|
|
101
|
+
setTimeout(msecs: number, callback?: () => void): this;
|
|
102
|
+
}
|