Compare commits

..

15 Commits

Author SHA1 Message Date
df28cd4778 4.3.6
Some checks failed
Default (tags) / security (push) Successful in 4s
Default (tags) / test (push) Successful in 43s
Default (tags) / release (push) Failing after 29s
Default (tags) / metadata (push) Successful in 54s
2025-10-26 14:12:38 +00:00
f49cbd2b6a fix(ci): Use .npmrc for registry authentication in Gitea workflow and add conditional npmjs publish 2025-10-26 14:12:37 +00:00
984b53cba2 4.3.5
Some checks failed
Default (tags) / security (push) Successful in 11s
Default (tags) / test (push) Successful in 47s
Default (tags) / release (push) Failing after 29s
Default (tags) / metadata (push) Successful in 57s
2025-10-26 14:07:55 +00:00
4c55243646 fix(workflows): Remove npmci wrappers from CI workflows and use pnpm/npm CLI directly 2025-10-26 14:07:55 +00:00
49cfcaedd1 4.3.4
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Successful in 1m1s
Default (tags) / release (push) Failing after 46s
Default (tags) / metadata (push) Successful in 56s
2025-10-25 17:16:01 +00:00
3996a69f91 fix(ci): Fix Gitea workflow publish invocation to run npm publish via npmci command 2025-10-25 17:16:01 +00:00
629f6dd425 4.3.3
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Successful in 1m5s
Default (tags) / release (push) Failing after 48s
Default (tags) / metadata (push) Successful in 57s
2025-10-25 17:09:33 +00:00
d141ceeaf7 fix(ci): Improve Gitea release workflow: install deps, configure Gitea npm registry, and optionally publish to npmjs.org 2025-10-25 17:09:33 +00:00
7d3c94cae6 4.3.2
Some checks failed
Default (tags) / security (push) Successful in 58s
Default (tags) / test (push) Successful in 1m45s
Default (tags) / release (push) Failing after 59s
Default (tags) / metadata (push) Successful in 1m0s
2025-10-17 09:09:12 +00:00
5bae452365 fix(core): Remove stray console.log from core module 2025-10-17 09:09:12 +00:00
ffabcf7bdb 4.3.1
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Successful in 1m2s
Default (tags) / release (push) Failing after 43s
Default (tags) / metadata (push) Successful in 53s
2025-08-19 01:36:44 +00:00
361d97f440 fix(core): Improve streaming support and timeout handling; add browser streaming & timeout tests and README clarifications 2025-08-19 01:36:44 +00:00
35867d9148 feat(response): Add streamNode() method for Node.js stream support; update tests 2025-08-19 01:20:19 +00:00
d455a34632 4.3.0
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Successful in 59s
Default (tags) / release (push) Failing after 44s
Default (tags) / metadata (push) Successful in 54s
2025-08-18 22:29:24 +00:00
9c5a939499 feat(client/smartrequest): Add streaming and raw buffer support to SmartRequest (buffer, stream, raw); update docs and tests 2025-08-18 22:29:24 +00:00
19 changed files with 537 additions and 78 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,64 @@
# 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
- Add SmartRequest.buffer(data, contentType?) to send Buffer or Uint8Array bodies with Content-Type header.
- Add SmartRequest.stream(stream, contentType?) to accept Node.js Readable streams or web ReadableStream and set Content-Type when provided.
- Add SmartRequest.raw(streamFunc) to allow custom raw streaming functions (Node.js only) and a RawStreamFunction type.
- Wire Node.js stream handling into CoreRequest by passing a requestDataFunc when creating CoreRequest instances.
- Add comprehensive streaming examples and documentation to README describing buffer/stream/raw usage and streaming methods.
- Add tests for streaming behavior (test/test.streaming.ts) covering buffer, stream, raw, and Uint8Array usage.
- Update client exports and plugins to support streaming features and FormData usage where needed.
## 2025-08-18 - 4.2.2 - fix(client)
Fix CI configuration, prevent socket hangs with auto-drain, and apply various client/core TypeScript fixes and test updates

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartrequest",
"version": "4.2.2",
"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": {

View File

@@ -25,7 +25,7 @@ yarn add @push.rocks/smartrequest
-**Keep-Alive Connections** - Efficient connection pooling in Node.js
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
- 📡 **Streaming Support** - Handle large files and real-time data
- 📡 **Streaming Support** - Stream buffers, files, and custom data without loading into memory
- 🔧 **Highly Configurable** - Timeouts, retries, headers, and more
## Architecture
@@ -303,6 +303,103 @@ async function uploadMultipleFiles(
}
```
### Streaming Request Bodies
SmartRequest provides multiple ways to stream data in requests, making it easy to upload large files or send real-time data without loading everything into memory:
```typescript
import { SmartRequest } from '@push.rocks/smartrequest';
import * as fs from 'fs';
import { Readable } from 'stream';
// Stream a Buffer directly
async function uploadBuffer() {
const buffer = Buffer.from('Hello, World!');
const response = await SmartRequest.create()
.url('https://api.example.com/upload')
.buffer(buffer, 'text/plain')
.post();
return await response.json();
}
// Stream a file using Node.js streams
async function uploadLargeFile(filePath: string) {
const fileStream = fs.createReadStream(filePath);
const response = await SmartRequest.create()
.url('https://api.example.com/upload')
.stream(fileStream, 'application/octet-stream')
.post();
return await response.json();
}
// Stream data from any readable source
async function streamData(dataSource: Readable) {
const response = await SmartRequest.create()
.url('https://api.example.com/stream')
.stream(dataSource)
.post();
return await response.json();
}
// Advanced: Full control over request streaming (Node.js only)
async function customStreaming() {
const response = await SmartRequest.create()
.url('https://api.example.com/stream')
.raw((request) => {
// Custom streaming logic - you have full control
request.write('chunk1');
request.write('chunk2');
// Stream from another source
someReadableStream.pipe(request);
})
.post();
return await response.json();
}
// Send Uint8Array (works in both Node.js and browser)
async function uploadBinaryData() {
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
const response = await SmartRequest.create()
.url('https://api.example.com/binary')
.buffer(data, 'application/octet-stream')
.post();
return await response.json();
}
```
#### Streaming Methods
- **`.buffer(data, contentType?)`** - Stream a Buffer or Uint8Array directly
- `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 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
- `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
- Streaming real-time data to servers
- Proxying data between services
- Implementing chunked transfer encoding
### Unix Socket Support (Node.js only)
```typescript

View 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();

74
test/test.streaming.ts Normal file
View File

@@ -0,0 +1,74 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as fs from 'fs';
import { SmartRequest } from '../ts/index.js';
tap.test('should send a buffer using buffer() method', async () => {
const testBuffer = Buffer.from('Hello, World!');
const smartRequest = SmartRequest.create()
.url('https://httpbin.org/post')
.buffer(testBuffer, 'text/plain')
.method('POST');
const response = await smartRequest.post();
const data = await response.json();
expect(data).toHaveProperty('data');
expect(data.data).toEqual('Hello, World!');
expect(data.headers['Content-Type']).toEqual('text/plain');
});
tap.test('should send a stream using stream() method', async () => {
// Create a simple readable stream
const { Readable } = await import('stream');
const testData = 'Stream data test';
const stream = Readable.from([testData]);
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');
expect(data.data).toEqual(testData);
});
tap.test('should handle raw streaming with custom function', async () => {
const testData = 'Custom raw stream data';
const smartRequest = SmartRequest.create()
.url('https://httpbin.org/post')
.raw((request) => {
// Custom streaming logic
request.write(testData);
request.end();
})
.method('POST');
const response = await smartRequest.post();
const data = await response.json();
expect(data).toHaveProperty('data');
expect(data.data).toEqual(testData);
});
tap.test('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();
// Just verify that data was sent
expect(data).toHaveProperty('data');
expect(data.headers['Content-Type']).toEqual('application/octet-stream');
});
export default tap.start();

