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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- 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
|
- name: Audit production dependencies
|
||||||
run: |
|
run: |
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
npm config set registry https://registry.npmjs.org
|
||||||
npmci command pnpm audit --audit-level=high --prod
|
pnpm audit --audit-level=high --prod
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Audit development dependencies
|
- name: Audit development dependencies
|
||||||
run: |
|
run: |
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
npm config set registry https://registry.npmjs.org
|
||||||
npmci command pnpm audit --audit-level=high --dev
|
pnpm audit --audit-level=high --dev
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -55,12 +47,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
pnpm install
|
||||||
npmci npm install
|
pnpm test
|
||||||
npmci npm test
|
|
||||||
|
|
||||||
- name: Test build
|
- name: Test build
|
||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
pnpm install
|
||||||
npmci npm install
|
pnpm build
|
||||||
npmci npm build
|
|
||||||
|
|||||||
@@ -23,22 +23,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @ship.zone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
run: |
|
run: |
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
npm config set registry https://registry.npmjs.org
|
||||||
npmci command pnpm audit --audit-level=high --prod
|
pnpm audit --audit-level=high --prod
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Audit development dependencies
|
- name: Audit development dependencies
|
||||||
run: |
|
run: |
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
npm config set registry https://registry.npmjs.org
|
||||||
npmci command pnpm audit --audit-level=high --dev
|
pnpm audit --audit-level=high --dev
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -51,23 +45,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @ship.zone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
pnpm install
|
||||||
npmci npm install
|
pnpm test
|
||||||
npmci npm test
|
|
||||||
|
|
||||||
- name: Test build
|
- name: Test build
|
||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
pnpm install
|
||||||
npmci npm install
|
pnpm build
|
||||||
npmci npm build
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: test
|
needs: test
|
||||||
@@ -79,16 +65,27 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @ship.zone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
pnpm install
|
||||||
npmci npm publish
|
# 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:
|
metadata:
|
||||||
needs: test
|
needs: test
|
||||||
@@ -101,24 +98,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @ship.zone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Code quality
|
- name: Code quality
|
||||||
run: |
|
run: |
|
||||||
npmci command npm install -g typescript
|
npm install -g typescript
|
||||||
npmci npm install
|
pnpm install
|
||||||
|
|
||||||
- name: Trigger
|
|
||||||
run: npmci trigger
|
|
||||||
|
|
||||||
- name: Build docs and upload artifacts
|
- name: Build docs and upload artifacts
|
||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
pnpm install
|
||||||
npmci npm install
|
|
||||||
pnpm install -g @git.zone/tsdoc
|
pnpm install -g @git.zone/tsdoc
|
||||||
npmci command tsdoc
|
tsdoc
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
48
changelog.md
48
changelog.md
@@ -1,5 +1,53 @@
|
|||||||
# Changelog
|
# 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)
|
## 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 streaming and raw buffer support to SmartRequest (buffer, stream, raw); update docs and tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartrequest",
|
"name": "@push.rocks/smartrequest",
|
||||||
"version": "4.3.0",
|
"version": "4.3.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
13
readme.md
13
readme.md
@@ -379,15 +379,20 @@ async function uploadBinaryData() {
|
|||||||
#### Streaming Methods
|
#### Streaming Methods
|
||||||
|
|
||||||
- **`.buffer(data, contentType?)`** - Stream a Buffer or Uint8Array directly
|
- **`.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')
|
- `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(stream, contentType?)`** - Stream from ReadableStream
|
||||||
- `stream`: The stream to pipe to the request
|
- `stream`: Web ReadableStream (both platforms) or Node.js stream (Node.js only)
|
||||||
- `contentType`: Optional content type
|
- `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
|
- `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:
|
These methods are particularly useful for:
|
||||||
- Uploading large files without loading them into memory
|
- 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 = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartrequest',
|
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.'
|
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
|
* Provide a custom function to handle raw request streaming
|
||||||
* This gives full control over the request body 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 {
|
raw(streamFunc: RawStreamFunction): this {
|
||||||
// Store the raw streaming function to be used later
|
// Store the raw streaming function to be used later
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ if (smartenvInstance.isNode) {
|
|||||||
plugins.smartpath.dirname(import.meta.url),
|
plugins.smartpath.dirname(import.meta.url),
|
||||||
'../core_node/index.js',
|
'../core_node/index.js',
|
||||||
);
|
);
|
||||||
console.log(modulePath);
|
|
||||||
const impl = await smartenvInstance.getSafeNodeModule(modulePath);
|
const impl = await smartenvInstance.getSafeNodeModule(modulePath);
|
||||||
CoreRequest = impl.CoreRequest;
|
CoreRequest = impl.CoreRequest;
|
||||||
CoreResponse = impl.CoreResponse;
|
CoreResponse = impl.CoreResponse;
|
||||||
|
|||||||
@@ -42,4 +42,9 @@ export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> {
|
|||||||
* Get response as a web-style ReadableStream
|
* Get response as a web-style ReadableStream
|
||||||
*/
|
*/
|
||||||
abstract stream(): ReadableStream<Uint8Array> | null;
|
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>;
|
text(): Promise<string>;
|
||||||
arrayBuffer(): Promise<ArrayBuffer>;
|
arrayBuffer(): Promise<ArrayBuffer>;
|
||||||
stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream
|
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,
|
types.ICoreRequestOptions,
|
||||||
CoreResponse
|
CoreResponse
|
||||||
> {
|
> {
|
||||||
|
private timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private abortController: AbortController | null = null;
|
||||||
|
|
||||||
constructor(url: string, options: types.ICoreRequestOptions = {}) {
|
constructor(url: string, options: types.ICoreRequestOptions = {}) {
|
||||||
super(url, options);
|
super(url, options);
|
||||||
|
|
||||||
@@ -61,11 +64,19 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
if (
|
if (
|
||||||
typeof this.options.requestBody === 'string' ||
|
typeof this.options.requestBody === 'string' ||
|
||||||
this.options.requestBody instanceof ArrayBuffer ||
|
this.options.requestBody instanceof ArrayBuffer ||
|
||||||
|
this.options.requestBody instanceof Uint8Array ||
|
||||||
this.options.requestBody instanceof FormData ||
|
this.options.requestBody instanceof FormData ||
|
||||||
this.options.requestBody instanceof URLSearchParams ||
|
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;
|
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 {
|
} else {
|
||||||
// Convert objects to JSON
|
// Convert objects to JSON
|
||||||
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
||||||
@@ -92,9 +103,13 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
||||||
const timeout =
|
const timeout =
|
||||||
this.options.hardDataCuttingTimeout || this.options.timeout;
|
this.options.hardDataCuttingTimeout || this.options.timeout;
|
||||||
const controller = new AbortController();
|
this.abortController = new AbortController();
|
||||||
setTimeout(() => controller.abort(), timeout);
|
this.timeoutId = setTimeout(() => {
|
||||||
fetchOptions.signal = controller.signal;
|
if (this.abortController) {
|
||||||
|
this.abortController.abort();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
fetchOptions.signal = this.abortController.signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchOptions;
|
return fetchOptions;
|
||||||
@@ -117,8 +132,12 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
|
// Clear timeout on successful response
|
||||||
|
this.clearTimeout();
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clear timeout on error
|
||||||
|
this.clearTimeout();
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
throw new Error('Request timed out');
|
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
|
* Static factory method to create and fire a request
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ export * from '../core_base/types.js';
|
|||||||
* Fetch-specific response extensions
|
* Fetch-specific response extensions
|
||||||
*/
|
*/
|
||||||
export interface IFetchResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
export interface IFetchResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||||
// Node.js stream method that throws in browser
|
|
||||||
streamNode(): never;
|
|
||||||
|
|
||||||
// Access to raw Response object
|
// Access to raw Response object
|
||||||
raw(): Response;
|
raw(): Response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,10 +119,11 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform the request
|
// Perform the request
|
||||||
|
let timeoutId: NodeJS.Timeout | null = null;
|
||||||
const request = requestModule.request(this.options, async (response) => {
|
const request = requestModule.request(this.options, async (response) => {
|
||||||
// Handle hard timeout
|
// Handle hard timeout
|
||||||
if (this.options.hardDataCuttingTimeout) {
|
if (this.options.hardDataCuttingTimeout) {
|
||||||
setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
response.destroy();
|
response.destroy();
|
||||||
done.reject(new Error('Request timed out'));
|
done.reject(new Error('Request timed out'));
|
||||||
}, this.options.hardDataCuttingTimeout);
|
}, this.options.hardDataCuttingTimeout);
|
||||||
@@ -132,6 +133,14 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
done.resolve(response);
|
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
|
// Write request body
|
||||||
if (this.options.requestBody) {
|
if (this.options.requestBody) {
|
||||||
if (this.options.requestBody instanceof plugins.formData) {
|
if (this.options.requestBody instanceof plugins.formData) {
|
||||||
@@ -159,11 +168,23 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
request.on('error', (e) => {
|
request.on('error', (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
request.destroy();
|
request.destroy();
|
||||||
|
// Clear timeout on error
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
done.reject(e);
|
done.reject(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get response and handle response errors
|
// Get response and handle response errors
|
||||||
const response = await done.promise;
|
const response = await done.promise;
|
||||||
|
|
||||||
|
// Clear timeout on successful response
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
response.on('error', (err) => {
|
response.on('error', (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
response.destroy();
|
response.destroy();
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export interface IExtendedIncomingMessage<T = any>
|
|||||||
* Node.js specific response extensions
|
* Node.js specific response extensions
|
||||||
*/
|
*/
|
||||||
export interface INodeResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
export interface INodeResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||||
// Node.js specific methods
|
|
||||||
streamNode(): NodeJS.ReadableStream; // Returns Node.js style stream
|
|
||||||
|
|
||||||
// Legacy compatibility
|
// Legacy compatibility
|
||||||
raw(): plugins.http.IncomingMessage;
|
raw(): plugins.http.IncomingMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user