Compare commits
	
		
			13 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| df28cd4778 | |||
| f49cbd2b6a | |||
| 984b53cba2 | |||
| 4c55243646 | |||
| 49cfcaedd1 | |||
| 3996a69f91 | |||
| 629f6dd425 | |||
| d141ceeaf7 | |||
| 7d3c94cae6 | |||
| 5bae452365 | |||
| ffabcf7bdb | |||
| 361d97f440 | |||
| 35867d9148 | 
| @@ -23,24 +23,16 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Install pnpm and npmci | ||||
|         run: | | ||||
|           pnpm install -g pnpm | ||||
|           pnpm install -g @ship.zone/npmci | ||||
|  | ||||
|       - name: Run npm prepare | ||||
|         run: npmci npm prepare | ||||
|  | ||||
|       - name: Audit production dependencies | ||||
|         run: | | ||||
|           npmci command npm config set registry https://registry.npmjs.org | ||||
|           npmci command pnpm audit --audit-level=high --prod | ||||
|           npm config set registry https://registry.npmjs.org | ||||
|           pnpm audit --audit-level=high --prod | ||||
|         continue-on-error: true | ||||
|  | ||||
|       - name: Audit development dependencies | ||||
|         run: | | ||||
|           npmci command npm config set registry https://registry.npmjs.org | ||||
|           npmci command pnpm audit --audit-level=high --dev | ||||
|           npm config set registry https://registry.npmjs.org | ||||
|           pnpm audit --audit-level=high --dev | ||||
|         continue-on-error: true | ||||
|  | ||||
|   test: | ||||
| @@ -55,12 +47,10 @@ jobs: | ||||
|  | ||||
|       - name: Test stable | ||||
|         run: | | ||||
|           npmci node install stable | ||||
|           npmci npm install | ||||
|           npmci npm test | ||||
|           pnpm install | ||||
|           pnpm test | ||||
|  | ||||
|       - name: Test build | ||||
|         run: | | ||||
|           npmci node install stable | ||||
|           npmci npm install | ||||
|           npmci npm build | ||||
|           pnpm install | ||||
|           pnpm build | ||||
|   | ||||
| @@ -23,22 +23,16 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Prepare | ||||
|         run: | | ||||
|           pnpm install -g pnpm | ||||
|           pnpm install -g @ship.zone/npmci | ||||
|           npmci npm prepare | ||||
|  | ||||
|       - name: Audit production dependencies | ||||
|         run: | | ||||
|           npmci command npm config set registry https://registry.npmjs.org | ||||
|           npmci command pnpm audit --audit-level=high --prod | ||||
|           npm config set registry https://registry.npmjs.org | ||||
|           pnpm audit --audit-level=high --prod | ||||
|         continue-on-error: true | ||||
|  | ||||
|       - name: Audit development dependencies | ||||
|         run: | | ||||
|           npmci command npm config set registry https://registry.npmjs.org | ||||
|           npmci command pnpm audit --audit-level=high --dev | ||||
|           npm config set registry https://registry.npmjs.org | ||||
|           pnpm audit --audit-level=high --dev | ||||
|         continue-on-error: true | ||||
|  | ||||
|   test: | ||||
| @@ -51,23 +45,15 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Prepare | ||||
|         run: | | ||||
|           pnpm install -g pnpm | ||||
|           pnpm install -g @ship.zone/npmci | ||||
|           npmci npm prepare | ||||
|  | ||||
|       - name: Test stable | ||||
|         run: | | ||||
|           npmci node install stable | ||||
|           npmci npm install | ||||
|           npmci npm test | ||||
|           pnpm install | ||||
|           pnpm test | ||||
|  | ||||
|       - name: Test build | ||||
|         run: | | ||||
|           npmci node install stable | ||||
|           npmci npm install | ||||
|           npmci npm build | ||||
|           pnpm install | ||||
|           pnpm build | ||||
|  | ||||
|   release: | ||||
|     needs: test | ||||
| @@ -79,16 +65,27 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Prepare | ||||
|         run: | | ||||
|           pnpm install -g pnpm | ||||
|           pnpm install -g @ship.zone/npmci | ||||
|           npmci npm prepare | ||||
|  | ||||
|       - name: Release | ||||
|         run: | | ||||
|           npmci node install stable | ||||
|           npmci npm publish | ||||
|           pnpm install | ||||
|           # Extract server host from GITHUB_SERVER_URL (remove https://) | ||||
|           GITEA_HOST="${GITHUB_SERVER_URL#https://}" | ||||
|           GITEA_REGISTRY="$GITHUB_SERVER_URL/api/packages/$GITHUB_REPOSITORY_OWNER/npm/" | ||||
|  | ||||
|           # Create .npmrc for Gitea authentication | ||||
|           echo "@${GITHUB_REPOSITORY_OWNER}:registry=${GITEA_REGISTRY}" > .npmrc | ||||
|           echo "//${GITEA_HOST}/api/packages/${GITHUB_REPOSITORY_OWNER}/npm/:_authToken=${GITEA_TOKEN}" >> .npmrc | ||||
|  | ||||
|           # Publish to Gitea | ||||
|           pnpm publish --no-git-checks | ||||
|  | ||||
|           # Conditionally publish to npmjs.org if token exists | ||||
|           if [ -n "$NPMCI_TOKEN_NPM" ]; then | ||||
|             # Update .npmrc for npmjs.org | ||||
|             echo "registry=https://registry.npmjs.org/" > .npmrc | ||||
|             echo "//registry.npmjs.org/:_authToken=${NPMCI_TOKEN_NPM}" >> .npmrc | ||||
|             pnpm publish --no-git-checks | ||||
|           fi | ||||
|  | ||||
|   metadata: | ||||
|     needs: test | ||||
| @@ -101,24 +98,14 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Prepare | ||||
|         run: | | ||||
|           pnpm install -g pnpm | ||||
|           pnpm install -g @ship.zone/npmci | ||||
|           npmci npm prepare | ||||
|  | ||||
|       - name: Code quality | ||||
|         run: | | ||||
|           npmci command npm install -g typescript | ||||
|           npmci npm install | ||||
|  | ||||
|       - name: Trigger | ||||
|         run: npmci trigger | ||||
|           npm install -g typescript | ||||
|           pnpm install | ||||
|  | ||||
|       - name: Build docs and upload artifacts | ||||
|         run: | | ||||
|           npmci node install stable | ||||
|           npmci npm install | ||||
|           pnpm install | ||||
|           pnpm install -g @git.zone/tsdoc | ||||
|           npmci command tsdoc | ||||
|           tsdoc | ||||
|         continue-on-error: true | ||||
|   | ||||
							
								
								
									
										48
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,53 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-10-26 - 4.3.6 - fix(ci) | ||||
| Use .npmrc for registry authentication in Gitea workflow and add conditional npmjs publish | ||||
|  | ||||
| - Replace npm config set commands with creating a .npmrc file for Gitea registry authentication in .gitea/workflows/default_tags.yaml | ||||
| - Add conditional update of .npmrc and publishing to npmjs.org when NPMCI_TOKEN_NPM is provided | ||||
| - Keep pnpm publish --no-git-checks; improve CI credential handling to be file-based | ||||
|  | ||||
| ## 2025-10-26 - 4.3.5 - fix(workflows) | ||||
| Remove npmci wrappers from CI workflows and use pnpm/npm CLI directly | ||||
|  | ||||
| - Removed global npmci installation and npmci npm prepare steps from Gitea workflow files | ||||
| - Use pnpm install/test/build instead of npmci-wrapped commands in test jobs | ||||
| - Replace npmci command npm config set ... with direct npm config set calls for registry/auth configuration | ||||
| - Use pnpm publish --no-git-checks for Gitea publishing and use pnpm publish for conditional npmjs publish when token present | ||||
| - Simplified dependency auditing to run pnpm audit and set registry via npm config set | ||||
| - Install tsdoc globally and run tsdoc during docs build step (replacing npmci command usage) | ||||
|  | ||||
| ## 2025-10-25 - 4.3.4 - fix(ci) | ||||
| Fix Gitea workflow publish invocation to run npm publish via npmci command | ||||
|  | ||||
| - Update .gitea/workflows/default_tags.yaml to use 'npmci command npm publish' for the publish step | ||||
| - Ensures the workflow runs npm publish through the npmci command wrapper to avoid incorrect task invocation | ||||
|  | ||||
| ## 2025-10-25 - 4.3.3 - fix(ci) | ||||
| Improve Gitea release workflow: install deps, configure Gitea npm registry, and optionally publish to npmjs.org | ||||
|  | ||||
| - Run npm install in the release job to ensure dependencies are available before publishing. | ||||
| - Configure Gitea/npm registry using GITHUB_SERVER_URL and set auth token for the @<owner> scope. | ||||
| - Publish to the Gitea npm registry during release. | ||||
| - If NPMCI_TOKEN_NPM is provided, also publish to the public npmjs.org registry (conditional publish). | ||||
| - Extract host from GITHUB_SERVER_URL to correctly set the registry auth URL. | ||||
|  | ||||
| ## 2025-10-17 - 4.3.2 - fix(core) | ||||
| Remove stray console.log from core module | ||||
|  | ||||
| - Removed a stray debug console.log(modulePath) from ts/core/index.ts that printed the module path during Node environment initialization | ||||
|  | ||||
| ## 2025-08-19 - 4.3.1 - fix(core) | ||||
| Improve streaming support and timeout handling; add browser streaming & timeout tests and README clarifications | ||||
|  | ||||
| - core_fetch: accept Uint8Array and Buffer-like bodies; set fetch duplex for ReadableStream bodies so streaming requests work in environments that require duplex | ||||
| - core_fetch: implement AbortController-based timeouts and ensure timeouts are cleared on success/error to avoid hanging timers | ||||
| - core_node: add explicit request timeout handling (request.setTimeout) and hard-data-cutting timeout tracking with proper timeoutId clear on success/error | ||||
| - client: document that raw(streamFunc) is Node-only (not supported in browsers) | ||||
| - tests: add browser streaming tests (test/test.streaming.browser.ts) that exercise buffer() and web ReadableStream via stream() | ||||
| - tests: add timeout tests (test/test.timeout.ts) to validate clearing timers, enforcing timeouts, and preventing timer leaks across multiple requests | ||||
| - docs: update README streaming section to clarify cross-platform behavior of buffer(), stream(), and raw() methods | ||||
|  | ||||
| ## 2025-08-18 - 4.3.0 - feat(client/smartrequest) | ||||
| Add streaming and raw buffer support to SmartRequest (buffer, stream, raw); update docs and tests | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@push.rocks/smartrequest", | ||||
|   "version": "4.3.0", | ||||
|   "version": "4.3.6", | ||||
|   "private": false, | ||||
|   "description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.", | ||||
|   "exports": { | ||||
|   | ||||
							
								
								
									
										13
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								readme.md
									
									
									
									
									
								
							| @@ -379,15 +379,20 @@ async function uploadBinaryData() { | ||||
| #### Streaming Methods | ||||
|  | ||||
| - **`.buffer(data, contentType?)`** - Stream a Buffer or Uint8Array directly | ||||
|   - `data`: Buffer or Uint8Array to send | ||||
|   - `data`: Buffer (Node.js) or Uint8Array (both platforms) to send | ||||
|   - `contentType`: Optional content type (defaults to 'application/octet-stream') | ||||
|   - ✅ Works in both Node.js and browsers | ||||
|  | ||||
| - **`.stream(stream, contentType?)`** - Stream from Node.js ReadableStream or web ReadableStream | ||||
|   - `stream`: The stream to pipe to the request | ||||
| - **`.stream(stream, contentType?)`** - Stream from ReadableStream | ||||
|   - `stream`: Web ReadableStream (both platforms) or Node.js stream (Node.js only) | ||||
|   - `contentType`: Optional content type | ||||
|   - ✅ Web ReadableStream works in both Node.js and browsers | ||||
|   - ⚠️ Node.js streams only work in Node.js environment | ||||
|  | ||||
| - **`.raw(streamFunc)`** - Advanced control over request streaming (Node.js only) | ||||
| - **`.raw(streamFunc)`** - Advanced control over request streaming | ||||
|   - `streamFunc`: Function that receives the raw request object for custom streaming | ||||
|   - ❌ **Node.js only** - not supported in browsers | ||||
|   - Use for advanced scenarios like chunked transfer encoding | ||||
|  | ||||
| These methods are particularly useful for: | ||||
| - Uploading large files without loading them into memory | ||||
|   | ||||
							
								
								
									
										41
									
								
								test/test.streaming.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								test/test.streaming.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import { SmartRequest } from '../ts/index.js'; | ||||
|  | ||||
| tap.test('browser: should send Uint8Array using buffer() method', async () => { | ||||
|   const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII | ||||
|    | ||||
|   const smartRequest = SmartRequest.create() | ||||
|     .url('https://httpbin.org/post') | ||||
|     .buffer(testData, 'application/octet-stream') | ||||
|     .method('POST'); | ||||
|    | ||||
|   const response = await smartRequest.post(); | ||||
|   const data = await response.json(); | ||||
|    | ||||
|   expect(data).toHaveProperty('data'); | ||||
|   expect(data.headers['Content-Type']).toEqual('application/octet-stream'); | ||||
| }); | ||||
|  | ||||
| tap.test('browser: should send web ReadableStream using stream() method', async () => { | ||||
|   // Create a web ReadableStream | ||||
|   const encoder = new TextEncoder(); | ||||
|   const stream = new ReadableStream({ | ||||
|     start(controller) { | ||||
|       controller.enqueue(encoder.encode('Test stream data')); | ||||
|       controller.close(); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   const smartRequest = SmartRequest.create() | ||||
|     .url('https://httpbin.org/post') | ||||
|     .stream(stream, 'text/plain') | ||||
|     .method('POST'); | ||||
|    | ||||
|   const response = await smartRequest.post(); | ||||
|   const data = await response.json(); | ||||
|    | ||||
|   expect(data).toHaveProperty('data'); | ||||
|   // httpbin should receive the streamed data | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
							
								
								
									
										27
									
								
								test/test.streamnode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								test/test.streamnode.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import { SmartRequest } from '../ts/index.js'; | ||||
|  | ||||
| tap.test('should have streamNode() method available', async () => { | ||||
|   const response = await SmartRequest.create() | ||||
|     .url('https://httpbin.org/get') | ||||
|     .get(); | ||||
|  | ||||
|   // Verify streamNode() method exists | ||||
|   expect(response.streamNode).toBeDefined(); | ||||
|   expect(typeof response.streamNode).toEqual('function'); | ||||
|    | ||||
|   // In Node.js, it should return a stream | ||||
|   const nodeStream = response.streamNode(); | ||||
|   expect(nodeStream).toBeDefined(); | ||||
|    | ||||
|   // Verify it's a Node.js readable stream | ||||
|   expect(typeof nodeStream.pipe).toEqual('function'); | ||||
|   expect(typeof nodeStream.on).toEqual('function'); | ||||
|    | ||||
|   // Consume the stream to avoid hanging | ||||
|   nodeStream.resume(); | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
| export default tap.start(); | ||||
							
								
								
									
										60
									
								
								test/test.timeout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								test/test.timeout.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import { SmartRequest } from '../ts/index.js'; | ||||
|  | ||||
| tap.test('should clear timeout when request completes before timeout', async () => { | ||||
|   // Set a long timeout that would keep the process alive if not cleared | ||||
|   const response = await SmartRequest.create() | ||||
|     .url('https://httpbin.org/delay/1') // 1 second delay | ||||
|     .timeout(10000) // 10 second timeout (much longer than needed) | ||||
|     .get(); | ||||
|    | ||||
|   const data = await response.json(); | ||||
|   expect(data).toBeDefined(); | ||||
|    | ||||
|   // The test should complete quickly, not wait for the 10 second timeout | ||||
|   // If the timeout isn't cleared, the process would hang for 10 seconds | ||||
| }); | ||||
|  | ||||
| tap.test('should timeout when request takes longer than timeout', async () => { | ||||
|   let errorThrown = false; | ||||
|    | ||||
|   try { | ||||
|     // Try to fetch with a very short timeout | ||||
|     await SmartRequest.create() | ||||
|       .url('https://httpbin.org/delay/3') // 3 second delay | ||||
|       .timeout(100) // 100ms timeout (will fail) | ||||
|       .get(); | ||||
|   } catch (error) { | ||||
|     errorThrown = true; | ||||
|     expect(error.message).toContain('Request timed out'); | ||||
|   } | ||||
|    | ||||
|   expect(errorThrown).toBeTrue(); | ||||
| }); | ||||
|  | ||||
| tap.test('should not leak timers with multiple successful requests', async () => { | ||||
|   // Make multiple requests with timeouts to ensure no timer leaks | ||||
|   const promises = []; | ||||
|    | ||||
|   for (let i = 0; i < 5; i++) { | ||||
|     promises.push( | ||||
|       SmartRequest.create() | ||||
|         .url('https://httpbin.org/get') | ||||
|         .timeout(5000) // 5 second timeout | ||||
|         .get() | ||||
|         .then(response => response.json()) | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   const results = await Promise.all(promises); | ||||
|    | ||||
|   // All requests should complete successfully | ||||
|   expect(results).toHaveLength(5); | ||||
|   results.forEach(result => { | ||||
|     expect(result).toBeDefined(); | ||||
|   }); | ||||
|    | ||||
|   // Process should exit cleanly after this test without hanging | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartrequest', | ||||
|   version: '4.3.0', | ||||
|   version: '4.3.6', | ||||
|   description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.' | ||||
| } | ||||
|   | ||||
| @@ -164,7 +164,7 @@ export class SmartRequest<T = any> { | ||||
|   /** | ||||
|    * Provide a custom function to handle raw request streaming | ||||
|    * This gives full control over the request body streaming | ||||
|    * Note: Only works in Node.js environment | ||||
|    * Note: Only works in Node.js environment, not supported in browsers | ||||
|    */ | ||||
|   raw(streamFunc: RawStreamFunction): this { | ||||
|     // Store the raw streaming function to be used later | ||||
|   | ||||
| @@ -15,7 +15,6 @@ if (smartenvInstance.isNode) { | ||||
|     plugins.smartpath.dirname(import.meta.url), | ||||
|     '../core_node/index.js', | ||||
|   ); | ||||
|   console.log(modulePath); | ||||
|   const impl = await smartenvInstance.getSafeNodeModule(modulePath); | ||||
|   CoreRequest = impl.CoreRequest; | ||||
|   CoreResponse = impl.CoreResponse; | ||||
|   | ||||
| @@ -42,4 +42,9 @@ export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> { | ||||
|    * Get response as a web-style ReadableStream | ||||
|    */ | ||||
|   abstract stream(): ReadableStream<Uint8Array> | null; | ||||
|  | ||||
|   /** | ||||
|    * Get response as a Node.js stream (throws in browser) | ||||
|    */ | ||||
|   abstract streamNode(): NodeJS.ReadableStream | never; | ||||
| } | ||||
|   | ||||
| @@ -86,4 +86,5 @@ export interface ICoreResponse<T = any> { | ||||
|   text(): Promise<string>; | ||||
|   arrayBuffer(): Promise<ArrayBuffer>; | ||||
|   stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream | ||||
|   streamNode(): NodeJS.ReadableStream | never; // Returns Node.js stream or throws in browser | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,9 @@ 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); | ||||
|  | ||||
| @@ -61,11 +64,19 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|       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 | ||||
|         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); | ||||
| @@ -92,9 +103,13 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|     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; | ||||
|       this.abortController = new AbortController(); | ||||
|       this.timeoutId = setTimeout(() => { | ||||
|         if (this.abortController) { | ||||
|           this.abortController.abort(); | ||||
|         } | ||||
|       }, timeout); | ||||
|       fetchOptions.signal = this.abortController.signal; | ||||
|     } | ||||
|  | ||||
|     return fetchOptions; | ||||
| @@ -117,8 +132,12 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|  | ||||
|     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'); | ||||
|       } | ||||
| @@ -126,6 +145,19 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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 | ||||
|    */ | ||||
|   | ||||
| @@ -7,9 +7,6 @@ export * from '../core_base/types.js'; | ||||
|  * Fetch-specific response extensions | ||||
|  */ | ||||
| export interface IFetchResponse<T = any> extends baseTypes.ICoreResponse<T> { | ||||
|   // Node.js stream method that throws in browser | ||||
|   streamNode(): never; | ||||
|  | ||||
|   // Access to raw Response object | ||||
|   raw(): Response; | ||||
| } | ||||
|   | ||||
| @@ -119,10 +119,11 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|     } | ||||
|  | ||||
|     // Perform the request | ||||
|     let timeoutId: NodeJS.Timeout | null = null; | ||||
|     const request = requestModule.request(this.options, async (response) => { | ||||
|       // Handle hard timeout | ||||
|       if (this.options.hardDataCuttingTimeout) { | ||||
|         setTimeout(() => { | ||||
|         timeoutId = setTimeout(() => { | ||||
|           response.destroy(); | ||||
|           done.reject(new Error('Request timed out')); | ||||
|         }, this.options.hardDataCuttingTimeout); | ||||
| @@ -132,6 +133,14 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|       done.resolve(response); | ||||
|     }); | ||||
|  | ||||
|     // Set request timeout (Node.js built-in timeout) | ||||
|     if (this.options.timeout) { | ||||
|       request.setTimeout(this.options.timeout, () => { | ||||
|         request.destroy(); | ||||
|         done.reject(new Error('Request timed out')); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Write request body | ||||
|     if (this.options.requestBody) { | ||||
|       if (this.options.requestBody instanceof plugins.formData) { | ||||
| @@ -159,11 +168,23 @@ export class CoreRequest extends AbstractCoreRequest< | ||||
|     request.on('error', (e) => { | ||||
|       console.error(e); | ||||
|       request.destroy(); | ||||
|       // Clear timeout on error | ||||
|       if (timeoutId) { | ||||
|         clearTimeout(timeoutId); | ||||
|         timeoutId = null; | ||||
|       } | ||||
|       done.reject(e); | ||||
|     }); | ||||
|  | ||||
|     // Get response and handle response errors | ||||
|     const response = await done.promise; | ||||
|      | ||||
|     // Clear timeout on successful response | ||||
|     if (timeoutId) { | ||||
|       clearTimeout(timeoutId); | ||||
|       timeoutId = null; | ||||
|     } | ||||
|      | ||||
|     response.on('error', (err) => { | ||||
|       console.error(err); | ||||
|       response.destroy(); | ||||
|   | ||||
| @@ -16,9 +16,6 @@ export interface IExtendedIncomingMessage<T = any> | ||||
|  * Node.js specific response extensions | ||||
|  */ | ||||
| export interface INodeResponse<T = any> extends baseTypes.ICoreResponse<T> { | ||||
|   // Node.js specific methods | ||||
|   streamNode(): NodeJS.ReadableStream; // Returns Node.js style stream | ||||
|  | ||||
|   // Legacy compatibility | ||||
|   raw(): plugins.http.IncomingMessage; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user