27
test/test.streamnode.ts Normal file
View 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
View 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();

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartrequest',
version: '4.2.2',
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.'
}

View File

@@ -8,6 +8,7 @@ import type {
ResponseType,
FormField,
RateLimitConfig,
RawStreamFunction,
} from './types/common.js';
import {
type TPaginationConfig,
@@ -121,6 +122,56 @@ export class SmartRequest<T = any> {
return this;
}
/**
* Set raw buffer data for the request
*/
buffer(data: Buffer | Uint8Array, contentType?: string): this {
if (!this._options.headers) {
this._options.headers = {};
}
this._options.headers['Content-Type'] = contentType || 'application/octet-stream';
this._options.requestBody = data;
return this;
}
/**
* Stream data for the request
* Accepts Node.js Readable streams or web ReadableStream
*/
stream(stream: NodeJS.ReadableStream | ReadableStream<Uint8Array>, contentType?: string): this {
if (!this._options.headers) {
this._options.headers = {};
}
// Set content type if provided
if (contentType) {
this._options.headers['Content-Type'] = contentType;
}
// Check if it's a Node.js stream (has pipe method)
if ('pipe' in stream && typeof (stream as any).pipe === 'function') {
// For Node.js streams, we need to use a custom approach
// Store the stream to be used later
(this._options as any).__nodeStream = stream;
} else {
// For web ReadableStream, pass directly
this._options.requestBody = stream;
}
return this;
}
/**
* 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, not supported in browsers
*/
raw(streamFunc: RawStreamFunction): this {
// Store the raw streaming function to be used later
(this._options as any).__rawStreamFunc = streamFunc;
return this;
}
/**
* Set request timeout in milliseconds
*/
@@ -389,7 +440,22 @@ export class SmartRequest<T = any> {
// Main retry loop
for (let attempt = 0; attempt <= this._retries; attempt++) {
try {
const request = new CoreRequest(this._url, this._options as any);
// Check if we have a Node.js stream or raw function that needs special handling
let requestDataFunc = null;
if ((this._options as any).__nodeStream) {
const nodeStream = (this._options as any).__nodeStream;
requestDataFunc = (req: any) => {
nodeStream.pipe(req);
};
// Remove the temporary stream reference
delete (this._options as any).__nodeStream;
} else if ((this._options as any).__rawStreamFunc) {
requestDataFunc = (this._options as any).__rawStreamFunc;
// Remove the temporary function reference
delete (this._options as any).__rawStreamFunc;
}
const request = new CoreRequest(this._url, this._options as any, requestDataFunc);
const response = (await request.fire()) as ICoreResponse<R>;
// Check for 429 status if rate limit handling is enabled

View File

@@ -66,3 +66,9 @@ export interface RateLimitConfig {
backoffFactor?: number; // Exponential backoff factor (default: 2)
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
}
/**
* Raw streaming function for advanced request body control
* Note: The request parameter type depends on the environment (Node.js ClientRequest or fetch Request)
*/
export type RawStreamFunction = (request: any) => void;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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
*/

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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;
}