@gjsify/http 0.1.15 → 0.3.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.
@@ -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 { deferEmit, ensureMainLoop } from "@gjsify/utils";
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
- _soupMsg;
80
+ _bridge;
78
81
  _timeoutTimer = null;
79
- constructor(soupMsg) {
82
+ constructor(bridge) {
80
83
  super();
81
- this._soupMsg = soupMsg;
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
- * 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.
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
- responseHeaders.replace(key, value);
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 + flushes each chunk. */
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
- const responseBody = this._soupMsg.get_response_body();
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() — completes the body (streaming) or sends batch response (no-body). */
167
+ /** Called by Writable.end() — finishes the bridge response. */
171
168
  _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);
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
- const contentType = this._headers.get("content-type") || "text/plain";
205
- this._soupMsg.set_response(contentType, Soup.MemoryUse.COPY, new Uint8Array(0));
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
- _soupServer = null;
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._soupServer = new Soup.Server({});
252
- this._soupServer.add_handler(null, (server, msg, path) => {
253
- this._handleRequest(msg, path);
231
+ this._bridge = new BridgeServer();
232
+ this._bridge.connect("request-received", (_self, req, res) => {
233
+ this._handleRequest(req, res);
254
234
  });
255
- this._soupServer.listen_local(port, Soup.ServerListenOptions.IPV4_ONLY);
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: actualPort, family: "IPv4", address: hostname };
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 error = err instanceof Error ? err : new Error(String(err));
271
- if (this.listenerCount("error") === 0) {
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(soupMsg, path) {
253
+ _handleRequest(bridgeReq, bridgeRes) {
279
254
  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;
255
+ const res = new ServerResponse(bridgeRes);
256
+ req.method = bridgeReq.method;
257
+ req.url = bridgeReq.url;
285
258
  req.httpVersion = "1.1";
286
- const requestHeaders = soupMsg.get_request_headers();
287
- requestHeaders.foreach((name, value) => {
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
- existing.push(value);
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
- 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) {
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
- if (ioStream) {
310
- const socket = new NetSocket();
311
- socket._setupFromIOStream(ioStream);
312
- this.emit("upgrade", req, socket, Buffer.alloc(0));
313
- return;
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
- 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);
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._soupServer) {
346
+ if (!this._bridge) {
339
347
  throw new Error("Server must be listening before adding WebSocket handlers. Call listen() first.");
340
348
  }
341
- this._soupServer.add_websocket_handler(
349
+ const soupServer = this._bridge.soup_server;
350
+ soupServer.add_websocket_handler(
342
351
  path,
343
352
  null,
344
353
  null,
345
- (_srv, _msg, _path, connection) => {
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._soupServer) {
353
- this._soupServer.disconnect();
354
- this._soupServer = null;
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,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -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
+ }
@@ -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
- * Extends OutgoingMessage for shared header management.
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 _soupMsg;
44
+ private _bridge;
42
45
  private _timeoutTimer;
43
- constructor(soupMsg: Soup.ServerMessage);
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
- * 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.
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 + flushes each chunk. */
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() — completes the body (streaming) or sends batch response (no-body). */
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 wrapping Soup.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 _soupServer;
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.1.15",
3
+ "version": "0.3.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": "gjs -m test.gjs.mjs",
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.1.15",
34
- "@gjsify/unit": "^0.1.15",
33
+ "@gjsify/cli": "^0.3.0",
34
+ "@gjsify/unit": "^0.3.0",
35
35
  "@types/node": "^25.6.0",
36
- "typescript": "^6.0.2"
36
+ "typescript": "^6.0.3"
37
37
  },
38
38
  "dependencies": {
39
- "@girs/gio-2.0": "^2.88.0-4.0.0-rc.3",
40
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.3",
41
- "@girs/soup-3.0": "^3.6.6-4.0.0-rc.3",
42
- "@gjsify/buffer": "^0.1.15",
43
- "@gjsify/events": "^0.1.15",
44
- "@gjsify/net": "^0.1.15",
45
- "@gjsify/stream": "^0.1.15",
46
- "@gjsify/url": "^0.1.15",
47
- "@gjsify/utils": "^0.1.15"
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.3.0",
43
+ "@gjsify/events": "^0.3.0",
44
+ "@gjsify/http-soup-bridge": "^0.3.0",
45
+ "@gjsify/net": "^0.3.0",
46
+ "@gjsify/stream": "^0.3.0",
47
+ "@gjsify/url": "^0.3.0",
48
+ "@gjsify/utils": "^0.3.0"
48
49
  }
49
50
  }
@@ -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) {