@gjsify/http 0.0.4 → 0.1.1

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 CHANGED
@@ -1,4 +1,33 @@
1
1
  # @gjsify/http
2
2
 
3
- ## Inspirations
3
+ GJS partial implementation of the Node.js `http` module using Soup 3.0. Provides Server, IncomingMessage, and ServerResponse.
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @gjsify/http
11
+ # or
12
+ yarn add @gjsify/http
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { createServer } from '@gjsify/http';
19
+
20
+ const server = createServer((req, res) => {
21
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
22
+ res.end('Hello World');
23
+ });
24
+ server.listen(3000);
25
+ ```
26
+
27
+ ## Inspirations and credits
28
+
4
29
  - https://github.com/node-fetch/node-fetch/blob/main/src/headers.js
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,228 @@
1
+ import GLib from "@girs/glib-2.0";
2
+ import Soup from "@girs/soup-3.0";
3
+ import Gio from "@girs/gio-2.0";
4
+ import { Buffer } from "node:buffer";
5
+ import { URL } from "node:url";
6
+ 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
228
+ };
@@ -0,0 +1,105 @@
1
+ const STATUS_CODES = {
2
+ 100: "Continue",
3
+ 101: "Switching Protocols",
4
+ 102: "Processing",
5
+ 103: "Early Hints",
6
+ 200: "OK",
7
+ 201: "Created",
8
+ 202: "Accepted",
9
+ 203: "Non-Authoritative Information",
10
+ 204: "No Content",
11
+ 205: "Reset Content",
12
+ 206: "Partial Content",
13
+ 207: "Multi-Status",
14
+ 208: "Already Reported",
15
+ 226: "IM Used",
16
+ 300: "Multiple Choices",
17
+ 301: "Moved Permanently",
18
+ 302: "Found",
19
+ 303: "See Other",
20
+ 304: "Not Modified",
21
+ 305: "Use Proxy",
22
+ 307: "Temporary Redirect",
23
+ 308: "Permanent Redirect",
24
+ 400: "Bad Request",
25
+ 401: "Unauthorized",
26
+ 402: "Payment Required",
27
+ 403: "Forbidden",
28
+ 404: "Not Found",
29
+ 405: "Method Not Allowed",
30
+ 406: "Not Acceptable",
31
+ 407: "Proxy Authentication Required",
32
+ 408: "Request Timeout",
33
+ 409: "Conflict",
34
+ 410: "Gone",
35
+ 411: "Length Required",
36
+ 412: "Precondition Failed",
37
+ 413: "Payload Too Large",
38
+ 414: "URI Too Long",
39
+ 415: "Unsupported Media Type",
40
+ 416: "Range Not Satisfiable",
41
+ 417: "Expectation Failed",
42
+ 418: "I'm a Teapot",
43
+ 421: "Misdirected Request",
44
+ 422: "Unprocessable Entity",
45
+ 423: "Locked",
46
+ 424: "Failed Dependency",
47
+ 425: "Too Early",
48
+ 426: "Upgrade Required",
49
+ 428: "Precondition Required",
50
+ 429: "Too Many Requests",
51
+ 431: "Request Header Fields Too Large",
52
+ 451: "Unavailable For Legal Reasons",
53
+ 500: "Internal Server Error",
54
+ 501: "Not Implemented",
55
+ 502: "Bad Gateway",
56
+ 503: "Service Unavailable",
57
+ 504: "Gateway Timeout",
58
+ 505: "HTTP Version Not Supported",
59
+ 506: "Variant Also Negotiates",
60
+ 507: "Insufficient Storage",
61
+ 508: "Loop Detected",
62
+ 510: "Not Extended",
63
+ 511: "Network Authentication Required"
64
+ };
65
+ const METHODS = [
66
+ "ACL",
67
+ "BIND",
68
+ "CHECKOUT",
69
+ "CONNECT",
70
+ "COPY",
71
+ "DELETE",
72
+ "GET",
73
+ "HEAD",
74
+ "LINK",
75
+ "LOCK",
76
+ "M-SEARCH",
77
+ "MERGE",
78
+ "MKACTIVITY",
79
+ "MKCALENDAR",
80
+ "MKCOL",
81
+ "MOVE",
82
+ "NOTIFY",
83
+ "OPTIONS",
84
+ "PATCH",
85
+ "POST",
86
+ "PRI",
87
+ "PROPFIND",
88
+ "PROPPATCH",
89
+ "PURGE",
90
+ "PUT",
91
+ "REBIND",
92
+ "REPORT",
93
+ "SEARCH",
94
+ "SOURCE",
95
+ "SUBSCRIBE",
96
+ "TRACE",
97
+ "UNBIND",
98
+ "UNLINK",
99
+ "UNLOCK",
100
+ "UNSUBSCRIBE"
101
+ ];
102
+ export {
103
+ METHODS,
104
+ STATUS_CODES
105
+ };
@@ -0,0 +1,59 @@
1
+ import { Readable } from "node:stream";
2
+ import { Buffer } from "node:buffer";
3
+ class IncomingMessage extends Readable {
4
+ httpVersion = "1.1";
5
+ httpVersionMajor = 1;
6
+ httpVersionMinor = 1;
7
+ headers = {};
8
+ rawHeaders = [];
9
+ method;
10
+ url;
11
+ statusCode;
12
+ statusMessage;
13
+ complete = false;
14
+ socket = null;
15
+ aborted = false;
16
+ _timeoutTimer = null;
17
+ constructor() {
18
+ super();
19
+ }
20
+ _read(_size) {
21
+ }
22
+ /** Finish the readable stream with the body data (used by server-side handler). */
23
+ _pushBody(body) {
24
+ if (body && body.length > 0) {
25
+ this.push(Buffer.from(body));
26
+ }
27
+ this.push(null);
28
+ this.complete = true;
29
+ if (this._timeoutTimer) {
30
+ clearTimeout(this._timeoutTimer);
31
+ this._timeoutTimer = null;
32
+ }
33
+ }
34
+ setTimeout(msecs, callback) {
35
+ if (this._timeoutTimer) {
36
+ clearTimeout(this._timeoutTimer);
37
+ this._timeoutTimer = null;
38
+ }
39
+ if (callback) this.once("timeout", callback);
40
+ if (msecs > 0) {
41
+ this._timeoutTimer = setTimeout(() => {
42
+ this._timeoutTimer = null;
43
+ this.emit("timeout");
44
+ }, msecs);
45
+ }
46
+ return this;
47
+ }
48
+ destroy(error) {
49
+ if (this._timeoutTimer) {
50
+ clearTimeout(this._timeoutTimer);
51
+ this._timeoutTimer = null;
52
+ }
53
+ this.aborted = true;
54
+ return super.destroy(error);
55
+ }
56
+ }
57
+ export {
58
+ IncomingMessage
59
+ };
package/lib/esm/index.js CHANGED
@@ -1,18 +1,128 @@
1
+ import { STATUS_CODES, METHODS } from "./constants.js";
2
+ import { IncomingMessage } from "./incoming-message.js";
3
+ import { OutgoingMessage, Server, ServerResponse } from "./server.js";
4
+ import { ClientRequest } from "./client-request.js";
5
+ import { IncomingMessage as IncomingMessage2 } from "./incoming-message.js";
6
+ import { OutgoingMessage as OutgoingMessage2, Server as Server2, ServerResponse as ServerResponse2 } from "./server.js";
7
+ import { ClientRequest as ClientRequest2 } from "./client-request.js";
8
+ import { URL } from "node:url";
1
9
  function validateHeaderName(name) {
2
- if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) {
10
+ if (typeof name !== "string" || !/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) {
3
11
  const error = new TypeError(`Header name must be a valid HTTP token ["${name}"]`);
4
12
  Object.defineProperty(error, "code", { value: "ERR_INVALID_HTTP_TOKEN" });
5
13
  throw error;
6
14
  }
7
15
  }
8
16
  function validateHeaderValue(name, value) {
9
- if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) {
17
+ if (value === void 0) {
18
+ const error = new TypeError(`Header "${name}" value must not be undefined`);
19
+ Object.defineProperty(error, "code", { value: "ERR_HTTP_INVALID_HEADER_VALUE" });
20
+ throw error;
21
+ }
22
+ if (typeof value === "string" && /[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) {
10
23
  const error = new TypeError(`Invalid character in header content ["${name}"]`);
11
24
  Object.defineProperty(error, "code", { value: "ERR_INVALID_CHAR" });
12
25
  throw error;
13
26
  }
14
27
  }
28
+ class Agent {
29
+ defaultPort = 80;
30
+ protocol = "http:";
31
+ maxSockets;
32
+ maxTotalSockets;
33
+ maxFreeSockets;
34
+ keepAliveMsecs;
35
+ keepAlive;
36
+ scheduling;
37
+ /** Pending requests per host (compatibility — Soup manages internally). */
38
+ requests = {};
39
+ /** Active sockets per host (compatibility — Soup manages internally). */
40
+ sockets = {};
41
+ /** Idle sockets per host (compatibility — Soup manages internally). */
42
+ freeSockets = {};
43
+ constructor(options) {
44
+ this.keepAlive = options?.keepAlive ?? false;
45
+ this.keepAliveMsecs = options?.keepAliveMsecs ?? 1e3;
46
+ this.maxSockets = options?.maxSockets ?? Infinity;
47
+ this.maxTotalSockets = options?.maxTotalSockets ?? Infinity;
48
+ this.maxFreeSockets = options?.maxFreeSockets ?? 256;
49
+ this.scheduling = options?.scheduling ?? "lifo";
50
+ }
51
+ /** Destroy the agent and close idle connections. */
52
+ destroy() {
53
+ }
54
+ /** Return a connection pool key for the given options. */
55
+ getName(options) {
56
+ let name = options.host || "localhost";
57
+ if (options.port) name += ":" + options.port;
58
+ if (options.localAddress) name += ":" + options.localAddress;
59
+ if (options.family === 4 || options.family === 6) name += ":" + options.family;
60
+ return name;
61
+ }
62
+ }
63
+ const globalAgent = new Agent();
64
+ function createServer(options, requestListener) {
65
+ if (typeof options === "function") {
66
+ return new Server2(options);
67
+ }
68
+ return new Server2(requestListener);
69
+ }
70
+ function request(url, options, callback) {
71
+ return new ClientRequest2(url, options, callback);
72
+ }
73
+ function get(url, options, callback) {
74
+ let opts;
75
+ let cb = callback;
76
+ if (typeof url === "string" || url instanceof URL) {
77
+ opts = typeof options === "object" ? { ...options, method: "GET" } : { method: "GET" };
78
+ if (typeof options === "function") cb = options;
79
+ } else {
80
+ opts = { ...url, method: "GET" };
81
+ if (typeof options === "function") cb = options;
82
+ url = opts;
83
+ }
84
+ const req = typeof url === "string" || url instanceof URL ? new ClientRequest2(url, { ...opts, method: "GET" }, cb) : new ClientRequest2({ ...opts, method: "GET" }, cb);
85
+ req.end();
86
+ return req;
87
+ }
88
+ const maxHeaderSize = 16384;
89
+ function setMaxIdleHTTPParsers(_max) {
90
+ }
91
+ import { STATUS_CODES as _STATUS_CODES, METHODS as _METHODS } from "./constants.js";
92
+ var index_default = {
93
+ STATUS_CODES: _STATUS_CODES,
94
+ METHODS: _METHODS,
95
+ Server: Server2,
96
+ IncomingMessage: IncomingMessage2,
97
+ OutgoingMessage: OutgoingMessage2,
98
+ ServerResponse: ServerResponse2,
99
+ ClientRequest: ClientRequest2,
100
+ Agent,
101
+ globalAgent,
102
+ createServer,
103
+ request,
104
+ get,
105
+ validateHeaderName,
106
+ validateHeaderValue,
107
+ maxHeaderSize,
108
+ setMaxIdleHTTPParsers
109
+ };
15
110
  export {
111
+ Agent,
112
+ ClientRequest,
113
+ IncomingMessage,
114
+ METHODS,
115
+ OutgoingMessage,
116
+ STATUS_CODES,
117
+ Server,
118
+ ServerResponse,
119
+ createServer,
120
+ index_default as default,
121
+ get,
122
+ globalAgent,
123
+ maxHeaderSize,
124
+ request,
125
+ setMaxIdleHTTPParsers,
16
126
  validateHeaderName,
17
127
  validateHeaderValue
18
128
  };