update
This commit is contained in:
4
ts/core_node/index.ts
Normal file
4
ts/core_node/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Core exports
|
||||
export * from './types.js';
|
||||
export * from './response.js';
|
||||
export { CoreRequest, isUnixSocket, parseUnixSocketUrl } from './request.js';
|
20
ts/core_node/plugins.ts
Normal file
20
ts/core_node/plugins.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// node native scope
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
|
||||
export { http, https, fs, path };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smarturl from '@push.rocks/smarturl';
|
||||
|
||||
export { smartpromise, smarturl };
|
||||
|
||||
// third party scope
|
||||
import { HttpAgent, HttpsAgent } from 'agentkeepalive';
|
||||
const agentkeepalive = { HttpAgent, HttpsAgent };
|
||||
import formData from 'form-data';
|
||||
|
||||
export { agentkeepalive, formData };
|
184
ts/core_node/request.ts
Normal file
184
ts/core_node/request.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
|
||||
// Keep-alive agents for connection pooling
|
||||
const httpAgent = new plugins.agentkeepalive.HttpAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpAgentKeepAliveFalse = new plugins.agentkeepalive.HttpAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Core Request class that handles all HTTP/HTTPS requests
|
||||
*/
|
||||
export class CoreRequest {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const incomingMessage = await this.fireCore();
|
||||
return new CoreResponse(incomingMessage, this.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw IncomingMessage
|
||||
*/
|
||||
async fireCore(): Promise<plugins.http.IncomingMessage> {
|
||||
const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();
|
||||
|
||||
// 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 (CoreRequest.isUnixSocket(this.url)) {
|
||||
const { socketPath, path } = CoreRequest.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 fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.ICoreRequestOptions = {}
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience exports for backward compatibility
|
||||
*/
|
||||
export const isUnixSocket = CoreRequest.isUnixSocket;
|
||||
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;
|
110
ts/core_node/response.ts
Normal file
110
ts/core_node/response.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
|
||||
/**
|
||||
* Core Response class that provides a fetch-like API
|
||||
*/
|
||||
export class CoreResponse<T = any> implements types.ICoreResponse<T> {
|
||||
private incomingMessage: plugins.http.IncomingMessage;
|
||||
private bodyBufferPromise: Promise<Buffer> | null = null;
|
||||
private consumed = false;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly headers: plugins.http.IncomingHttpHeaders;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(incomingMessage: plugins.http.IncomingMessage, url: string) {
|
||||
this.incomingMessage = incomingMessage;
|
||||
this.url = url;
|
||||
this.status = incomingMessage.statusCode || 0;
|
||||
this.statusText = incomingMessage.statusMessage || '';
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
this.headers = incomingMessage.headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the body can only be consumed once
|
||||
*/
|
||||
private ensureNotConsumed(): void {
|
||||
if (this.consumed) {
|
||||
throw new Error('Body has already been consumed');
|
||||
}
|
||||
this.consumed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the body as a buffer
|
||||
*/
|
||||
private async collectBody(): Promise<Buffer> {
|
||||
this.ensureNotConsumed();
|
||||
|
||||
if (this.bodyBufferPromise) {
|
||||
return this.bodyBufferPromise;
|
||||
}
|
||||
|
||||
this.bodyBufferPromise = new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
this.incomingMessage.on('data', (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
this.incomingMessage.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
|
||||
this.incomingMessage.on('error', reject);
|
||||
});
|
||||
|
||||
return this.bodyBufferPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
async json(): Promise<T> {
|
||||
const buffer = await this.collectBody();
|
||||
const text = buffer.toString('utf-8');
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
const buffer = await this.collectBody();
|
||||
return buffer.toString('utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
const buffer = await this.collectBody();
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a readable stream
|
||||
*/
|
||||
stream(): NodeJS.ReadableStream {
|
||||
this.ensureNotConsumed();
|
||||
return this.incomingMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw IncomingMessage (for legacy compatibility)
|
||||
*/
|
||||
raw(): plugins.http.IncomingMessage {
|
||||
return this.incomingMessage;
|
||||
}
|
||||
|
||||
}
|
67
ts/core_node/types.ts
Normal file
67
ts/core_node/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
/**
|
||||
* Core request options extending Node.js RequestOptions
|
||||
*/
|
||||
export interface ICoreRequestOptions extends plugins.https.RequestOptions {
|
||||
keepAlive?: boolean;
|
||||
requestBody?: any;
|
||||
queryParams?: { [key: string]: string };
|
||||
hardDataCuttingTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Methods supported
|
||||
*/
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/**
|
||||
* Response types supported
|
||||
*/
|
||||
export type ResponseType = 'json' | 'text' | 'binary' | 'stream';
|
||||
|
||||
/**
|
||||
* Extended IncomingMessage with body property (legacy compatibility)
|
||||
*/
|
||||
export interface IExtendedIncomingMessage<T = any> extends plugins.http.IncomingMessage {
|
||||
body: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form field data for multipart/form-data requests
|
||||
*/
|
||||
export interface IFormField {
|
||||
name: string;
|
||||
value: string | Buffer;
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encoded form field
|
||||
*/
|
||||
export interface IUrlEncodedField {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core response object that provides fetch-like API
|
||||
*/
|
||||
export interface ICoreResponse<T = any> {
|
||||
// Properties
|
||||
ok: boolean;
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: plugins.http.IncomingHttpHeaders;
|
||||
url: string;
|
||||
|
||||
// Methods
|
||||
json(): Promise<T>;
|
||||
text(): Promise<string>;
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
stream(): NodeJS.ReadableStream;
|
||||
|
||||
// Legacy compatibility
|
||||
raw(): plugins.http.IncomingMessage;
|
||||
}
|
Reference in New Issue
Block a user