BREAKING CHANGE(core): major architectural refactoring with fetch-like API
This commit is contained in:
159
ts/core/request.ts
Normal file
159
ts/core/request.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { SmartResponse } from './response.js';
|
||||
|
||||
// Keep-alive agents for connection pooling
|
||||
const httpAgent = new plugins.agentkeepalive({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpAgentKeepAliveFalse = new plugins.agentkeepalive({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests if a URL is a unix socket
|
||||
*/
|
||||
export const 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],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Core request function that handles all HTTP/HTTPS requests
|
||||
*/
|
||||
export async function coreRequest(
|
||||
urlArg: string,
|
||||
optionsArg: types.ICoreRequestOptions = {},
|
||||
requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null
|
||||
): 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 || {},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern request function that returns a SmartResponse
|
||||
*/
|
||||
export async function request(
|
||||
urlArg: string,
|
||||
optionsArg: types.ICoreRequestOptions = {}
|
||||
): Promise<SmartResponse> {
|
||||
const response = await coreRequest(urlArg, optionsArg);
|
||||
return new SmartResponse(response, urlArg);
|
||||
}
|
Reference in New Issue
Block a user