@gjsify/http 0.1.13 → 0.2.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/lib/esm/incoming-message.js +7 -0
- package/lib/esm/server-request-socket.js +71 -0
- package/lib/esm/server.js +126 -117
- package/lib/types/incoming-message.d.ts +3 -0
- package/lib/types/listen-error.spec.d.ts +2 -0
- package/lib/types/server-request-socket.d.ts +32 -0
- package/lib/types/server.d.ts +24 -13
- package/package.json +15 -14
- package/src/incoming-message.ts +6 -0
- package/src/listen-error.spec.ts +73 -0
- package/src/server-request-socket.ts +88 -0
- package/src/server.ts +176 -162
- package/src/test.mts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -13,12 +13,19 @@ class IncomingMessage extends Readable {
|
|
|
13
13
|
complete = false;
|
|
14
14
|
socket = null;
|
|
15
15
|
aborted = false;
|
|
16
|
+
/** Node.js legacy alias for socket — needed by engine.io and other HTTP consumers. */
|
|
17
|
+
get connection() {
|
|
18
|
+
return this.socket;
|
|
19
|
+
}
|
|
16
20
|
_timeoutTimer = null;
|
|
17
21
|
constructor() {
|
|
18
22
|
super();
|
|
19
23
|
}
|
|
20
24
|
_read(_size) {
|
|
21
25
|
}
|
|
26
|
+
// 'close' means connection lost, not body-stream end — don't auto-emit after 'end'.
|
|
27
|
+
_autoClose() {
|
|
28
|
+
}
|
|
22
29
|
/** Finish the readable stream with the body data (used by server-side handler). */
|
|
23
30
|
_pushBody(body) {
|
|
24
31
|
if (body && body.length > 0) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Duplex } from "node:stream";
|
|
2
|
+
class ServerRequestSocket extends Duplex {
|
|
3
|
+
remoteAddress;
|
|
4
|
+
remotePort;
|
|
5
|
+
localAddress;
|
|
6
|
+
localPort;
|
|
7
|
+
remoteFamily = "IPv4";
|
|
8
|
+
encrypted;
|
|
9
|
+
connecting = false;
|
|
10
|
+
pending = false;
|
|
11
|
+
bytesRead = 0;
|
|
12
|
+
bytesWritten = 0;
|
|
13
|
+
// Bridge response we forward pause/resume to (via super.pause/resume
|
|
14
|
+
// for now; the bridge will grow explicit pause/unpause hooks later).
|
|
15
|
+
_bridgeRes;
|
|
16
|
+
_bridgePaused = false;
|
|
17
|
+
constructor(remoteAddress, remotePort, localAddress, localPort, bridgeRes, encrypted = false) {
|
|
18
|
+
super({ allowHalfOpen: true });
|
|
19
|
+
this.remoteAddress = remoteAddress;
|
|
20
|
+
this.remotePort = remotePort;
|
|
21
|
+
this.localAddress = localAddress;
|
|
22
|
+
this.localPort = localPort;
|
|
23
|
+
this.encrypted = encrypted;
|
|
24
|
+
this._bridgeRes = bridgeRes;
|
|
25
|
+
}
|
|
26
|
+
pause() {
|
|
27
|
+
if (this._bridgePaused) return this;
|
|
28
|
+
this._bridgePaused = true;
|
|
29
|
+
return super.pause();
|
|
30
|
+
}
|
|
31
|
+
resume() {
|
|
32
|
+
if (!this._bridgePaused) return super.resume();
|
|
33
|
+
this._bridgePaused = false;
|
|
34
|
+
return super.resume();
|
|
35
|
+
}
|
|
36
|
+
// The bridge owns the TCP connection — no data flows through this Duplex.
|
|
37
|
+
_read(_size) {
|
|
38
|
+
}
|
|
39
|
+
_write(_chunk, _encoding, cb) {
|
|
40
|
+
cb();
|
|
41
|
+
}
|
|
42
|
+
destroySoon() {
|
|
43
|
+
if (!this.writableEnded) this.end();
|
|
44
|
+
if (this.writableFinished)
|
|
45
|
+
this.destroy();
|
|
46
|
+
else
|
|
47
|
+
this.once("finish", () => this.destroy());
|
|
48
|
+
}
|
|
49
|
+
setTimeout(_timeout, cb) {
|
|
50
|
+
if (cb) this.once("timeout", cb);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
setNoDelay(_noDelay) {
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
setKeepAlive(_enable, _delay) {
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
ref() {
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
unref() {
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
address() {
|
|
66
|
+
return { address: this.localAddress, family: "IPv4", port: this.localPort };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
ServerRequestSocket
|
|
71
|
+
};
|
package/lib/esm/server.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import Soup from "@girs/soup-3.0";
|
|
2
1
|
import { EventEmitter } from "node:events";
|
|
3
2
|
import { Writable } from "node:stream";
|
|
4
3
|
import { Buffer } from "node:buffer";
|
|
5
4
|
import { Socket as NetSocket } from "@gjsify/net/socket";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Server as BridgeServer
|
|
7
|
+
} from "@gjsify/http-soup-bridge";
|
|
8
|
+
import { ServerRequestSocket } from "./server-request-socket.js";
|
|
9
|
+
import { createNodeError, deferEmit, ensureMainLoop } from "@gjsify/utils";
|
|
7
10
|
import { STATUS_CODES } from "./constants.js";
|
|
8
11
|
import { IncomingMessage } from "./incoming-message.js";
|
|
9
12
|
class OutgoingMessage extends Writable {
|
|
@@ -74,11 +77,14 @@ class ServerResponse extends OutgoingMessage {
|
|
|
74
77
|
statusCode = 200;
|
|
75
78
|
statusMessage = "";
|
|
76
79
|
_streaming = false;
|
|
77
|
-
|
|
80
|
+
_bridge;
|
|
78
81
|
_timeoutTimer = null;
|
|
79
|
-
constructor(
|
|
82
|
+
constructor(bridge) {
|
|
80
83
|
super();
|
|
81
|
-
this.
|
|
84
|
+
this._bridge = bridge;
|
|
85
|
+
bridge.connect("close", () => {
|
|
86
|
+
this.emit("close");
|
|
87
|
+
});
|
|
82
88
|
}
|
|
83
89
|
/** Set a timeout for the response. Emits 'timeout' if response not sent within msecs. */
|
|
84
90
|
setTimeout(msecs, callback) {
|
|
@@ -131,8 +137,8 @@ class ServerResponse extends OutgoingMessage {
|
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
140
|
+
* Push our header map to the bridge and call write_head() on first
|
|
141
|
+
* write. Idempotent.
|
|
136
142
|
*/
|
|
137
143
|
_startStreaming() {
|
|
138
144
|
if (this._streaming) return;
|
|
@@ -142,67 +148,37 @@ class ServerResponse extends OutgoingMessage {
|
|
|
142
148
|
clearTimeout(this._timeoutTimer);
|
|
143
149
|
this._timeoutTimer = null;
|
|
144
150
|
}
|
|
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
151
|
for (const [key, value] of this._headers) {
|
|
152
152
|
if (Array.isArray(value)) {
|
|
153
|
-
for (const v of value)
|
|
154
|
-
responseHeaders.append(key, v);
|
|
155
|
-
}
|
|
153
|
+
for (const v of value) this._bridge.append_header(key, v);
|
|
156
154
|
} else {
|
|
157
|
-
|
|
155
|
+
this._bridge.set_header(key, value);
|
|
158
156
|
}
|
|
159
157
|
}
|
|
158
|
+
this._bridge.write_head(this.statusCode, this.statusMessage || null);
|
|
160
159
|
}
|
|
161
|
-
/** Writable stream _write — sends headers on first call, then appends
|
|
160
|
+
/** Writable stream _write — sends headers on first call, then appends each chunk. */
|
|
162
161
|
_write(chunk, encoding, callback) {
|
|
163
162
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
164
163
|
this._startStreaming();
|
|
165
|
-
|
|
166
|
-
responseBody.append(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
167
|
-
this._soupMsg.unpause();
|
|
164
|
+
this._bridge.write_chunk(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
168
165
|
callback();
|
|
169
166
|
}
|
|
170
|
-
/** Called by Writable.end() —
|
|
167
|
+
/** Called by Writable.end() — finishes the bridge response. */
|
|
171
168
|
_final(callback) {
|
|
172
|
-
if (this._streaming) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
169
|
+
if (!this._streaming) {
|
|
170
|
+
for (const [key, value] of this._headers) {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
for (const v of value) this._bridge.append_header(key, v);
|
|
173
|
+
} else {
|
|
174
|
+
this._bridge.set_header(key, value);
|
|
199
175
|
}
|
|
200
|
-
} else {
|
|
201
|
-
responseHeaders.replace(key, value);
|
|
202
176
|
}
|
|
177
|
+
this._bridge.write_head(this.statusCode, this.statusMessage || null);
|
|
203
178
|
}
|
|
204
|
-
|
|
205
|
-
this.
|
|
179
|
+
this._bridge.end();
|
|
180
|
+
this.finished = true;
|
|
181
|
+
callback();
|
|
206
182
|
}
|
|
207
183
|
/** Write status + headers + body in one call (convenience). */
|
|
208
184
|
end(chunk, encoding, callback) {
|
|
@@ -228,14 +204,18 @@ class Server extends EventEmitter {
|
|
|
228
204
|
keepAliveTimeout = 5e3;
|
|
229
205
|
headersTimeout = 6e4;
|
|
230
206
|
requestTimeout = 3e5;
|
|
231
|
-
|
|
207
|
+
_bridge = null;
|
|
232
208
|
_address = null;
|
|
209
|
+
/** Exposes the underlying Soup.Server so consumers (e.g. WebSocketServer
|
|
210
|
+
* in `@gjsify/ws`) can register additional handlers (websocket, path-
|
|
211
|
+
* specific) on the same server instance without port-sharing conflicts. */
|
|
212
|
+
get soupServer() {
|
|
213
|
+
return this._bridge?.soup_server ?? null;
|
|
214
|
+
}
|
|
233
215
|
constructor(optionsOrListener, requestListener) {
|
|
234
216
|
super();
|
|
235
217
|
const listener = typeof optionsOrListener === "function" ? optionsOrListener : requestListener;
|
|
236
|
-
if (listener)
|
|
237
|
-
this.on("request", listener);
|
|
238
|
-
}
|
|
218
|
+
if (listener) this.on("request", listener);
|
|
239
219
|
}
|
|
240
220
|
listen(...args) {
|
|
241
221
|
let port = 0;
|
|
@@ -248,110 +228,139 @@ class Server extends EventEmitter {
|
|
|
248
228
|
}
|
|
249
229
|
if (callback) this.once("listening", callback);
|
|
250
230
|
try {
|
|
251
|
-
this.
|
|
252
|
-
this.
|
|
253
|
-
this._handleRequest(
|
|
231
|
+
this._bridge = new BridgeServer();
|
|
232
|
+
this._bridge.connect("request-received", (_self, req, res) => {
|
|
233
|
+
this._handleRequest(req, res);
|
|
254
234
|
});
|
|
255
|
-
this.
|
|
235
|
+
this._bridge.connect("upgrade", (_self, req, iostream, _head) => {
|
|
236
|
+
this._handleUpgrade(req, iostream);
|
|
237
|
+
});
|
|
238
|
+
this._bridge.connect("error-occurred", (_self, msg) => {
|
|
239
|
+
this.emit("error", new Error(msg));
|
|
240
|
+
});
|
|
241
|
+
this._bridge.listen(port, hostname);
|
|
256
242
|
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
243
|
this.listening = true;
|
|
266
|
-
this._address = { port:
|
|
244
|
+
this._address = { port: this._bridge.port, family: "IPv4", address: this._bridge.address || hostname };
|
|
267
245
|
_activeServers.add(this);
|
|
268
246
|
deferEmit(this, "listening");
|
|
269
247
|
} catch (err) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
deferEmit(this, "error", error);
|
|
248
|
+
const nodeErr = createNodeError(err, "listen", { address: hostname, port });
|
|
249
|
+
deferEmit(this, "error", nodeErr);
|
|
275
250
|
}
|
|
276
251
|
return this;
|
|
277
252
|
}
|
|
278
|
-
_handleRequest(
|
|
253
|
+
_handleRequest(bridgeReq, bridgeRes) {
|
|
279
254
|
const req = new IncomingMessage();
|
|
280
|
-
const res = new ServerResponse(
|
|
281
|
-
req.method =
|
|
282
|
-
req.url =
|
|
283
|
-
const query = soupMsg.get_uri().get_query();
|
|
284
|
-
if (query) req.url += "?" + query;
|
|
255
|
+
const res = new ServerResponse(bridgeRes);
|
|
256
|
+
req.method = bridgeReq.method;
|
|
257
|
+
req.url = bridgeReq.url;
|
|
285
258
|
req.httpVersion = "1.1";
|
|
286
|
-
const
|
|
287
|
-
|
|
259
|
+
const pairs = bridgeReq.header_pairs ?? [];
|
|
260
|
+
for (let i = 0; i + 1 < pairs.length; i += 2) {
|
|
261
|
+
const name = pairs[i];
|
|
262
|
+
const value = pairs[i + 1];
|
|
288
263
|
const lower = name.toLowerCase();
|
|
289
264
|
req.rawHeaders.push(name, value);
|
|
290
265
|
if (lower in req.headers) {
|
|
291
266
|
const existing = req.headers[lower];
|
|
292
|
-
if (Array.isArray(existing))
|
|
293
|
-
|
|
294
|
-
} else {
|
|
295
|
-
req.headers[lower] = [existing, value];
|
|
296
|
-
}
|
|
267
|
+
if (Array.isArray(existing)) existing.push(value);
|
|
268
|
+
else req.headers[lower] = [existing, value];
|
|
297
269
|
} else {
|
|
298
270
|
req.headers[lower] = value;
|
|
299
271
|
}
|
|
272
|
+
}
|
|
273
|
+
req.socket = new ServerRequestSocket(
|
|
274
|
+
bridgeReq.remote_address ?? "127.0.0.1",
|
|
275
|
+
bridgeReq.remote_port ?? 0,
|
|
276
|
+
this._address?.address ?? "127.0.0.1",
|
|
277
|
+
this._address?.port ?? 0,
|
|
278
|
+
bridgeRes
|
|
279
|
+
);
|
|
280
|
+
const body = bridgeReq.get_body();
|
|
281
|
+
if (body.length > 0) req._pushBody(body);
|
|
282
|
+
else req._pushBody(null);
|
|
283
|
+
bridgeReq.connect("aborted_signal", () => {
|
|
284
|
+
if (!req.aborted) {
|
|
285
|
+
req.aborted = true;
|
|
286
|
+
req.emit("aborted");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
bridgeReq.connect("close", () => {
|
|
290
|
+
req.emit("close");
|
|
300
291
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
292
|
+
try {
|
|
293
|
+
const result = this.emit("request", req, res);
|
|
294
|
+
if (result instanceof Promise || result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
295
|
+
result.catch((err) => {
|
|
296
|
+
console.error("[HTTP] Unhandled error in async request handler:", err);
|
|
297
|
+
if (!res.headersSent) {
|
|
298
|
+
try {
|
|
299
|
+
res.writeHead(500);
|
|
300
|
+
res.end("Internal Server Error");
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
308
305
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error("[HTTP] Unhandled error in request handler:", err);
|
|
308
|
+
if (!res.headersSent) {
|
|
309
|
+
try {
|
|
310
|
+
res.writeHead(500);
|
|
311
|
+
res.end("Internal Server Error");
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
}
|
|
317
|
+
_handleUpgrade(bridgeReq, iostream) {
|
|
318
|
+
const req = new IncomingMessage();
|
|
319
|
+
req.method = bridgeReq.method;
|
|
320
|
+
req.url = bridgeReq.url;
|
|
321
|
+
req.httpVersion = "1.1";
|
|
322
|
+
const pairs = bridgeReq.header_pairs ?? [];
|
|
323
|
+
for (let i = 0; i + 1 < pairs.length; i += 2) {
|
|
324
|
+
const name = pairs[i];
|
|
325
|
+
const value = pairs[i + 1];
|
|
326
|
+
const lower = name.toLowerCase();
|
|
327
|
+
req.rawHeaders.push(name, value);
|
|
328
|
+
req.headers[lower] = value;
|
|
329
|
+
}
|
|
330
|
+
if (this.listenerCount("upgrade") > 0) {
|
|
331
|
+
const socket = new NetSocket();
|
|
332
|
+
socket._attachOutputOnly(iostream);
|
|
333
|
+
this.emit("upgrade", req, socket, Buffer.alloc(0));
|
|
321
334
|
}
|
|
322
|
-
soupMsg.pause();
|
|
323
|
-
res.on("finish", () => {
|
|
324
|
-
soupMsg.unpause();
|
|
325
|
-
});
|
|
326
|
-
this.emit("request", req, res);
|
|
327
335
|
}
|
|
328
336
|
address() {
|
|
329
337
|
return this._address;
|
|
330
338
|
}
|
|
331
339
|
/**
|
|
332
340
|
* Register a WebSocket handler on this server (GJS only).
|
|
333
|
-
* Delegates to Soup.Server.add_websocket_handler()
|
|
341
|
+
* Delegates to the underlying `Soup.Server.add_websocket_handler()`.
|
|
334
342
|
* @param path URL path to handle WebSocket upgrades (e.g., '/ws')
|
|
335
343
|
* @param callback Called for each new WebSocket connection with the Soup.WebsocketConnection
|
|
336
344
|
*/
|
|
337
345
|
addWebSocketHandler(path, callback) {
|
|
338
|
-
if (!this.
|
|
346
|
+
if (!this._bridge) {
|
|
339
347
|
throw new Error("Server must be listening before adding WebSocket handlers. Call listen() first.");
|
|
340
348
|
}
|
|
341
|
-
this.
|
|
349
|
+
const soupServer = this._bridge.soup_server;
|
|
350
|
+
soupServer.add_websocket_handler(
|
|
342
351
|
path,
|
|
343
352
|
null,
|
|
344
353
|
null,
|
|
345
|
-
(_srv, _msg,
|
|
354
|
+
(_srv, _msg, _p, connection) => {
|
|
346
355
|
callback(connection);
|
|
347
356
|
}
|
|
348
357
|
);
|
|
349
358
|
}
|
|
350
359
|
close(callback) {
|
|
351
360
|
if (callback) this.once("close", callback);
|
|
352
|
-
if (this.
|
|
353
|
-
this.
|
|
354
|
-
this.
|
|
361
|
+
if (this._bridge) {
|
|
362
|
+
this._bridge.close();
|
|
363
|
+
this._bridge = null;
|
|
355
364
|
}
|
|
356
365
|
this.listening = false;
|
|
357
366
|
_activeServers.delete(this);
|
|
@@ -15,9 +15,12 @@ export declare class IncomingMessage extends Readable {
|
|
|
15
15
|
complete: boolean;
|
|
16
16
|
socket: any;
|
|
17
17
|
aborted: boolean;
|
|
18
|
+
/** Node.js legacy alias for socket — needed by engine.io and other HTTP consumers. */
|
|
19
|
+
get connection(): any;
|
|
18
20
|
private _timeoutTimer;
|
|
19
21
|
constructor();
|
|
20
22
|
_read(_size: number): void;
|
|
23
|
+
protected _autoClose(): void;
|
|
21
24
|
/** Finish the readable stream with the body data (used by server-side handler). */
|
|
22
25
|
_pushBody(body: Uint8Array | null): void;
|
|
23
26
|
setTimeout(msecs: number, callback?: () => void): this;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Duplex } from 'node:stream';
|
|
2
|
+
import type { Response as BridgeResponse } from '@gjsify/http-soup-bridge';
|
|
3
|
+
export declare class ServerRequestSocket extends Duplex {
|
|
4
|
+
readonly remoteAddress: string;
|
|
5
|
+
readonly remotePort: number;
|
|
6
|
+
readonly localAddress: string;
|
|
7
|
+
readonly localPort: number;
|
|
8
|
+
readonly remoteFamily = "IPv4";
|
|
9
|
+
readonly encrypted: boolean;
|
|
10
|
+
readonly connecting = false;
|
|
11
|
+
readonly pending = false;
|
|
12
|
+
bytesRead: number;
|
|
13
|
+
bytesWritten: number;
|
|
14
|
+
private readonly _bridgeRes;
|
|
15
|
+
private _bridgePaused;
|
|
16
|
+
constructor(remoteAddress: string, remotePort: number, localAddress: string, localPort: number, bridgeRes: BridgeResponse, encrypted?: boolean);
|
|
17
|
+
pause(): this;
|
|
18
|
+
resume(): this;
|
|
19
|
+
_read(_size: number): void;
|
|
20
|
+
_write(_chunk: unknown, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void;
|
|
21
|
+
destroySoon(): void;
|
|
22
|
+
setTimeout(_timeout: number, cb?: () => void): this;
|
|
23
|
+
setNoDelay(_noDelay?: boolean): this;
|
|
24
|
+
setKeepAlive(_enable?: boolean, _delay?: number): this;
|
|
25
|
+
ref(): this;
|
|
26
|
+
unref(): this;
|
|
27
|
+
address(): {
|
|
28
|
+
address: string;
|
|
29
|
+
family: string;
|
|
30
|
+
port: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
package/lib/types/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Soup from '@girs/soup-3.0';
|
|
2
1
|
import { EventEmitter } from 'node:events';
|
|
3
2
|
import { Writable } from 'node:stream';
|
|
3
|
+
import { type Response as BridgeResponse } from '@gjsify/http-soup-bridge';
|
|
4
4
|
import { IncomingMessage } from './incoming-message.js';
|
|
5
5
|
/**
|
|
6
6
|
* OutgoingMessage — Base class for ServerResponse and ClientRequest.
|
|
@@ -32,15 +32,18 @@ export declare class OutgoingMessage extends Writable {
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* ServerResponse — Writable stream representing an HTTP response.
|
|
35
|
-
*
|
|
35
|
+
*
|
|
36
|
+
* Holds a `BridgeResponse` from `@gjsify/http-soup-bridge`. All header /
|
|
37
|
+
* status / body operations delegate to the bridge, which handles the
|
|
38
|
+
* underlying Soup.ServerMessage in C-space (no JS-visible boxed types).
|
|
36
39
|
*/
|
|
37
40
|
export declare class ServerResponse extends OutgoingMessage {
|
|
38
41
|
statusCode: number;
|
|
39
42
|
statusMessage: string;
|
|
40
43
|
private _streaming;
|
|
41
|
-
private
|
|
44
|
+
private _bridge;
|
|
42
45
|
private _timeoutTimer;
|
|
43
|
-
constructor(
|
|
46
|
+
constructor(bridge: BridgeResponse);
|
|
44
47
|
/** Set a timeout for the response. Emits 'timeout' if response not sent within msecs. */
|
|
45
48
|
setTimeout(msecs: number, callback?: () => void): this;
|
|
46
49
|
/** Write the status line and headers. */
|
|
@@ -54,21 +57,24 @@ export declare class ServerResponse extends OutgoingMessage {
|
|
|
54
57
|
/** Add trailing headers for chunked transfer encoding. */
|
|
55
58
|
addTrailers(headers: Record<string, string>): void;
|
|
56
59
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
60
|
+
* Push our header map to the bridge and call write_head() on first
|
|
61
|
+
* write. Idempotent.
|
|
59
62
|
*/
|
|
60
63
|
private _startStreaming;
|
|
61
|
-
/** Writable stream _write — sends headers on first call, then appends
|
|
64
|
+
/** Writable stream _write — sends headers on first call, then appends each chunk. */
|
|
62
65
|
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
|
63
|
-
/** Called by Writable.end() —
|
|
66
|
+
/** Called by Writable.end() — finishes the bridge response. */
|
|
64
67
|
_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
68
|
/** Write status + headers + body in one call (convenience). */
|
|
68
69
|
end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void): this;
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* HTTP Server
|
|
72
|
+
* HTTP Server — wraps a `@gjsify/http-soup-bridge` Server.
|
|
73
|
+
*
|
|
74
|
+
* Public API matches Node.js `http.Server`. Internal differences from the
|
|
75
|
+
* pre-bridge version are invisible to consumers (Hono / Express / MCP /
|
|
76
|
+
* engine.io / `@gjsify/ws`) — they continue to use `.on('request', …)`,
|
|
77
|
+
* `.on('upgrade', …)`, `.listen()`, `.close()`, `.address()`, etc.
|
|
72
78
|
*/
|
|
73
79
|
export declare class Server extends EventEmitter {
|
|
74
80
|
listening: boolean;
|
|
@@ -77,14 +83,19 @@ export declare class Server extends EventEmitter {
|
|
|
77
83
|
keepAliveTimeout: number;
|
|
78
84
|
headersTimeout: number;
|
|
79
85
|
requestTimeout: number;
|
|
80
|
-
private
|
|
86
|
+
private _bridge;
|
|
81
87
|
private _address;
|
|
88
|
+
/** Exposes the underlying Soup.Server so consumers (e.g. WebSocketServer
|
|
89
|
+
* in `@gjsify/ws`) can register additional handlers (websocket, path-
|
|
90
|
+
* specific) on the same server instance without port-sharing conflicts. */
|
|
91
|
+
get soupServer(): unknown;
|
|
82
92
|
constructor(requestListener?: ((req: IncomingMessage, res: ServerResponse) => void) | Record<string, unknown>);
|
|
83
93
|
constructor(options: Record<string, unknown>, requestListener?: (req: IncomingMessage, res: ServerResponse) => void);
|
|
84
94
|
listen(port?: number, hostname?: string, backlog?: number, callback?: () => void): this;
|
|
85
95
|
listen(port?: number, hostname?: string, callback?: () => void): this;
|
|
86
96
|
listen(port?: number, callback?: () => void): this;
|
|
87
97
|
private _handleRequest;
|
|
98
|
+
private _handleUpgrade;
|
|
88
99
|
address(): {
|
|
89
100
|
port: number;
|
|
90
101
|
family: string;
|
|
@@ -92,7 +103,7 @@ export declare class Server extends EventEmitter {
|
|
|
92
103
|
} | null;
|
|
93
104
|
/**
|
|
94
105
|
* Register a WebSocket handler on this server (GJS only).
|
|
95
|
-
* Delegates to Soup.Server.add_websocket_handler()
|
|
106
|
+
* Delegates to the underlying `Soup.Server.add_websocket_handler()`.
|
|
96
107
|
* @param path URL path to handle WebSocket upgrades (e.g., '/ws')
|
|
97
108
|
* @param callback Called for each new WebSocket connection with the Soup.WebsocketConnection
|
|
98
109
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Node.js http module for Gjs",
|
|
5
5
|
"module": "lib/esm/index.js",
|
|
6
6
|
"types": "lib/types/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
22
22
|
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
23
23
|
"test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
|
|
24
|
-
"test:gjs": "
|
|
24
|
+
"test:gjs": "gjsify run test.gjs.mjs",
|
|
25
25
|
"test:node": "node test.node.mjs"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -30,20 +30,21 @@
|
|
|
30
30
|
"http"
|
|
31
31
|
],
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@gjsify/cli": "^0.
|
|
34
|
-
"@gjsify/unit": "^0.
|
|
33
|
+
"@gjsify/cli": "^0.2.0",
|
|
34
|
+
"@gjsify/unit": "^0.2.0",
|
|
35
35
|
"@types/node": "^25.6.0",
|
|
36
|
-
"typescript": "^6.0.
|
|
36
|
+
"typescript": "^6.0.3"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.
|
|
40
|
-
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.
|
|
41
|
-
"@girs/soup-3.0": "^3.6.6-4.0.0-rc.
|
|
42
|
-
"@gjsify/buffer": "^0.
|
|
43
|
-
"@gjsify/events": "^0.
|
|
44
|
-
"@gjsify/
|
|
45
|
-
"@gjsify/
|
|
46
|
-
"@gjsify/
|
|
47
|
-
"@gjsify/
|
|
39
|
+
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.9",
|
|
40
|
+
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
|
|
41
|
+
"@girs/soup-3.0": "^3.6.6-4.0.0-rc.9",
|
|
42
|
+
"@gjsify/buffer": "^0.2.0",
|
|
43
|
+
"@gjsify/events": "^0.2.0",
|
|
44
|
+
"@gjsify/http-soup-bridge": "^0.2.0",
|
|
45
|
+
"@gjsify/net": "^0.2.0",
|
|
46
|
+
"@gjsify/stream": "^0.2.0",
|
|
47
|
+
"@gjsify/url": "^0.2.0",
|
|
48
|
+
"@gjsify/utils": "^0.2.0"
|
|
48
49
|
}
|
|
49
50
|
}
|
package/src/incoming-message.ts
CHANGED
|
@@ -21,6 +21,9 @@ export class IncomingMessage extends Readable {
|
|
|
21
21
|
socket: any = null;
|
|
22
22
|
aborted = false;
|
|
23
23
|
|
|
24
|
+
/** Node.js legacy alias for socket — needed by engine.io and other HTTP consumers. */
|
|
25
|
+
get connection() { return this.socket; }
|
|
26
|
+
|
|
24
27
|
private _timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
28
|
|
|
26
29
|
constructor() {
|
|
@@ -31,6 +34,9 @@ export class IncomingMessage extends Readable {
|
|
|
31
34
|
// Data is pushed externally via _pushBody or _pushStream
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
// 'close' means connection lost, not body-stream end — don't auto-emit after 'end'.
|
|
38
|
+
protected _autoClose(): void { /* 'close' is emitted via destroy() only */ }
|
|
39
|
+
|
|
34
40
|
/** Finish the readable stream with the body data (used by server-side handler). */
|
|
35
41
|
_pushBody(body: Uint8Array | null): void {
|
|
36
42
|
if (body && body.length > 0) {
|