@unireq/http2 0.0.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/LICENSE +21 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Olivier Orabona
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Connector, RequestContext, Response, TransportWithCapabilities } from '@unireq/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP/2 connector using Node.js native http2 module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface Http2ConnectorOptions {
|
|
8
|
+
readonly enablePush?: boolean;
|
|
9
|
+
readonly sessionTimeout?: number;
|
|
10
|
+
}
|
|
11
|
+
declare class Http2Connector implements Connector {
|
|
12
|
+
private readonly options;
|
|
13
|
+
private readonly sessionCache;
|
|
14
|
+
constructor(options?: Http2ConnectorOptions);
|
|
15
|
+
connect(uri: string): {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Get or create an HTTP/2 session for the given origin
|
|
20
|
+
*/
|
|
21
|
+
private getOrCreateSession;
|
|
22
|
+
request(_client: unknown, ctx: RequestContext): Promise<Response>;
|
|
23
|
+
disconnect(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @unireq/http2 - HTTP/2 transport using Node.js http2 module with ALPN
|
|
28
|
+
*
|
|
29
|
+
* ## Why a dedicated HTTP/2 transport?
|
|
30
|
+
*
|
|
31
|
+
* Node.js built-in `fetch` (undici) defaults to HTTP/1.1, even when the server supports HTTP/2.
|
|
32
|
+
* While undici can negotiate HTTP/2 via ALPN, it requires explicit configuration and doesn't
|
|
33
|
+
* happen automatically with the global `fetch` API.
|
|
34
|
+
*
|
|
35
|
+
* This package provides a dedicated HTTP/2 transport using `node:http2` for scenarios where:
|
|
36
|
+
* - HTTP/2 is required (not optional)
|
|
37
|
+
* - Server push is needed
|
|
38
|
+
* - Multiplexing over a single connection is critical
|
|
39
|
+
* - ALPN negotiation must be explicit
|
|
40
|
+
*
|
|
41
|
+
* @see https://undici.nodejs.org
|
|
42
|
+
* @see https://nodejs.org/api/http2.html
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates an HTTP/2 transport using node:http2 with ALPN
|
|
47
|
+
*
|
|
48
|
+
* @param uri - Optional base URI for the transport
|
|
49
|
+
* @param connector - Optional connector instance (defaults to Http2Connector)
|
|
50
|
+
* @returns Transport with capabilities
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const h2api = client(http2('https://api.example.com'), json());
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function http2(uri?: string, connector?: Connector): TransportWithCapabilities;
|
|
58
|
+
|
|
59
|
+
export { Http2Connector, type Http2ConnectorOptions, http2 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import*as S from'http';import*as w from'http2';var l=class{constructor(t={}){this.options=t;}sessionCache=new Map;connect(t){return {baseUrl:t}}getOrCreateSession(t){let{sessionTimeout:c=3e4}=this.options,i=this.sessionCache.get(t);if(i&&!i.destroyed)return i.session;let o=w.connect(t,{timeout:c}),r={session:o,destroyed:false};this.sessionCache.set(t,r);let e=()=>{r.destroyed=true,this.sessionCache.delete(t);};return o.on("close",e),o.on("goaway",e),o.on("error",e),o.on("timeout",e),o.unref(),o}async request(t,c){let{url:i,method:o,headers:r,body:e,signal:O}=c;return new Promise((q,y)=>{let d=new URL(i),h=this.getOrCreateSession(d.origin),m=s=>y(s);h.on("error",m),O?.addEventListener("abort",()=>{y(new Error("Request aborted"));});let H={":method":o,":path":d.pathname+d.search,":scheme":d.protocol.slice(0,-1),...r},n=h.request(H,{endStream:e===void 0});e!==void 0&&(typeof e=="string"||e instanceof Buffer?n.write(e):e instanceof ArrayBuffer?n.write(Buffer.from(e)):ArrayBuffer.isView(e)?n.write(Buffer.from(e.buffer,e.byteOffset,e.byteLength)):typeof e=="object"&&e!==null?n.write(JSON.stringify(e)):n.write(String(e)),n.end());let a=Buffer.alloc(0),C={},u=200,g="OK";n.on("response",s=>{u=s[":status"]||200,g=S.STATUS_CODES[u]||"OK";for(let[f,b]of Object.entries(s))f.startsWith(":")||(C[f]=Array.isArray(b)?b.join(", "):String(b));}),n.on("data",s=>{a=Buffer.concat([a,s]);}),n.on("end",()=>{h.off("error",m);let s=C["content-type"]||"",f;s.includes("application/json")?f=JSON.parse(a.toString()):s.includes("text/")?f=a.toString():f=a.buffer.slice(a.byteOffset,a.byteOffset+a.byteLength),q({status:u,statusText:g,headers:C,data:f,ok:u>=200&&u<300});}),n.on("error",s=>{h.off("error",m),y(s);});})}disconnect(){for(let t of this.sessionCache.values())t.destroyed||(t.session.close(),t.destroyed=true);this.sessionCache.clear();}};function R(){return new l}function B(p,t){let c,i=t??R();return {transport:async r=>{p&&!c&&(c=await i.connect(p));let e=p&&r.url.startsWith("/")?`${p}${r.url}`:r.url;return i.request(c,{...r,url:e})},capabilities:{streams:true,http2:true,serverPush:true}}}export{l as Http2Connector,B as http2};//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/connectors/native.ts","../src/index.ts"],"names":["Http2Connector","options","uri","origin","sessionTimeout","cached","session","entry","cleanupSession","_client","ctx","url","method","headers","body","signal","resolve","reject","parsedURL","sessionErrorHandler","err","h2Headers","req","responseData","responseHeaders","status","statusText","h2ResponseHeaders","key","value","chunk","contentType","data","createDefaultHttp2Connector","http2","connector","client","actualConnector","finalUrl"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * HTTP/2 connector using Node.js native http2 module\n */\n\nimport * as http from 'node:http';\nimport * as http2 from 'node:http2';\nimport type { Connector, RequestContext, Response } from '@unireq/core';\n\nexport interface Http2ConnectorOptions {\n readonly enablePush?: boolean;\n readonly sessionTimeout?: number;\n}\n\n/**\n * Session cache entry with the session and its state\n */\ninterface SessionCacheEntry {\n session: http2.ClientHttp2Session;\n destroyed: boolean;\n}\n\nexport class Http2Connector implements Connector {\n private readonly sessionCache = new Map<string, SessionCacheEntry>();\n\n constructor(private readonly options: Http2ConnectorOptions = {}) {}\n\n connect(uri: string) {\n return { baseUrl: uri };\n }\n\n /**\n * Get or create an HTTP/2 session for the given origin\n */\n private getOrCreateSession(origin: string): http2.ClientHttp2Session {\n const { sessionTimeout = 30000 } = this.options;\n\n // Check if we have a cached session that's still valid\n const cached = this.sessionCache.get(origin);\n if (cached && !cached.destroyed) {\n return cached.session;\n }\n\n // Create a new session\n const session = http2.connect(origin, {\n timeout: sessionTimeout,\n });\n\n const entry: SessionCacheEntry = {\n session,\n destroyed: false,\n };\n\n // Cache the session\n this.sessionCache.set(origin, entry);\n\n // Handle session lifecycle events to remove from cache\n const cleanupSession = () => {\n entry.destroyed = true;\n this.sessionCache.delete(origin);\n };\n\n session.on('close', cleanupSession);\n session.on('goaway', cleanupSession);\n session.on('error', cleanupSession);\n session.on('timeout', cleanupSession);\n\n // Unref the session so it doesn't prevent process exit\n session.unref();\n\n return session;\n }\n\n async request(_client: unknown, ctx: RequestContext): Promise<Response> {\n const { url, method, headers, body, signal } = ctx;\n\n return new Promise((resolve, reject) => {\n const parsedURL = new URL(url);\n\n const session = this.getOrCreateSession(parsedURL.origin);\n\n // Only add error handler for this request, not globally\n const sessionErrorHandler = (err: Error) => reject(err);\n session.on('error', sessionErrorHandler);\n\n signal?.addEventListener('abort', () => {\n reject(new Error('Request aborted'));\n });\n\n const h2Headers: http2.OutgoingHttpHeaders = {\n ':method': method,\n ':path': parsedURL.pathname + parsedURL.search,\n ':scheme': parsedURL.protocol.slice(0, -1),\n ...headers,\n };\n\n const req = session.request(h2Headers, {\n endStream: body === undefined,\n });\n\n if (body !== undefined) {\n if (typeof body === 'string') {\n req.write(body);\n } else if (body instanceof Buffer) {\n req.write(body);\n } else if (body instanceof ArrayBuffer) {\n req.write(Buffer.from(body));\n } else if (ArrayBuffer.isView(body)) {\n // Handle TypedArrays (Uint8Array, Int32Array, etc.) and DataView\n req.write(Buffer.from(body.buffer, body.byteOffset, body.byteLength));\n } else if (typeof body === 'object' && body !== null) {\n // Plain objects and arrays\n req.write(JSON.stringify(body));\n } else {\n // Fallback for primitives (numbers, booleans) - convert to string\n req.write(String(body));\n }\n req.end();\n }\n\n let responseData = Buffer.alloc(0);\n const responseHeaders: Record<string, string> = {};\n let status = 200;\n let statusText = 'OK';\n\n req.on('response', (h2ResponseHeaders) => {\n status = (h2ResponseHeaders[':status'] as number) || 200;\n statusText = http.STATUS_CODES[status] || 'OK';\n\n for (const [key, value] of Object.entries(h2ResponseHeaders)) {\n if (!key.startsWith(':')) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : String(value);\n }\n }\n });\n\n req.on('data', (chunk: Buffer) => {\n responseData = Buffer.concat([responseData, chunk]);\n });\n\n req.on('end', () => {\n // Remove the request-specific error handler\n session.off('error', sessionErrorHandler);\n\n const contentType = responseHeaders['content-type'] || '';\n let data: unknown;\n\n if (contentType.includes('application/json')) {\n data = JSON.parse(responseData.toString());\n } else if (contentType.includes('text/')) {\n data = responseData.toString();\n } else {\n // Use slice() to create a copy of only the used portion of the ArrayBuffer\n // This prevents exposure of garbage data from previous allocations\n data = responseData.buffer.slice(responseData.byteOffset, responseData.byteOffset + responseData.byteLength);\n }\n\n resolve({\n status,\n statusText,\n headers: responseHeaders,\n data,\n ok: status >= 200 && status < 300,\n });\n });\n\n req.on('error', (err) => {\n // Remove the request-specific error handler\n session.off('error', sessionErrorHandler);\n reject(err);\n });\n });\n }\n\n disconnect() {\n // Close all cached sessions\n for (const entry of this.sessionCache.values()) {\n if (!entry.destroyed) {\n entry.session.close();\n entry.destroyed = true;\n }\n }\n this.sessionCache.clear();\n }\n}\n","/**\n * @unireq/http2 - HTTP/2 transport using Node.js http2 module with ALPN\n *\n * ## Why a dedicated HTTP/2 transport?\n *\n * Node.js built-in `fetch` (undici) defaults to HTTP/1.1, even when the server supports HTTP/2.\n * While undici can negotiate HTTP/2 via ALPN, it requires explicit configuration and doesn't\n * happen automatically with the global `fetch` API.\n *\n * This package provides a dedicated HTTP/2 transport using `node:http2` for scenarios where:\n * - HTTP/2 is required (not optional)\n * - Server push is needed\n * - Multiplexing over a single connection is critical\n * - ALPN negotiation must be explicit\n *\n * @see https://undici.nodejs.org\n * @see https://nodejs.org/api/http2.html\n */\n\nimport type { Connector, RequestContext, Response, TransportWithCapabilities } from '@unireq/core';\nimport { Http2Connector } from './connectors/native.js';\n\nfunction createDefaultHttp2Connector(): Connector {\n return new Http2Connector();\n}\n\n/**\n * Creates an HTTP/2 transport using node:http2 with ALPN\n *\n * @param uri - Optional base URI for the transport\n * @param connector - Optional connector instance (defaults to Http2Connector)\n * @returns Transport with capabilities\n *\n * @example\n * ```ts\n * const h2api = client(http2('https://api.example.com'), json());\n * ```\n */\nexport function http2(uri?: string, connector?: Connector): TransportWithCapabilities {\n let client: unknown;\n const actualConnector = connector ?? createDefaultHttp2Connector();\n\n const transport = async (ctx: RequestContext): Promise<Response> => {\n // Connect once if URI provided\n if (uri && !client) {\n client = await actualConnector.connect(uri);\n }\n\n // Build final URL: if uri provided and ctx.url is relative, combine them\n const finalUrl = uri && ctx.url.startsWith('/') ? `${uri}${ctx.url}` : ctx.url;\n\n return actualConnector.request(client, { ...ctx, url: finalUrl });\n };\n\n return {\n transport,\n capabilities: {\n streams: true,\n http2: true,\n serverPush: true,\n },\n };\n}\n\nexport type { Http2ConnectorOptions } from './connectors/native.js';\nexport { Http2Connector } from './connectors/native.js';\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unireq/http2",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "HTTP/2 transport for unireq using Node.js http2 module",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"author": "Olivier Orabona",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@unireq/core": "0.0.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.0.3",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"tsup": "^8.5.1"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/oorabona/unireq",
|
|
33
|
+
"directory": "packages/http2"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/oorabona/unireq/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/oorabona/unireq/tree/main/packages/http2",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"type-check": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
44
|
+
}
|
|
45
|
+
}
|