178 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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<
 | |
|   types.ICoreRequestOptions,
 | |
|   CoreResponse
 | |
| > {
 | |
|   private timeoutId: ReturnType<typeof setTimeout> | null = null;
 | |
|   private abortController: AbortController | null = null;
 | |
| 
 | |
|   constructor(url: string, options: types.ICoreRequestOptions = {}) {
 | |
|     super(url, options);
 | |
| 
 | |
|     // Check for unsupported Node.js-specific options
 | |
|     if (options.agent || options.socketPath) {
 | |
|       throw new Error(
 | |
|         'Node.js specific options (agent, socketPath) are not supported in browser/fetch implementation',
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 Uint8Array ||
 | |
|         this.options.requestBody instanceof FormData ||
 | |
|         this.options.requestBody instanceof URLSearchParams ||
 | |
|         this.options.requestBody instanceof ReadableStream ||
 | |
|         // Check for Buffer (Node.js polyfills in browser may provide this)
 | |
|         (typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
 | |
|       ) {
 | |
|         fetchOptions.body = this.options.requestBody;
 | |
|         
 | |
|         // If streaming, we need to set duplex mode
 | |
|         if (this.options.requestBody instanceof ReadableStream) {
 | |
|           (fetchOptions as any).duplex = 'half';
 | |
|         }
 | |
|       } 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<string, string>;
 | |
|           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;
 | |
|       this.abortController = new AbortController();
 | |
|       this.timeoutId = setTimeout(() => {
 | |
|         if (this.abortController) {
 | |
|           this.abortController.abort();
 | |
|         }
 | |
|       }, timeout);
 | |
|       fetchOptions.signal = this.abortController.signal;
 | |
|     }
 | |
| 
 | |
|     return fetchOptions;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Fire the request and return a CoreResponse
 | |
|    */
 | |
|   async fire(): Promise<CoreResponse> {
 | |
|     const response = await this.fireCore();
 | |
|     return new CoreResponse(response);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Fire the request and return the raw Response
 | |
|    */
 | |
|   async fireCore(): Promise<Response> {
 | |
|     const url = this.buildUrl();
 | |
|     const options = this.buildFetchOptions();
 | |
| 
 | |
|     try {
 | |
|       const response = await fetch(url, options);
 | |
|       // Clear timeout on successful response
 | |
|       this.clearTimeout();
 | |
|       return response;
 | |
|     } catch (error) {
 | |
|       // Clear timeout on error
 | |
|       this.clearTimeout();
 | |
|       if (error.name === 'AbortError') {
 | |
|         throw new Error('Request timed out');
 | |
|       }
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clear the timeout and abort controller
 | |
|    */
 | |
|   private clearTimeout(): void {
 | |
|     if (this.timeoutId) {
 | |
|       clearTimeout(this.timeoutId);
 | |
|       this.timeoutId = null;
 | |
|     }
 | |
|     if (this.abortController) {
 | |
|       this.abortController = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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;
 |