This commit is contained in:
2025-07-28 14:30:27 +00:00
parent 01bbfa4a06
commit 31c25c8333
5 changed files with 180 additions and 127 deletions

View File

@@ -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",

29
pnpm-lock.yaml generated
View File

@@ -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': {}

View File

@@ -1,4 +1,4 @@
// Core exports
export * from './types.js';
export * from './response.js';
export { request, coreRequest, isUnixSocket, parseUnixSocketUrl } from './request.js';
export { SmartRequest, request, coreRequest, isUnixSocket, parseUnixSocketUrl } from './request.js';

View File

@@ -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 };

View File

@@ -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,
});
@@ -25,64 +25,82 @@ const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
keepAlive: false,
});
/**
* Modern Request class that handles all HTTP/HTTPS requests
*/
export class SmartRequest {
/**
* Tests if a URL is a unix socket
*/
export const isUnixSocket = (url: string): boolean => {
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 } => {
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;
}
/**
* Core request function that handles all HTTP/HTTPS requests
* Execute the request and return a SmartResponse
*/
export async function coreRequest(
urlArg: string,
optionsArg: types.ICoreRequestOptions = {},
requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null
): Promise<plugins.http.IncomingMessage> {
async execute(): Promise<SmartResponse> {
const incomingMessage = await this.executeCore();
return new SmartResponse(incomingMessage, this.url);
}
/**
* Execute the request and return the raw IncomingMessage
*/
async executeCore(): Promise<plugins.http.IncomingMessage> {
const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();
// No defaults - let users explicitly set options to match fetch behavior
// Parse URL
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg, {
searchParams: optionsArg.queryParams || {},
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.url, {
searchParams: this.options.queryParams || {},
});
optionsArg.hostname = parsedUrl.hostname;
this.options.hostname = parsedUrl.hostname;
if (parsedUrl.port) {
optionsArg.port = parseInt(parsedUrl.port, 10);
this.options.port = parseInt(parsedUrl.port, 10);
}
optionsArg.path = parsedUrl.path;
this.options.path = parsedUrl.path;
// Handle unix socket URLs
if (isUnixSocket(urlArg)) {
const { socketPath, path } = parseUnixSocketUrl(optionsArg.path);
optionsArg.socketPath = socketPath;
optionsArg.path = path;
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 (!optionsArg.agent) {
if (!this.options.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 (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)
}
@@ -91,17 +109,17 @@ export async function coreRequest(
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`);
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(optionsArg, async (response) => {
const request = requestModule.request(this.options, async (response) => {
// Handle hard timeout
if (optionsArg.hardDataCuttingTimeout) {
if (this.options.hardDataCuttingTimeout) {
setTimeout(() => {
response.destroy();
done.reject(new Error('Request timed out'));
}, optionsArg.hardDataCuttingTimeout);
}, this.options.hardDataCuttingTimeout);
}
// Always return the raw stream
@@ -109,23 +127,23 @@ export async function coreRequest(
});
// Write request body
if (optionsArg.requestBody) {
if (optionsArg.requestBody instanceof plugins.formData) {
optionsArg.requestBody.pipe(request).on('finish', () => {
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 optionsArg.requestBody === 'string'
? optionsArg.requestBody
: optionsArg.requestBody instanceof Buffer
? optionsArg.requestBody
: JSON.stringify(optionsArg.requestBody); // Still stringify for backward compatibility
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 (requestDataFunc) {
requestDataFunc(request);
} else if (this.requestDataFunc) {
this.requestDataFunc(request);
} else {
request.end();
}
@@ -147,6 +165,31 @@ export async function coreRequest(
return response;
}
/**
* Static factory method to create and execute a request
*/
static async create(
url: string,
options: types.ICoreRequestOptions = {}
): Promise<SmartResponse> {
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<plugins.http.IncomingMessage> {
const request = new SmartRequest(urlArg, optionsArg, requestDataFunc);
return request.executeCore();
}
/**
* Modern request function that returns a SmartResponse
*/
@@ -154,6 +197,11 @@ export async function request(
urlArg: string,
optionsArg: types.ICoreRequestOptions = {}
): Promise<SmartResponse> {
const response = await coreRequest(urlArg, optionsArg);
return new SmartResponse(response, urlArg);
return SmartRequest.create(urlArg, optionsArg);
}
/**
* Convenience exports for backward compatibility
*/
export const isUnixSocket = SmartRequest.isUnixSocket;
export const parseUnixSocketUrl = SmartRequest.parseUnixSocketUrl;