From b8d707b36391aa59387e7bf47098d3620f84f9df Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 28 Jul 2025 16:51:30 +0000 Subject: [PATCH] update --- package.json | 3 +- ts/core_fetch/index.ts | 4 ++ ts/core_fetch/request.ts | 126 ++++++++++++++++++++++++++++++++++++++ ts/core_fetch/response.ts | 78 +++++++++++++++++++++++ ts/core_fetch/types.ts | 27 ++++++++ ts/core_node/types.ts | 3 +- 6 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 ts/core_fetch/index.ts create mode 100644 ts/core_fetch/request.ts create mode 100644 ts/core_fetch/response.ts create mode 100644 ts/core_fetch/types.ts diff --git a/package.json b/package.json index 063c3ee..1a84549 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.", "exports": { ".": "./dist_ts_web/index.js", - "./legacy": "./dist_ts/legacy/index.js" + "./legacy": "./dist_ts/legacy/index.js", + "./fetch": "./dist_ts/core_fetch/index.js" }, "type": "module", "scripts": { diff --git a/ts/core_fetch/index.ts b/ts/core_fetch/index.ts new file mode 100644 index 0000000..79e3339 --- /dev/null +++ b/ts/core_fetch/index.ts @@ -0,0 +1,4 @@ +// Core fetch exports - native fetch implementation +export * from './types.js'; +export * from './request.js'; +export * from './response.js'; \ No newline at end of file diff --git a/ts/core_fetch/request.ts b/ts/core_fetch/request.ts new file mode 100644 index 0000000..9f34e43 --- /dev/null +++ b/ts/core_fetch/request.ts @@ -0,0 +1,126 @@ +import * as types from './types.js'; +import { CoreResponse } from './response.js'; +import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js'; + +/** + * Fetch-based implementation of Core Request class + */ +export class CoreRequest extends AbstractCoreRequest { + constructor(url: string, options: types.ICoreRequestOptions = {}) { + super(url, options); + } + + /** + * Build the full URL with query parameters + */ + private buildUrl(): string { + if (!this.options.queryParams || Object.keys(this.options.queryParams).length === 0) { + return this.url; + } + + const url = new URL(this.url); + Object.entries(this.options.queryParams).forEach(([key, value]) => { + url.searchParams.append(key, value); + }); + return url.toString(); + } + + /** + * Convert our options to fetch RequestInit + */ + private buildFetchOptions(): RequestInit { + const fetchOptions: RequestInit = { + method: this.options.method, + headers: this.options.headers, + credentials: this.options.credentials, + mode: this.options.mode, + cache: this.options.cache, + redirect: this.options.redirect, + referrer: this.options.referrer, + referrerPolicy: this.options.referrerPolicy, + integrity: this.options.integrity, + keepalive: this.options.keepAlive, + signal: this.options.signal, + }; + + // Handle request body + if (this.options.requestBody !== undefined) { + if (typeof this.options.requestBody === 'string' || + this.options.requestBody instanceof ArrayBuffer || + this.options.requestBody instanceof FormData || + this.options.requestBody instanceof URLSearchParams || + this.options.requestBody instanceof ReadableStream) { + fetchOptions.body = this.options.requestBody; + } else { + // Convert objects to JSON + fetchOptions.body = JSON.stringify(this.options.requestBody); + // Set content-type if not already set + if (!fetchOptions.headers) { + fetchOptions.headers = { 'Content-Type': 'application/json' }; + } else if (fetchOptions.headers instanceof Headers) { + if (!fetchOptions.headers.has('Content-Type')) { + fetchOptions.headers.set('Content-Type', 'application/json'); + } + } else if (typeof fetchOptions.headers === 'object' && !Array.isArray(fetchOptions.headers)) { + const headersObj = fetchOptions.headers as Record; + if (!headersObj['Content-Type']) { + headersObj['Content-Type'] = 'application/json'; + } + } + } + } + + // Handle timeout + if (this.options.timeout || this.options.hardDataCuttingTimeout) { + const timeout = this.options.hardDataCuttingTimeout || this.options.timeout; + const controller = new AbortController(); + setTimeout(() => controller.abort(), timeout); + fetchOptions.signal = controller.signal; + } + + return fetchOptions; + } + + /** + * Fire the request and return a CoreResponse + */ + async fire(): Promise { + const response = await this.fireCore(); + return new CoreResponse(response); + } + + /** + * Fire the request and return the raw Response + */ + async fireCore(): Promise { + const url = this.buildUrl(); + const options = this.buildFetchOptions(); + + try { + const response = await fetch(url, options); + return response; + } catch (error) { + if (error.name === 'AbortError') { + throw new Error('Request timed out'); + } + throw error; + } + } + + /** + * Static factory method to create and fire a request + */ + static async create( + url: string, + options: types.ICoreRequestOptions = {} + ): Promise { + const request = new CoreRequest(url, options); + return request.fire(); + } +} + +/** + * Convenience exports for backward compatibility + */ +export const isUnixSocket = CoreRequest.isUnixSocket; +export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl; \ No newline at end of file diff --git a/ts/core_fetch/response.ts b/ts/core_fetch/response.ts new file mode 100644 index 0000000..ab5ea9c --- /dev/null +++ b/ts/core_fetch/response.ts @@ -0,0 +1,78 @@ +import * as types from './types.js'; +import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js'; + +/** + * Fetch-based implementation of Core Response class + */ +export class CoreResponse extends AbstractCoreResponse implements types.ICoreResponse { + private response: Response; + private responseClone: Response; + + // Public properties + public readonly ok: boolean; + public readonly status: number; + public readonly statusText: string; + public readonly headers: types.AbstractHeaders; + public readonly url: string; + + constructor(response: Response) { + super(); + // Clone the response so we can read the body multiple times if needed + this.response = response; + this.responseClone = response.clone(); + + this.ok = response.ok; + this.status = response.status; + this.statusText = response.statusText; + this.url = response.url; + + // Convert Headers to plain object + this.headers = {}; + response.headers.forEach((value, key) => { + this.headers[key] = value; + }); + } + + /** + * Parse response as JSON + */ + async json(): Promise { + this.ensureNotConsumed(); + try { + return await this.response.json(); + } catch (error) { + throw new Error(`Failed to parse JSON: ${error.message}`); + } + } + + /** + * Get response as text + */ + async text(): Promise { + this.ensureNotConsumed(); + return await this.response.text(); + } + + /** + * Get response as ArrayBuffer + */ + async arrayBuffer(): Promise { + this.ensureNotConsumed(); + return await this.response.arrayBuffer(); + } + + /** + * Get response as a readable stream (Web Streams API) + */ + stream(): ReadableStream | null { + this.ensureNotConsumed(); + return this.response.body; + } + + /** + * Get the raw Response object + */ + raw(): Response { + return this.responseClone; + } +} \ No newline at end of file diff --git a/ts/core_fetch/types.ts b/ts/core_fetch/types.ts new file mode 100644 index 0000000..f9e0203 --- /dev/null +++ b/ts/core_fetch/types.ts @@ -0,0 +1,27 @@ +import * as baseTypes from '../core_base/types.js'; + +// Re-export base types +export * from '../core_base/types.js'; + +/** + * Core request options for fetch-based implementation + * Extends RequestInit from the Fetch API + */ +export interface ICoreRequestOptions extends RequestInit { + // Override method to be more specific + method?: baseTypes.THttpMethod; + // Additional options not in RequestInit + requestBody?: any; + queryParams?: { [key: string]: string }; + timeout?: number; + hardDataCuttingTimeout?: number; + // keepAlive maps to keepalive in RequestInit + keepAlive?: boolean; +} + +/** + * Core response object for fetch implementation + */ +export interface ICoreResponse extends baseTypes.IAbstractResponse { + // Fetch-specific properties (all from base interface) +} \ No newline at end of file diff --git a/ts/core_node/types.ts b/ts/core_node/types.ts index c087eb3..cc88a69 100644 --- a/ts/core_node/types.ts +++ b/ts/core_node/types.ts @@ -6,8 +6,9 @@ export * from '../core_base/types.js'; /** * Core request options extending Node.js RequestOptions + * Node.js RequestOptions already includes method and headers */ -export interface ICoreRequestOptions extends plugins.https.RequestOptions, Omit { +export interface ICoreRequestOptions extends plugins.https.RequestOptions { keepAlive?: boolean; requestBody?: any; queryParams?: { [key: string]: string };