quico 0.1.1 → 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/package.json CHANGED
@@ -1,28 +1,83 @@
1
1
  {
2
2
  "name": "quico",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
5
- "main": "index.js",
6
- "scripts": {
7
- "start": "node examples/webtransport_datagram_echo/server.js"
5
+ "type": "module",
6
+ "main": "index.cjs",
7
+ "module": "index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "require": "./index.cjs"
12
+ }
13
+ },
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "types": "index.d.ts",
18
+ "files": [
19
+ "index.js",
20
+ "index.cjs",
21
+ "index.d.ts",
22
+ "src/",
23
+ "LICENSE",
24
+ "README.md"
25
+ ],
26
+ "keywords": [
27
+ "quic",
28
+ "http3",
29
+ "http",
30
+ "https",
31
+ "h3",
32
+ "quic-node",
33
+ "http3-node",
34
+ "quic-server",
35
+ "quic-client",
36
+ "http3-server",
37
+ "http3-client",
38
+ "h3-node",
39
+ "webtransport",
40
+ "websocket",
41
+ "datagram",
42
+ "udp",
43
+ "qpack",
44
+ "tls",
45
+ "tls1.3",
46
+ "tls13",
47
+ "nodejs",
48
+ "backend",
49
+ "real-time",
50
+ "low-latency",
51
+ "transport",
52
+ "http-server",
53
+ "socketio"
54
+ ],
55
+ "author": {
56
+ "name": "colocohen",
57
+ "email": "support@quicojs.dev"
8
58
  },
9
- "keywords": ["quic", "http3", "h3", "webtransport", "datagram", "udp", "qpack", "tls", "tls1.3", "nodejs"],
10
- "author": "colocohen",
11
59
  "license": "Apache-2.0",
12
60
  "repository": {
13
61
  "type": "git",
14
62
  "url": "https://github.com/colocohen/quico.git"
15
63
  },
16
64
  "bugs": {
65
+ "email": "support@quicojs.dev",
17
66
  "url": "https://github.com/colocohen/quico/issues"
18
67
  },
19
- "homepage": "https://github.com/colocohen/quico",
68
+ "homepage": "https://github.com/colocohen/quico#readme",
69
+ "funding": [
70
+ {
71
+ "type": "github",
72
+ "url": "https://github.com/sponsors/colocohen"
73
+ },
74
+ {
75
+ "type": "opencollective",
76
+ "url": "https://opencollective.com/quicojs"
77
+ }
78
+ ],
20
79
  "dependencies": {
21
- "@noble/curves": ">=1.9.6",
22
- "@noble/hashes": ">=1.8.0",
23
- "@peculiar/x509": ">=1.13.0",
24
- "@stablelib/aes": ">=2.0.1",
25
- "@stablelib/gcm": ">=2.0.1",
26
- "flat-ranges": ">=1.0.2"
80
+ "flat-ranges": "^2.0.0",
81
+ "lemon-tls": "^0.2.0"
27
82
  }
