@gjsify/http2 0.4.0 → 0.4.3

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/package.json CHANGED
@@ -1,47 +1,50 @@
1
1
  {
2
- "name": "@gjsify/http2",
3
- "version": "0.4.0",
4
- "description": "Node.js http2 module for Gjs",
5
- "type": "module",
6
- "module": "lib/esm/index.js",
7
- "types": "lib/types/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/types/index.d.ts",
11
- "default": "./lib/esm/index.js"
2
+ "name": "@gjsify/http2",
3
+ "version": "0.4.3",
4
+ "description": "Node.js http2 module for Gjs",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib"
16
+ ],
17
+ "scripts": {
18
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
19
+ "check": "tsc --noEmit",
20
+ "build": "gjsify run build:gjsify && gjsify run build:types",
21
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
22
+ "build:types": "tsc",
23
+ "build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
24
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
25
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
26
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
27
+ "test:gjs": "gjsify run test.gjs.mjs",
28
+ "test:node": "node test.node.mjs"
29
+ },
30
+ "keywords": [
31
+ "gjs",
32
+ "node",
33
+ "http2"
34
+ ],
35
+ "devDependencies": {
36
+ "@gjsify/cli": "workspace:^",
37
+ "@gjsify/unit": "workspace:^",
38
+ "@types/node": "^25.6.2",
39
+ "typescript": "^6.0.3"
40
+ },
41
+ "dependencies": {
42
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
43
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
44
+ "@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
45
+ "@girs/soup-3.0": "3.6.6-4.0.0-rc.15",
46
+ "@gjsify/events": "workspace:^",
47
+ "@gjsify/http2-native": "workspace:^",
48
+ "@gjsify/utils": "workspace:^"
12
49
  }
