From 31c25c8333b1f059ac5a90059bff6058723900f4 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 28 Jul 2025 14:30:27 +0000 Subject: [PATCH] update --- package.json | 1 + pnpm-lock.yaml | 29 ++--- ts/core/index.ts | 2 +- ts/core/plugins.ts | 3 +- ts/core/request.ts | 272 ++++++++++++++++++++++++++------------------- 5 files changed, 180 insertions(+), 127 deletions(-) diff --git a/package.json b/package.json index 68a9680..063c3ee 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "homepage": "https://code.foss.global/push.rocks/smartrequest", "dependencies": { + "@push.rocks/smartenv": "^5.0.13", "@push.rocks/smartpromise": "^4.0.4", "@push.rocks/smarturl": "^3.1.0", "agentkeepalive": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 247aca5..af9c372 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@push.rocks/smartenv': + specifier: ^5.0.13 + version: 5.0.13 '@push.rocks/smartpromise': specifier: ^4.0.4 version: 4.2.3 @@ -749,8 +752,8 @@ packages: '@push.rocks/smartdelay@3.0.5': resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} - '@push.rocks/smartenv@5.0.12': - resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==} + '@push.rocks/smartenv@5.0.13': + resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} '@push.rocks/smartexit@1.0.23': resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==} @@ -4268,7 +4271,7 @@ snapshots: '@push.rocks/lik': 6.1.0 '@push.rocks/smartchok': 1.0.34 '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartfeed': 1.0.11 '@push.rocks/smartfile': 11.2.0 '@push.rocks/smartjson': 5.0.20 @@ -5373,7 +5376,7 @@ snapshots: '@push.rocks/lik': 6.1.0 '@push.rocks/smartbucket': 3.3.7 '@push.rocks/smartcache': 1.0.16 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartexit': 1.0.23 '@push.rocks/smartfile': 11.2.0 '@push.rocks/smartjson': 5.0.20 @@ -5522,7 +5525,7 @@ snapshots: dependencies: '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartenv@5.0.12': + '@push.rocks/smartenv@5.0.13': dependencies: '@push.rocks/smartpromise': 4.2.3 @@ -5599,7 +5602,7 @@ snapshots: '@push.rocks/smartjson@5.0.20': dependencies: - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartstring': 4.0.15 fast-json-stable-stringify: 2.1.0 lodash.clonedeep: 4.5.0 @@ -5815,7 +5818,7 @@ snapshots: '@push.rocks/isounique': 1.0.5 '@push.rocks/lik': 6.1.0 '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartjson': 5.0.20 '@push.rocks/smartlog': 3.0.7 '@push.rocks/smartpromise': 4.2.3 @@ -5862,14 +5865,14 @@ snapshots: '@push.rocks/smartstream@3.2.5': dependencies: '@push.rocks/lik': 6.1.0 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.7 '@push.rocks/smartstring@4.0.15': dependencies: '@push.rocks/isounique': 1.0.5 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@types/randomatic': 3.1.5 crypto-random-string: 5.0.0 js-base64: 3.7.7 @@ -5918,7 +5921,7 @@ snapshots: '@push.rocks/qenv': 6.1.0 '@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartexpect': 1.6.1 '@push.rocks/smartfile': 11.2.0 '@push.rocks/smartjson': 5.0.20 @@ -5956,7 +5959,7 @@ snapshots: '@push.rocks/webrequest@3.0.37': dependencies: '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartjson': 5.0.20 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/webstore': 2.0.20 @@ -5971,7 +5974,7 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/lik': 6.1.0 - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@push.rocks/smartjson': 5.0.20 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.7 @@ -5980,7 +5983,7 @@ snapshots: '@push.rocks/webstream@1.0.10': dependencies: - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@pushrocks/isounique@1.0.5': {} diff --git a/ts/core/index.ts b/ts/core/index.ts index d4369e4..6b0f9cd 100644 --- a/ts/core/index.ts +++ b/ts/core/index.ts @@ -1,4 +1,4 @@ // Core exports export * from './types.js'; export * from './response.js'; -export { request, coreRequest, isUnixSocket, parseUnixSocketUrl } from './request.js'; \ No newline at end of file +export { SmartRequest, request, coreRequest, isUnixSocket, parseUnixSocketUrl } from './request.js'; \ No newline at end of file diff --git a/ts/core/plugins.ts b/ts/core/plugins.ts index 2b35a5d..26bb6b0 100644 --- a/ts/core/plugins.ts +++ b/ts/core/plugins.ts @@ -13,7 +13,8 @@ import * as smarturl from '@push.rocks/smarturl'; export { smartpromise, smarturl }; // third party scope -import agentkeepalive from 'agentkeepalive'; +import { HttpAgent, HttpsAgent } from 'agentkeepalive'; +const agentkeepalive = { HttpAgent, HttpsAgent }; import formData from 'form-data'; export { agentkeepalive, formData }; \ No newline at end of file diff --git a/ts/core/request.ts b/ts/core/request.ts index b3723e3..dd6b2e9 100644 --- a/ts/core/request.ts +++ b/ts/core/request.ts @@ -3,14 +3,14 @@ import * as types from './types.js'; import { SmartResponse } from './response.js'; // Keep-alive agents for connection pooling -const httpAgent = new plugins.agentkeepalive({ +const httpAgent = new plugins.agentkeepalive.HttpAgent({ keepAlive: true, maxFreeSockets: 10, maxSockets: 100, maxTotalSockets: 1000, }); -const httpAgentKeepAliveFalse = new plugins.agentkeepalive({ +const httpAgentKeepAliveFalse = new plugins.agentkeepalive.HttpAgent({ keepAlive: false, }); @@ -26,125 +26,168 @@ const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({ }); /** - * Tests if a URL is a unix socket + * Modern Request class that handles all HTTP/HTTPS requests */ -export const isUnixSocket = (url: string): boolean => { - const unixRegex = /^(http:\/\/|https:\/\/|)unix:/; - return unixRegex.test(url); -}; +export class SmartRequest { + /** + * Tests if a URL is a unix socket + */ + static isUnixSocket(url: string): boolean { + const unixRegex = /^(http:\/\/|https:\/\/|)unix:/; + return unixRegex.test(url); + } -/** - * Parses socket path and route from unix socket URL - */ -export const parseUnixSocketUrl = (url: string): { socketPath: string; path: string } => { - const parseRegex = /(.*):(.*)/; - const result = parseRegex.exec(url); - return { - socketPath: result[1], - path: result[2], - }; -}; + /** + * Parses socket path and route from unix socket URL + */ + static parseUnixSocketUrl(url: string): { socketPath: string; path: string } { + const parseRegex = /(.*):(.*)/; + const result = parseRegex.exec(url); + return { + socketPath: result[1], + path: result[2], + }; + } + private url: string; + private options: types.ICoreRequestOptions; + private requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null; + constructor( + url: string, + options: types.ICoreRequestOptions = {}, + requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null + ) { + this.url = url; + this.options = options; + this.requestDataFunc = requestDataFunc; + } + + /** + * Execute the request and return a SmartResponse + */ + async execute(): Promise { + const incomingMessage = await this.executeCore(); + return new SmartResponse(incomingMessage, this.url); + } + + /** + * Execute the request and return the raw IncomingMessage + */ + async executeCore(): Promise { + const done = plugins.smartpromise.defer(); + + // Parse URL + const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.url, { + searchParams: this.options.queryParams || {}, + }); + + this.options.hostname = parsedUrl.hostname; + if (parsedUrl.port) { + this.options.port = parseInt(parsedUrl.port, 10); + } + this.options.path = parsedUrl.path; + + // Handle unix socket URLs + if (SmartRequest.isUnixSocket(this.url)) { + const { socketPath, path } = SmartRequest.parseUnixSocketUrl(this.options.path); + this.options.socketPath = socketPath; + this.options.path = path; + } + + // Determine agent based on protocol and keep-alive setting + if (!this.options.agent) { + // Only use keep-alive agents if explicitly requested + if (this.options.keepAlive === true) { + this.options.agent = parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent; + } else if (this.options.keepAlive === false) { + this.options.agent = parsedUrl.protocol === 'https:' ? httpsAgentKeepAliveFalse : httpAgentKeepAliveFalse; + } + // If keepAlive is undefined, don't set any agent (more fetch-like behavior) + } + + // Determine request module + const requestModule = parsedUrl.protocol === 'https:' ? plugins.https : plugins.http; + + if (!requestModule) { + throw new Error(`The request to ${this.url} is missing a viable protocol. Must be http or https`); + } + + // Perform the request + const request = requestModule.request(this.options, async (response) => { + // Handle hard timeout + if (this.options.hardDataCuttingTimeout) { + setTimeout(() => { + response.destroy(); + done.reject(new Error('Request timed out')); + }, this.options.hardDataCuttingTimeout); + } + + // Always return the raw stream + done.resolve(response); + }); + + // Write request body + if (this.options.requestBody) { + if (this.options.requestBody instanceof plugins.formData) { + this.options.requestBody.pipe(request).on('finish', () => { + request.end(); + }); + } else { + // Write body as-is - caller is responsible for serialization + const bodyData = typeof this.options.requestBody === 'string' + ? this.options.requestBody + : this.options.requestBody instanceof Buffer + ? this.options.requestBody + : JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility + request.write(bodyData); + request.end(); + } + } else if (this.requestDataFunc) { + this.requestDataFunc(request); + } else { + request.end(); + } + + // Handle request errors + request.on('error', (e) => { + console.error(e); + request.destroy(); + done.reject(e); + }); + + // Get response and handle response errors + const response = await done.promise; + response.on('error', (err) => { + console.error(err); + response.destroy(); + }); + + return response; + } + + /** + * Static factory method to create and execute a request + */ + static async create( + url: string, + options: types.ICoreRequestOptions = {} + ): Promise { + const request = new SmartRequest(url, options); + return request.execute(); + } +} /** * Core request function that handles all HTTP/HTTPS requests + * @deprecated Use SmartRequest class instead */ export async function coreRequest( urlArg: string, optionsArg: types.ICoreRequestOptions = {}, requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null ): Promise { - const done = plugins.smartpromise.defer(); - - // No defaults - let users explicitly set options to match fetch behavior - - // Parse URL - const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg, { - searchParams: optionsArg.queryParams || {}, - }); - - optionsArg.hostname = parsedUrl.hostname; - if (parsedUrl.port) { - optionsArg.port = parseInt(parsedUrl.port, 10); - } - optionsArg.path = parsedUrl.path; - - // Handle unix socket URLs - if (isUnixSocket(urlArg)) { - const { socketPath, path } = parseUnixSocketUrl(optionsArg.path); - optionsArg.socketPath = socketPath; - optionsArg.path = path; - } - - // Determine agent based on protocol and keep-alive setting - if (!optionsArg.agent) { - // Only use keep-alive agents if explicitly requested - if (optionsArg.keepAlive === true) { - optionsArg.agent = parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent; - } else if (optionsArg.keepAlive === false) { - optionsArg.agent = parsedUrl.protocol === 'https:' ? httpsAgentKeepAliveFalse : httpAgentKeepAliveFalse; - } - // If keepAlive is undefined, don't set any agent (more fetch-like behavior) - } - - // Determine request module - const requestModule = parsedUrl.protocol === 'https:' ? plugins.https : plugins.http; - - if (!requestModule) { - throw new Error(`The request to ${urlArg} is missing a viable protocol. Must be http or https`); - } - - // Perform the request - const request = requestModule.request(optionsArg, async (response) => { - // Handle hard timeout - if (optionsArg.hardDataCuttingTimeout) { - setTimeout(() => { - response.destroy(); - done.reject(new Error('Request timed out')); - }, optionsArg.hardDataCuttingTimeout); - } - - // Always return the raw stream - done.resolve(response); - }); - - // Write request body - if (optionsArg.requestBody) { - if (optionsArg.requestBody instanceof plugins.formData) { - optionsArg.requestBody.pipe(request).on('finish', () => { - request.end(); - }); - } else { - // Write body as-is - caller is responsible for serialization - const bodyData = typeof optionsArg.requestBody === 'string' - ? optionsArg.requestBody - : optionsArg.requestBody instanceof Buffer - ? optionsArg.requestBody - : JSON.stringify(optionsArg.requestBody); // Still stringify for backward compatibility - request.write(bodyData); - request.end(); - } - } else if (requestDataFunc) { - requestDataFunc(request); - } else { - request.end(); - } - - // Handle request errors - request.on('error', (e) => { - console.error(e); - request.destroy(); - done.reject(e); - }); - - // Get response and handle response errors - const response = await done.promise; - response.on('error', (err) => { - console.error(err); - response.destroy(); - }); - - return response; + const request = new SmartRequest(urlArg, optionsArg, requestDataFunc); + return request.executeCore(); } /** @@ -154,6 +197,11 @@ export async function request( urlArg: string, optionsArg: types.ICoreRequestOptions = {} ): Promise { - const response = await coreRequest(urlArg, optionsArg); - return new SmartResponse(response, urlArg); -} \ No newline at end of file + return SmartRequest.create(urlArg, optionsArg); +} + +/** + * Convenience exports for backward compatibility + */ +export const isUnixSocket = SmartRequest.isUnixSocket; +export const parseUnixSocketUrl = SmartRequest.parseUnixSocketUrl; \ No newline at end of file