28
- }
83
+ }
package/src/agent.js ADDED
@@ -0,0 +1,275 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import { EventEmitter } from 'node:events';
16
+
17
+
18
+ // ============================================================
19
+ // Agent — connection pooling + protocol discovery
20
+ //
21
+ // Mirrors node:https.Agent API surface.
22
+ // Caches which protocol works per host so subsequent requests
23
+ // skip the discovery step.
24
+ //
25
+ // Options:
26
+ // http2: true (default) — allow HTTP/2 in fallback chain
27
+ // http1: true (default) — allow HTTP/1.1 in fallback chain
28
+ // h3Timeout: 3000 — ms before falling back from H3
29
+ // ============================================================
30
+
31
+ class Agent extends EventEmitter {
32
+
33
+ constructor(options) {
34
+ super();
35
+
36
+ options = options || {};
37
+
38
+ this.keepAlive = options.keepAlive !== false;
39
+ this.keepAliveMsecs = options.keepAliveMsecs || 1000;
40
+ this.maxSockets = options.maxSockets || 256;
41
+ this.maxFreeSockets = options.maxFreeSockets || 32;
42
+ this.timeout = options.timeout || 30000;
43
+
44
+ // Protocol flags (defaults for all requests through this agent)
45
+ this.http2 = options.http2 !== false;
46
+ this.http1 = options.http1 !== false;
47
+
48
+ // H3 connect timeout before falling back (ms)
49
+ this.h3Timeout = options.h3Timeout || 3000;
50
+
51
+ // Protocol cache: "host:port" → { protocol, ts, altSvcMaxAge }
52
+ // protocol: 'h3' | 'h2' | 'https'
53
+ this._protocolCache = {};
54
+
55
+ // Active QUIC connections: "host:port" → QUICConnection
56
+ // Allows multiplexing multiple requests over one QUIC connection
57
+ this._h3Pool = {};
58
+
59
+ // Active HTTP/2 sessions: "host:port" → http2.ClientHttp2Session
60
+ this._h2Pool = {};
61
+
62
+ // Request count per host (for maxSockets enforcement)
63
+ this._activeCounts = {};
64
+
65
+ // Sweep interval — clean stale pool entries
66
+ this._sweepTimer = setInterval(() => this._sweep(), 15000);
67
+ if (this._sweepTimer.unref) this._sweepTimer.unref();
68
+ }
69
+
70
+
71
+ // ============================================================
72
+ // Protocol cache
73
+ // ============================================================
74
+
75
+ /**
76
+ * Get cached protocol for a host, or null if unknown.
77
+ */
78
+ getProtocol(host, port) {
79
+ var key = host + ':' + (port || 443);
80
+ var entry = this._protocolCache[key];
81
+ if (!entry) return null;
82
+
83
+ // Check expiry (Alt-Svc max-age, default 24h)
84
+ var maxAge = entry.altSvcMaxAge || 86400000;
85
+ if (Date.now() - entry.ts > maxAge) {
86
+ delete this._protocolCache[key];
87
+ return null;
88
+ }
89
+
90
+ return entry.protocol;
91
+ }
92
+
93
+ /**
94
+ * Record which protocol works for a host.
95
+ */
96
+ setProtocol(host, port, protocol, altSvcMaxAge) {
97
+ var key = host + ':' + (port || 443);
98
+ this._protocolCache[key] = {
99
+ protocol: protocol,
100
+ ts: Date.now(),
101
+ altSvcMaxAge: altSvcMaxAge || 86400000
102
+ };
103
+ }
104
+
105
+
106
+ // ============================================================
107
+ // Alt-Svc header parsing
108
+ // ============================================================
109
+
110
+ /**
111
+ * Parse Alt-Svc header from an HTTP/1.1 or HTTP/2 response.
112
+ * If it advertises h3, cache it for future requests.
113
+ *
114
+ * Examples:
115
+ * Alt-Svc: h3=":443"; ma=86400
116
+ * Alt-Svc: h3=":443", h3-29=":443"
117
+ */
118
+ parseAltSvc(host, port, altSvcValue) {
119
+ if (!altSvcValue || altSvcValue === 'clear') {
120
+ var key = host + ':' + (port || 443);
121
+ delete this._protocolCache[key];
122
+ return;
123
+ }
124
+
125
+ var entries = altSvcValue.split(',');
126
+ for (var i = 0; i < entries.length; i++) {
127
+ var entry = entries[i].trim();
128
+ var match = entry.match(/^h3(?:-\d+)?="([^"]*)"(.*)$/);
129
+ if (match) {
130
+ var authority = match[1];
131
+ var params = match[2] || '';
132
+
133
+ var maMatch = params.match(/ma=(\d+)/);
134
+ var maxAge = maMatch ? parseInt(maMatch[1]) * 1000 : 86400000;
135
+
136
+ var h3Port = port;
137
+ var colonIdx = authority.lastIndexOf(':');
138
+ if (colonIdx >= 0) {
139
+ var parsed = parseInt(authority.substring(colonIdx + 1));
140
+ if (!isNaN(parsed)) h3Port = parsed;
141
+ }
142
+
143
+ this.setProtocol(host, h3Port, 'h3', maxAge);
144
+ return;
145
+ }
146
+ }
147
+ }
148
+
149
+
150
+ // ============================================================
151
+ // H3 connection pool
152
+ // Stores { quic, h3, udpSocket, _nextStreamId } objects
153
+ // from h3_client.js for multiplexing.
154
+ // ============================================================
155
+
156
+ getH3Connection(host, port) {
157
+ var key = host + ':' + (port || 443);
158
+ var conn = this._h3Pool[key];
159
+ if (!conn) return null;
160
+ var state = conn.quic ? conn.quic.state : 'closed';
161
+ if (state === 'closed' || state === 'draining') {
162
+ delete this._h3Pool[key];
163
+ return null;
164
+ }
165
+ return conn;
166
+ }
167
+
168
+ setH3Connection(host, port, conn) {
169
+ var key = host + ':' + (port || 443);
170
+ this._h3Pool[key] = conn;
171
+ if (conn.quic) {
172
+ conn.quic.on('close', () => {
173
+ if (this._h3Pool[key] === conn) delete this._h3Pool[key];
174
+ });
175
+ }
176
+ }
177
+
178
+
179
+ // ============================================================
180
+ // H2 session pool
181
+ // ============================================================
182
+
183
+ getH2Session(host, port) {
184
+ var key = host + ':' + (port || 443);
185
+ var session = this._h2Pool[key];
186
+ if (!session) return null;
187
+ if (session.closed || session.destroyed) {
188
+ delete this._h2Pool[key];
189
+ return null;
190
+ }
191
+ return session;
192
+ }
193
+
194
+ setH2Session(host, port, session) {
195
+ var key = host + ':' + (port || 443);
196
+ this._h2Pool[key] = session;
197
+ session.on('close', () => {
198
+ if (this._h2Pool[key] === session) delete this._h2Pool[key];
199
+ });
200
+ session.on('error', () => {
201
+ if (this._h2Pool[key] === session) delete this._h2Pool[key];
202
+ });
203
+ }
204
+
205
+
206
+ // ============================================================
207
+ // Active request tracking
208
+ // ============================================================
209
+
210
+ canCreateSocket(host, port) {
211
+ var key = host + ':' + (port || 443);
212
+ return (this._activeCounts[key] || 0) < this.maxSockets;
213
+ }
214
+
215
+ trackRequest(host, port) {
216
+ var key = host + ':' + (port || 443);
217
+ this._activeCounts[key] = (this._activeCounts[key] || 0) + 1;
218
+ }
219
+
220
+ releaseRequest(host, port) {
221
+ var key = host + ':' + (port || 443);
222
+ if (this._activeCounts[key]) {
223
+ this._activeCounts[key]--;
224
+ if (this._activeCounts[key] <= 0) delete this._activeCounts[key];
225
+ }
226
+ }
227
+
228
+
229
+ // ============================================================
230
+ // Sweep stale entries
231
+ // ============================================================
232
+
233
+ _sweep() {
234
+ var now = Date.now();
235
+
236
+ for (var key in this._protocolCache) {
237
+ var entry = this._protocolCache[key];
238
+ var maxAge = entry.altSvcMaxAge || 86400000;
239
+ if (now - entry.ts > maxAge) delete this._protocolCache[key];
240
+ }
241
+
242
+ for (var key in this._h3Pool) {
243
+ var conn = this._h3Pool[key];
244
+ var state = conn.quic ? conn.quic.state : 'closed';
245
+ if (state === 'closed' || state === 'draining') delete this._h3Pool[key];
246
+ }
247
+
248
+ for (var key in this._h2Pool) {
249
+ var session = this._h2Pool[key];
250
+ if (session.closed || session.destroyed) delete this._h2Pool[key];
251
+ }
252
+ }
253
+
254
+ destroy() {
255
+ if (this._sweepTimer) { clearInterval(this._sweepTimer); this._sweepTimer = null; }
256
+
257
+ for (var key in this._h3Pool) {
258
+ try { this._h3Pool[key].quic.close(0, 'agent destroy'); } catch (e) {}
259
+ }
260
+ this._h3Pool = {};
261
+
262
+ for (var key in this._h2Pool) {
263
+ try { this._h2Pool[key].close(); } catch (e) {}
264
+ }
265
+ this._h2Pool = {};
266
+
267
+ this._protocolCache = {};
268
+ this._activeCounts = {};
269
+ }
270
+ }
271
+
272
+
273
+ var globalAgent = new Agent();
274
+
275
+ export { Agent, globalAgent };