13
- },
14
- "scripts": {
15
- "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
16
- "check": "tsc --noEmit",
17
- "build": "yarn build:gjsify && yarn build:types",
18
- "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
19
- "build:types": "tsc",
20
- "build:test": "yarn build:test:gjs && yarn build:test:node",
21
- "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
22
- "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
23
- "test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
24
- "test:gjs": "gjsify run test.gjs.mjs",
25
- "test:node": "node test.node.mjs"
26
- },
27
- "keywords": [
28
- "gjs",
29
- "node",
30
- "http2"
31
- ],
32
- "devDependencies": {
33
- "@gjsify/cli": "^0.4.0",
34
- "@gjsify/unit": "^0.4.0",
35
- "@types/node": "^25.6.2",
36
- "typescript": "^6.0.3"
37
- },
38
- "dependencies": {
39
- "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
40
- "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
41
- "@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
42
- "@girs/soup-3.0": "3.6.6-4.0.0-rc.15",
43
- "@gjsify/events": "^0.4.0",
44
- "@gjsify/http2-native": "^0.4.0",
45
- "@gjsify/utils": "^0.4.0"
46
- }
47
- }
50
+ }
@@ -1,352 +0,0 @@
1
- // Reference: Node.js lib/internal/http2/core.js
2
- // Reimplemented for GJS using Soup.Session (HTTP/2 transparently via ALPN over HTTPS)
3
- //
4
- // Phase 1 limitations:
5
- // - connect() over HTTP (non-TLS) uses HTTP/1.1 only (Soup does not support h2c)
6
- // - connect() over HTTPS upgrades to HTTP/2 automatically via ALPN if the server supports it
7
- // - pushStream, stream IDs, flow control are Soup-internal — not exposed
8
- // - rejectUnauthorized: false is best-effort (requires system-level TLS trust or custom DB)
9
-
10
- import Soup from '@girs/soup-3.0';
11
- import Gio from '@girs/gio-2.0';
12
- import GLib from '@girs/glib-2.0';
13
- import { EventEmitter } from 'node:events';
14
- import { Duplex } from 'node:stream';
15
- import { Buffer } from 'node:buffer';
16
- import { readBytesAsync } from '@gjsify/utils';
17
- import { constants, getDefaultSettings, type Http2Settings } from './protocol.js';
18
-
19
- export type { Http2Settings };
20
-
21
- export interface ClientSessionOptions {
22
- maxDeflateDynamicTableSize?: number;
23
- maxSessionMemory?: number;
24
- maxHeaderListPairs?: number;
25
- maxOutstandingPings?: number;
26
- maxReservedRemoteStreams?: number;
27
- maxSendHeaderBlockLength?: number;
28
- paddingStrategy?: number;
29
- peerMaxHeaderListSize?: number;
30
- protocol?: string;
31
- settings?: Http2Settings;
32
- // TLS options
33
- rejectUnauthorized?: boolean;
34
- ca?: string | Buffer | Array<string | Buffer>;
35
- cert?: string | Buffer | Array<string | Buffer>;
36
- key?: string | Buffer | Array<string | Buffer>;
37
- ALPNProtocols?: string[];
38
- }
39
-
40
- export interface ClientStreamOptions {
41
- endStream?: boolean;
42
- exclusive?: boolean;
43
- parent?: number;
44
- weight?: number;
45
- waitForTrailers?: boolean;
46
- signal?: AbortSignal;
47
- }
48
-
49
- // ─── Http2Session (base) ──────────────────────────────────────────────────────
50
-
51
- export class Http2Session extends EventEmitter {
52
- readonly type: number = constants.NGHTTP2_SESSION_CLIENT;
53
- readonly alpnProtocol: string | undefined = undefined;
54
- readonly encrypted: boolean = false;
55
-
56
- private _closed = false;
57
- private _destroyed = false;
58
- private _settings: Http2Settings;
59
-
60
- constructor() {
61
- super();
62
- this._settings = getDefaultSettings();
63
- }
64
-
65
- get closed(): boolean { return this._closed; }
66
- get destroyed(): boolean { return this._destroyed; }
67
- get connecting(): boolean { return false; }
68
- get pendingSettingsAck(): boolean { return false; }
69
- get localSettings(): Http2Settings { return { ...this._settings }; }
70
- get remoteSettings(): Http2Settings { return getDefaultSettings(); }
71
- get originSet(): Set<string> { return new Set(); }
72
-
73
- settings(settings: Http2Settings, callback?: () => void): void {
74
- Object.assign(this._settings, settings);
75
- if (callback) Promise.resolve().then(callback);
76
- }
77
-
78
- goaway(code?: number, _lastStreamId?: number, _data?: Uint8Array): void {
79
- this.emit('goaway', code ?? constants.NGHTTP2_NO_ERROR);
80
- this.destroy();
81
- }
82
-
83
- ping(payload?: Uint8Array, callback?: (err: Error | null, duration: number, payload: Uint8Array) => void): boolean {
84
- const buf = payload || new Uint8Array(8);
85
- if (callback) Promise.resolve().then(() => callback(null, 0, buf));
86
- return true;
87
- }
88
-
89
- close(callback?: () => void): void {
90
- if (this._closed) return;
91
- this._closed = true;
92
- this.emit('close');
93
- if (callback) callback();
94
- }
95
-
96
- destroy(error?: Error, code?: number): void {
97
- if (this._destroyed) return;
98
- this._destroyed = true;
99
- this._closed = true;
100
- if (error) this.emit('error', error);
101
- if (code !== undefined) this.emit('goaway', code);
102
- this.emit('close');
103
- }
104
-
105
- ref(): void {}
106
- unref(): void {}
107
- }
108
-
109
- // ─── ClientHttp2Stream ────────────────────────────────────────────────────────
110
- // Duplex: writable = request body (buffered until end()), readable = response body.
111
- // The Soup request is dispatched when end() is called.
112
-
113
- export class ClientHttp2Stream extends Duplex {
114
- readonly id = 1;
115
- readonly pending = false;
116
- readonly aborted = false;
117
- readonly bufferSize = 0;
118
- readonly endAfterHeaders = false;
119
-
120
- private _session: ClientHttp2Session;
121
- private _requestHeaders: Record<string, string | string[]>;
122
- private _requestChunks: Buffer[] = [];
123
- private _cancellable: Gio.Cancellable;
124
- private _state: number = constants.NGHTTP2_STREAM_STATE_OPEN;
125
- private _responseHeaders: Record<string, string | string[]> = {};
126
-
127
- get state(): number { return this._state; }
128
- get rstCode(): number { return constants.NGHTTP2_NO_ERROR; }
129
- get session(): ClientHttp2Session { return this._session; }
130
- get sentHeaders(): Record<string, string | string[]> { return this._requestHeaders; }
131
-
132
- constructor(session: ClientHttp2Session, requestHeaders: Record<string, string | string[]>) {
133
- super();
134
- this._session = session;
135
- this._requestHeaders = requestHeaders;
136
- this._cancellable = new Gio.Cancellable();
137
- }
138
-
139
- _read(_size: number): void {}
140
-
141
- _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
142
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding as BufferEncoding);
143
- this._requestChunks.push(buf);
144
- callback();
145
- }
146
-
147
- _final(callback: (error?: Error | null) => void): void {
148
- this._sendRequest()
149
- .then(() => callback())
150
- .catch((err) => callback(err instanceof Error ? err : new Error(String(err))));
151
- }
152
-
153
- private async _sendRequest(): Promise<void> {
154
- const session = this._session._getSoupSession();
155
- const authority = this._session._getAuthority();
156
-
157
- const method = (this._requestHeaders[':method'] as string) || 'GET';
158
- const path = (this._requestHeaders[':path'] as string) || '/';
159
- const scheme = (this._requestHeaders[':scheme'] as string) || (authority.startsWith('https') ? 'https' : 'http');
160
- const host = (this._requestHeaders[':authority'] as string) || authority.replace(/^https?:\/\//, '');
161
-
162
- const url = `${scheme}://${host}${path}`;
163
- let uri: GLib.Uri;
164
- try {
165
- uri = GLib.Uri.parse(url, GLib.UriFlags.NONE);
166
- } catch (err) {
167
- throw new Error(`Invalid HTTP/2 request URL: ${url}`);
168
- }
169
-
170
- const message = new Soup.Message({ method, uri });
171
-
172
- // Apply request headers (skip HTTP/2 pseudo-headers)
173
- const reqHeaders = message.get_request_headers();
174
- for (const [key, value] of Object.entries(this._requestHeaders)) {
175
- if (key.startsWith(':')) continue;
176
- if (Array.isArray(value)) {
177
- for (const v of value) reqHeaders.append(key, v);
178
- } else {
179
- reqHeaders.replace(key, value as string);
180
- }
181
- }
182
-
183
- // Set request body if any
184
- const body = Buffer.concat(this._requestChunks);
185
- if (body.length > 0) {
186
- const contentType = (this._requestHeaders['content-type'] as string) || 'application/octet-stream';
187
- message.set_request_body_from_bytes(contentType, new GLib.Bytes(body));
188
- }
189
-
190
- try {
191
- const inputStream = await new Promise<Gio.InputStream>((resolve, reject) => {
192
- session.send_async(message, GLib.PRIORITY_DEFAULT, this._cancellable, (_self: any, asyncRes: Gio.AsyncResult) => {
193
- try {
194
- resolve(session.send_finish(asyncRes));
195
- } catch (error) {
196
- reject(error);
197
- }
198
- });
199
- });
200
-
201
- // Collect response headers
202
- const statusCode = message.status_code;
203
- const responseHeaders = message.get_response_headers();
204
- const headersObj: Record<string, string | string[]> = { ':status': String(statusCode) };
205
- responseHeaders.foreach((name: string, value: string) => {
206
- const lower = name.toLowerCase();
207
- if (lower in headersObj) {
208
- const existing = headersObj[lower];
209
- if (Array.isArray(existing)) {
210
- existing.push(value);
211
- } else {
212
- headersObj[lower] = [existing as string, value];
213
- }
214
- } else {
215
- headersObj[lower] = value;
216
- }
217
- });
218
- this._responseHeaders = headersObj;
219
- this._state = constants.NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL;
220
-
221
- // Emit 'response' before pushing body data so listeners can attach
222
- this.emit('response', headersObj, 0);
223
-
224
- // Stream response body
225
- try {
226
- let chunk: Uint8Array | null;
227
- while ((chunk = await readBytesAsync(inputStream, 16384, GLib.PRIORITY_DEFAULT, this._cancellable)) !== null) {
228
- if (chunk.length > 0) {
229
- this.push(Buffer.from(chunk));
230
- }
231
- }
232
- } catch (_readErr) {
233
- // Connection reset — push what we have
234
- }
235
-
236
- this.push(null);
237
- this._state = constants.NGHTTP2_STREAM_STATE_CLOSED;
238
- } catch (error: any) {
239
- this._state = constants.NGHTTP2_STREAM_STATE_CLOSED;
240
- if (!this._cancellable.is_cancelled()) {
241
- this.destroy(error instanceof Error ? error : new Error(String(error)));
242
- }
243
- }
244
- }
245
-
246
- close(code?: number, callback?: () => void): void {
247
- this._cancellable.cancel();
248
- this._state = constants.NGHTTP2_STREAM_STATE_CLOSED;
249
- this.emit('close', code ?? constants.NGHTTP2_NO_ERROR);
250
- if (callback) callback();
251
- }
252
-
253
- priority(_options: { exclusive?: boolean; parent?: number; weight?: number; silent?: boolean }): void {}
254
- sendTrailers(_headers: Record<string, string | string[]>): void {}
255
- setTimeout(_msecs: number, _callback?: () => void): this { return this; }
256
- }
257
-
258
- // ─── ClientHttp2Session ───────────────────────────────────────────────────────
259
-
260
- export class ClientHttp2Session extends Http2Session {
261
- override readonly type = constants.NGHTTP2_SESSION_CLIENT;
262
- declare readonly encrypted: boolean;
263
-
264
- private _authority: string;
265
- private _soupSession: Soup.Session;
266
- private _streams: Set<ClientHttp2Stream> = new Set();
267
-
268
- constructor(authority: string, options: ClientSessionOptions = {}) {
269
- super();
270
- this._authority = authority;
271
- this.encrypted = authority.startsWith('https:');
272
-
273
- this._soupSession = new Soup.Session();
274
-
275
- // Configure TLS for rejectUnauthorized: false (common in testing with self-signed certs)
276
- if (options.rejectUnauthorized === false) {
277
- // Connect to the accept-certificate signal on each message via session
278
- // This is a best-effort approach; system CA store may still reject the cert
279
- (this._soupSession as any).connect('accept-certificate',
280
- (_msg: any, _cert: any, _errors: any) => {
281
- return true;
282
- }
283
- );
284
- }
285
-
286
- // Emit 'connect' asynchronously after construction
287
- Promise.resolve().then(() => {
288
- if (!this.destroyed) {
289
- (this as any).alpnProtocol = this.encrypted ? 'h2' : undefined;
290
- this.emit('connect', this, null);
291
- }
292
- });
293
- }
294
-
295
- /** @internal Used by ClientHttp2Stream to get the Soup.Session */
296
- _getSoupSession(): Soup.Session {
297
- return this._soupSession;
298
- }
299
-
300
- /** @internal Used by ClientHttp2Stream to build the request URL */
301
- _getAuthority(): string {
302
- return this._authority;
303
- }
304
-
305
- request(headers: Record<string, string | string[]>, options?: ClientStreamOptions): ClientHttp2Stream {
306
- if (this.destroyed || this.closed) {
307
- throw new Error('Session is closed');
308
- }
309
-
310
- // Fill in missing pseudo-headers from the authority
311
- const finalHeaders = { ...headers };
312
- if (!finalHeaders[':scheme']) {
313
- finalHeaders[':scheme'] = this.encrypted ? 'https' : 'http';
314
- }
315
- if (!finalHeaders[':authority']) {
316
- finalHeaders[':authority'] = this._authority.replace(/^https?:\/\//, '');
317
- }
318
- if (!finalHeaders[':method']) {
319
- finalHeaders[':method'] = 'GET';
320
- }
321
- if (!finalHeaders[':path']) {
322
- finalHeaders[':path'] = '/';
323
- }
324
-
325
- const stream = new ClientHttp2Stream(this, finalHeaders);
326
- this._streams.add(stream);
327
- stream.once('close', () => this._streams.delete(stream));
328
-
329
- if (options?.endStream) {
330
- // No request body — end immediately to trigger the request
331
- stream.end();
332
- }
333
-
334
- return stream;
335
- }
336
-
337
- override close(callback?: () => void): void {
338
- for (const stream of this._streams) {
339
- stream.close();
340
- }
341
- this._streams.clear();
342
- super.close(callback);
343
- }
344
-
345
- override destroy(error?: Error, code?: number): void {
346
- for (const stream of this._streams) {
347
- stream.close();
348
- }
349
- this._streams.clear();
350
- super.destroy(error, code);
351
- }
352
- }