fix(client): Fix CI configuration, prevent socket hangs with auto-drain, and apply various client/core TypeScript fixes and test updates

This commit is contained in:
2025-08-18 00:21:14 +00:00
parent 9b9c8fd618
commit ee750dea58
34 changed files with 2144 additions and 892 deletions

View File

@@ -6,8 +6,8 @@ on:
- '**'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
@@ -26,7 +26,7 @@ jobs:
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare

View File

@@ -6,8 +6,8 @@ on:
- '*'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
@@ -26,7 +26,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
@@ -54,7 +54,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
@@ -82,7 +82,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
@@ -104,7 +104,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality

7
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# artifacts
coverage/
public/
pages/
# installs
node_modules/
@@ -17,4 +16,8 @@ node_modules/
dist/
dist_*/
# custom
# AI
.claude/
.serena/
#------# custom

View File

@@ -1,25 +1,41 @@
# Changelog
## 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
- CI/workflow updates: switch container IMAGE to code.foss.global/host.today/ht-docker-node:npmci, adjust NPMCI_COMPUTED_REPOURL, and install @ship.zone/npmci instead of @shipzone/npmci
- Prevent socket hanging by adding automatic draining of unconsumed Node.js response bodies (configurable via options.autoDrain / SmartRequest.autoDrain); added logging when auto-drain runs and updated tests to consume bodies
- Client improvements: fixes and cleanups in SmartRequest (accept header mapping, formData header handling, options(), pagination helpers, handle429Backoff backoff/Retry-After parsing and callbacks, retry logic and small API ergonomics)
- Core fixes: fetch and node implementations corrected (buildUrl, fetch options, request/response constructors, stream conversions to web ReadableStream, proper error messages) and consistent exports
- TypeScript and formatting fixes across many files (consistent trailing commas, object layout, newline fixes, typed function signatures, cleaned up exports and module imports)
- Package metadata and tooling updates: package.json bug/homepage URLs adjusted to code.foss.global, bumped @git.zone/tstest devDependency, added pnpm overrides field; small .gitignore additions
## 2025-07-29 - 4.2.1 - fix(client)
Fix socket hanging issues and add auto-drain feature
**Fixes:**
- Fixed socket hanging issues caused by unconsumed response bodies
- Resolved test timeout problems where sockets remained open after tests completed
**Features:**
- Added automatic response body draining to prevent socket pool exhaustion
- Made auto-drain configurable via `autoDrain()` method (enabled by default)
- Added logging when auto-drain activates for debugging purposes
**Improvements:**
- Updated all tests to properly consume response bodies
- Enhanced documentation about the importance of consuming response bodies
## 2025-07-29 - 4.2.0 - feat(client)
Add handle429Backoff method for intelligent rate limit handling
**Features:**
- Added `handle429Backoff()` method to SmartRequest class for automatic HTTP 429 handling
- Respects `Retry-After` headers with support for both seconds and HTTP date formats
- Configurable exponential backoff when no Retry-After header is present
@@ -28,30 +44,37 @@ Add handle429Backoff method for intelligent rate limit handling
- Maximum wait time capping to prevent excessive delays
**Improvements:**
- Updated test endpoints to use more reliable services (jsonplaceholder, echo.zuplo.io)
- Added timeout parameter to test script for better CI/CD compatibility
**Documentation:**
- Added comprehensive rate limiting section to README with examples
- Documented all configuration options for handle429Backoff
## 2025-07-29 - 4.1.0 - feat(client)
Add missing options() method to SmartRequest client
**Features:**
- Added `options()` method to SmartRequest class for setting arbitrary request options
- Enables setting keepAlive and other platform-specific options via fluent API
- Added test coverage for keepAlive functionality
**Documentation:**
- Updated README with examples of using the `options()` method
- Added specific examples for enabling keepAlive connections
- Corrected all documentation to use `options()` instead of `option()`
## 2025-07-28 - 4.0.0 - BREAKING CHANGE(core)
Complete architectural overhaul with cross-platform support
**Breaking Changes:**
- Renamed `SmartRequestClient` to `SmartRequest` for simpler, cleaner API
- Removed legacy API entirely (no more `/legacy` import path)
- Major architectural refactoring:
@@ -65,6 +88,7 @@ Complete architectural overhaul with cross-platform support
- Removed all "Abstract" prefixes from type names
**Features:**
- Full cross-platform support (Node.js and browsers)
- Automatic platform detection using @push.rocks/smartenv
- Consistent API across platforms with platform-specific capabilities
@@ -72,15 +96,18 @@ Complete architectural overhaul with cross-platform support
- Better error messages for unsupported platform features
**Documentation:**
- Completely rewritten README with platform-specific examples
- Added architecture overview section
- Added migration guide from v2.x and v3.x
- Updated all examples to use the new `SmartRequest` class name
## 2025-07-27 - 3.0.0 - BREAKING CHANGE(core)
Major architectural refactoring with fetch-like API
**Breaking Changes:**
- Legacy API functions are now imported from `@push.rocks/smartrequest/legacy` instead of the main export
- Modern API response objects now use fetch-like methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`) instead of direct `.body` access
- Renamed `responseType()` method to `accept()` in modern API
@@ -94,17 +121,20 @@ Major architectural refactoring with fetch-like API
- Legacy API is now just an adapter over the core module
**Features:**
- New fetch-like response API with single-use body consumption
- Better TypeScript support and type safety
- Cleaner separation of concerns between request and response
- More predictable behavior aligned with fetch API standards
**Documentation:**
- Updated all examples to show correct import paths
- Added comprehensive examples for the new response API
- Enhanced migration guide
## 2025-04-03 - 2.1.0 - feat(docs)
Enhance documentation and tests with modern API usage examples and migration guide
- Updated README to include detailed examples for the modern fluent API, covering GET, POST, headers, query, timeout, retries, and pagination
@@ -114,6 +144,7 @@ Enhance documentation and tests with modern API usage examples and migration gui
- Minor formatting improvements in the code and documentation examples
## 2024-11-06 - 2.0.23 - fix(core)
Enhance type safety for response in binary requests
- Updated the dependency versions in package.json to their latest versions.
@@ -121,31 +152,37 @@ Enhance type safety for response in binary requests
- Introduced generic typing to IExtendedIncomingMessage interface for better type safety.
## 2024-05-29 - 2.0.22 - Documentation
update description
## 2024-04-01 - 2.0.21 - Configuration
Updated configuration files
- Updated `tsconfig`
- Updated `npmextra.json`: githost
## 2023-07-10 - 2.0.15 - Structure
Refactored the organization structure
- Switched to a new organization scheme
## 2022-07-29 - 1.1.57 to 2.0.0 - Major Update
Significant changes and improvements leading to a major version update
- **BREAKING CHANGE**: Switched the core to use ECMAScript modules (ESM)
## 2018-08-14 - 1.1.12 to 1.1.13 - Functional Enhancements
Enhanced request capabilities and removed unnecessary dependencies
- Fixed request module to allow sending strings
- Removed CI dependencies
## 2018-07-19 - 1.1.1 to 1.1.11 - Various Fixes and Improvements
Improvements and fixes across various components
- Added formData capability
@@ -155,11 +192,13 @@ Improvements and fixes across various components
- Updated request ending method
## 2018-06-19 - 1.0.14 - Structural Fix
Resolved conflicts with file extensions
- Changed `.json.ts` to `.jsonrest.ts` to avoid conflicts
## 2018-06-13 - 1.0.8 to 1.0.10 - Core Updates
Ensured binary handling compliance
- Enhanced core to uphold latest standards
@@ -167,9 +206,9 @@ Ensured binary handling compliance
- Fix for handling and returning binary responses
## 2017-06-09 - 1.0.4 to 1.0.6 - Infrastructure and Type Improvements
Types and infrastructure updates
- Improved types
- Removed need for content type on post requests
- Updated for new infrastructure

View File

@@ -34,4 +34,4 @@
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}
}

View File

@@ -35,9 +35,9 @@
"author": "Task Venture Capital GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/push.rocks/smartrequest/issues"
"url": "https://code.foss.global/push.rocks/smartrequest/issues"
},
"homepage": "https://code.foss.global/push.rocks/smartrequest",
"homepage": "https://code.foss.global/push.rocks/smartrequest#readme",
"dependencies": {
"@push.rocks/smartenv": "^5.0.13",
"@push.rocks/smartpath": "^6.0.0",
@@ -49,7 +49,7 @@
"devDependencies": {
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.2",
"@git.zone/tstest": "^2.3.4",
"@types/node": "^22.9.0"
},
"files": [
@@ -67,5 +67,8 @@
"browserslist": [
"last 1 chrome versions"
],
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
"pnpm": {
"overrides": {}
}
}

2105
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
# SmartRequest Architecture Hints
## Core Features
- supports http
- supports https
- supports https
- supports unix socks
- supports formData
- supports file uploads
@@ -15,17 +16,19 @@
- used in modules like @push.rocks/smartproxy and @api.global/typedrequest
## Architecture Overview (as of v3.0.0 major refactoring)
- The project now has a multi-layer architecture with platform abstraction
- Base layer (ts/core_base/) contains abstract classes and unified types
- Node.js implementation (ts/core_node/) uses native http/https modules
- Fetch implementation (ts/core_fetch/) uses Fetch API for browser compatibility
- Core module (ts/core/) dynamically selects the appropriate implementation based on environment
- Client API (ts/client/) provides a fluent, chainable interface
- Client API (ts/client/) provides a fluent, chainable interface
- Legacy API has been completely removed in v3.0.0
## Key Components
### Core Base Module (ts/core_base/)
- `request.ts`: Abstract CoreRequest class defining the request interface
- `response.ts`: Abstract CoreResponse class with fetch-like API
- Defines `stream()` method that always returns web-style ReadableStream
@@ -35,6 +38,7 @@
- Implementations handle unsupported options by throwing errors
### Core Node Module (ts/core_node/)
- `request.ts`: Node.js implementation using http/https modules
- Supports unix socket connections and keep-alive agents
- Converts Node.js specific options from unified interface
@@ -44,6 +48,7 @@
- Methods like `json()`, `text()`, `arrayBuffer()` handle parsing
### Core Fetch Module (ts/core_fetch/)
- `request.ts`: Fetch API implementation for browsers
- Throws errors for Node.js specific options (agent, socketPath)
- Native support for CORS, credentials, and other browser features
@@ -52,27 +57,32 @@
- `streamNode()` throws error explaining it's not available in browser
### Core Module (ts/core/)
- Dynamically loads appropriate implementation based on environment
- Uses @push.rocks/smartenv for environment detection
- Exports unified types from core_base
### Client API (ts/client/)
- SmartRequest: Fluent API with method chaining
- Returns CoreResponse objects with fetch-like methods
- Supports pagination, retries, timeouts, and various response types
### Stream Handling
- `stream()` method always returns web-style ReadableStream<Uint8Array>
- In Node.js, converts native streams to web streams
- `streamNode()` available only in Node.js environment for native streams
- Consistent API across platforms while preserving platform-specific capabilities
### Binary Request Handling
- Binary requests handled through ArrayBuffer API
- Response body kept as Buffer/ArrayBuffer without string conversion
- No automatic transformations applied to binary data
## Testing
- Use `pnpm test` to run all tests
- Tests use @git.zone/tstest/tapbundle for assertions
- Separate test files for Node.js (test.node.ts) and browser (test.browser.ts)

127
readme.md
View File

@@ -1,12 +1,14 @@
# @push.rocks/smartrequest
A modern, cross-platform HTTP/HTTPS request library for Node.js and browsers with a unified API, supporting form data, file uploads, JSON, binary data, streams, and unix sockets.
## Install
```bash
# Using npm
npm install @push.rocks/smartrequest --save
# Using pnpm
# Using pnpm
pnpm add @push.rocks/smartrequest
# Using yarn
@@ -79,10 +81,10 @@ async function directCoreRequest() {
const request = new CoreRequest('https://api.example.com/data', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
Accept: 'application/json',
},
});
const response = await request.fire();
const data = await response.json();
return data;
@@ -100,7 +102,7 @@ async function searchRepositories(query: string, perPage: number = 10) {
.header('Accept', 'application/vnd.github.v3+json')
.query({
q: query,
per_page: perPage.toString()
per_page: perPage.toString(),
})
.get();
@@ -136,8 +138,8 @@ import { SmartRequest } from '@push.rocks/smartrequest';
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.options({
keepAlive: true, // Enable connection reuse (Node.js)
timeout: 10000, // 10 second timeout
keepAlive: true, // Enable connection reuse (Node.js)
timeout: 10000, // 10 second timeout
hardDataCuttingTimeout: 15000, // 15 second hard timeout
// Platform-specific options are also supported
})
@@ -153,19 +155,15 @@ import { SmartRequest } from '@push.rocks/smartrequest';
// JSON response (default)
async function fetchJson(url: string) {
const response = await SmartRequest.create()
.url(url)
.get();
const response = await SmartRequest.create().url(url).get();
return await response.json(); // Parses JSON automatically
}
// Text response
async function fetchText(url: string) {
const response = await SmartRequest.create()
.url(url)
.get();
const response = await SmartRequest.create().url(url).get();
return await response.text(); // Returns response as string
}
@@ -182,16 +180,14 @@ async function downloadImage(url: string) {
// Streaming response (Web Streams API)
async function streamLargeFile(url: string) {
const response = await SmartRequest.create()
.url(url)
.get();
const response = await SmartRequest.create().url(url).get();
// Get a web-style ReadableStream (works in both Node.js and browsers)
const stream = response.stream();
if (stream) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
@@ -206,13 +202,11 @@ async function streamLargeFile(url: string) {
// Node.js specific stream (only in Node.js environment)
async function streamWithNodeApi(url: string) {
const response = await SmartRequest.create()
.url(url)
.get();
const response = await SmartRequest.create().url(url).get();
// Only available in Node.js, throws error in browser
const nodeStream = response.streamNode();
nodeStream.on('data', (chunk) => {
console.log(`Received ${chunk.length} bytes of data`);
});
@@ -240,6 +234,7 @@ Each body method can only be called once per response, similar to the fetch API.
### Important: Always Consume Response Bodies
**You should always consume response bodies, even if you don't need the data.** Unconsumed response bodies can cause:
- Memory leaks as data accumulates in buffers
- Socket hanging with keep-alive connections
- Connection pool exhaustion
@@ -249,7 +244,7 @@ Each body method can only be called once per response, similar to the fetch API.
const response = await SmartRequest.create()
.url('https://api.example.com/status')
.get();
if (response.ok) {
console.log('Success!');
}
@@ -259,7 +254,7 @@ if (response.ok) {
const response = await SmartRequest.create()
.url('https://api.example.com/status')
.get();
if (response.ok) {
console.log('Success!');
}
@@ -269,13 +264,14 @@ await response.text(); // Consume the body even if not needed
In Node.js, SmartRequest automatically drains unconsumed responses to prevent socket hanging, but it's still best practice to explicitly consume response bodies. When auto-drain occurs, you'll see a console log: `Auto-draining unconsumed response body for [URL] (status: [STATUS])`.
You can disable auto-drain if needed:
```typescript
// Disable auto-drain (not recommended unless you have specific requirements)
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.autoDrain(false) // Disable auto-drain
.autoDrain(false) // Disable auto-drain
.get();
// Now you MUST consume the body or the socket will hang
await response.text();
```
@@ -288,19 +284,21 @@ await response.text();
import { SmartRequest } from '@push.rocks/smartrequest';
import * as fs from 'fs';
async function uploadMultipleFiles(files: Array<{name: string, path: string}>) {
const formFields = files.map(file => ({
async function uploadMultipleFiles(
files: Array<{ name: string; path: string }>,
) {
const formFields = files.map((file) => ({
name: 'files',
value: fs.readFileSync(file.path),
filename: file.name,
contentType: 'application/octet-stream'
contentType: 'application/octet-stream',
}));
const response = await SmartRequest.create()
.url('https://api.example.com/upload')
.formData(formFields)
.post();
return await response.json();
}
```
@@ -315,7 +313,7 @@ async function queryViaUnixSocket() {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
.get();
return await response.json();
}
```
@@ -336,7 +334,7 @@ async function fetchAllUsers() {
limitParam: 'limit',
startPage: 1,
pageSize: 20,
totalPath: 'meta.total'
totalPath: 'meta.total',
});
// Get first page with pagination info
@@ -362,7 +360,7 @@ async function fetchAllPosts() {
.withCursorPagination({
cursorParam: 'cursor',
cursorPath: 'meta.nextCursor',
hasMorePath: 'meta.hasMore'
hasMorePath: 'meta.hasMore',
})
.getAllPages();
@@ -415,7 +413,7 @@ import { SmartRequest } from '@push.rocks/smartrequest';
async function fetchWithRateLimitHandling() {
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.handle429Backoff() // Automatically retry on 429
.handle429Backoff() // Automatically retry on 429
.get();
return await response.json();
@@ -426,14 +424,14 @@ async function fetchWithCustomRateLimiting() {
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.handle429Backoff({
maxRetries: 5, // Try up to 5 times (default: 3)
respectRetryAfter: true, // Honor Retry-After header (default: true)
maxWaitTime: 30000, // Max 30 seconds wait (default: 60000)
fallbackDelay: 2000, // 2s initial delay if no Retry-After (default: 1000)
backoffFactor: 2, // Exponential backoff multiplier (default: 2)
maxRetries: 5, // Try up to 5 times (default: 3)
respectRetryAfter: true, // Honor Retry-After header (default: true)
maxWaitTime: 30000, // Max 30 seconds wait (default: 60000)
fallbackDelay: 2000, // 2s initial delay if no Retry-After (default: 1000)
backoffFactor: 2, // Exponential backoff multiplier (default: 2)
onRateLimit: (attempt, waitTime) => {
console.log(`Rate limited. Attempt ${attempt}, waiting ${waitTime}ms`);
}
},
})
.get();
@@ -448,8 +446,10 @@ class RateLimitedApiClient {
.handle429Backoff({
maxRetries: 3,
onRateLimit: (attempt, waitTime) => {
console.log(`API rate limit hit. Waiting ${waitTime}ms before retry ${attempt}`);
}
console.log(
`API rate limit hit. Waiting ${waitTime}ms before retry ${attempt}`,
);
},
});
}
@@ -461,6 +461,7 @@ class RateLimitedApiClient {
```
The rate limiting feature:
- Automatically detects 429 responses and retries with backoff
- Respects the `Retry-After` header when present (supports both seconds and HTTP date formats)
- Uses exponential backoff when no `Retry-After` header is provided
@@ -478,9 +479,9 @@ const response = await SmartRequest.create()
.url('https://api.example.com/data')
.options({
credentials: 'include', // Include cookies
mode: 'cors', // CORS mode
cache: 'no-cache', // Cache mode
referrerPolicy: 'no-referrer'
mode: 'cors', // CORS mode
cache: 'no-cache', // Cache mode
referrerPolicy: 'no-referrer',
})
.get();
```
@@ -496,7 +497,7 @@ const response = await SmartRequest.create()
.url('https://api.example.com/data')
.options({
agent: new Agent({ keepAlive: true }), // Custom agent
socketPath: '/var/run/api.sock', // Unix socket
socketPath: '/var/run/api.sock', // Unix socket
})
.get();
```
@@ -523,40 +524,38 @@ interface Post {
class BlogApiClient {
private baseUrl = 'https://jsonplaceholder.typicode.com';
private async request(path: string) {
return SmartRequest.create()
.url(`${this.baseUrl}${path}`)
.header('Accept', 'application/json');
}
async getUser(id: number): Promise<User> {
const response = await this.request(`/users/${id}`).get();
return response.json<User>();
}
async createPost(post: Omit<Post, 'id'>): Promise<Post> {
const response = await this.request('/posts')
.json(post)
.post();
const response = await this.request('/posts').json(post).post();
return response.json<Post>();
}
async deletePost(id: number): Promise<void> {
const response = await this.request(`/posts/${id}`).delete();
if (!response.ok) {
throw new Error(`Failed to delete post: ${response.statusText}`);
}
}
async getAllPosts(userId?: number): Promise<Post[]> {
const client = this.request('/posts');
if (userId) {
client.query({ userId: userId.toString() });
}
const response = await client.get();
return response.json<Post[]>();
}
@@ -580,15 +579,15 @@ async function fetchWithErrorHandling(url: string) {
.timeout(5000)
.retry(2)
.get();
// Check if request was successful
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Handle different content types
const contentType = response.headers['content-type'];
if (contentType?.includes('application/json')) {
return await response.json();
} else if (contentType?.includes('text/')) {
@@ -622,7 +621,7 @@ Version 3.0 brings significant architectural improvements and a more consistent
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -637,4 +636,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -6,13 +6,15 @@ import { CoreRequest, CoreResponse } from '../ts/core/index.js';
import type { ICoreRequestOptions } from '../ts/core_base/types.js';
tap.test('browser: should request a JSON document over https', async () => {
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts/1');
const request = new CoreRequest(
'https://jsonplaceholder.typicode.com/posts/1',
);
const response = await request.fire();
expect(response).not.toBeNull();
expect(response).toHaveProperty('status');
expect(response.status).toEqual(200);
const data = await response.json();
expect(data).toHaveProperty('id');
expect(data.id).toEqual(1);
@@ -22,16 +24,19 @@ tap.test('browser: should request a JSON document over https', async () => {
tap.test('browser: should handle CORS requests', async () => {
const options: ICoreRequestOptions = {
headers: {
'Accept': 'application/vnd.github.v3+json'
}
Accept: 'application/vnd.github.v3+json',
},
};
const request = new CoreRequest('https://api.github.com/users/github', options);
const request = new CoreRequest(
'https://api.github.com/users/github',
options,
);
const response = await request.fire();
expect(response).not.toBeNull();
expect(response.status).toEqual(200);
const data = await response.json();
expect(data).toHaveProperty('login');
expect(data.login).toEqual('github');
@@ -39,21 +44,24 @@ tap.test('browser: should handle CORS requests', async () => {
tap.test('browser: should handle request timeouts', async () => {
let timedOut = false;
const options: ICoreRequestOptions = {
timeout: 1 // Extremely short timeout to guarantee failure
timeout: 1, // Extremely short timeout to guarantee failure
};
try {
// Use a URL that will definitely take longer than 1ms
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts/1', options);
const request = new CoreRequest(
'https://jsonplaceholder.typicode.com/posts/1',
options,
);
await request.fire();
} catch (error) {
timedOut = true;
// Accept any error since different browsers handle timeouts differently
expect(error).toBeDefined();
}
expect(timedOut).toEqual(true);
});
@@ -61,19 +69,22 @@ tap.test('browser: should handle POST requests with JSON', async () => {
const testData = {
title: 'foo',
body: 'bar',
userId: 1
userId: 1,
};
const options: ICoreRequestOptions = {
method: 'POST',
requestBody: testData
requestBody: testData,
};
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts', options);
const request = new CoreRequest(
'https://jsonplaceholder.typicode.com/posts',
options,
);
const response = await request.fire();
expect(response.status).toEqual(201);
const responseData = await response.json();
expect(responseData).toHaveProperty('id');
expect(responseData.title).toEqual(testData.title);
@@ -84,15 +95,18 @@ tap.test('browser: should handle POST requests with JSON', async () => {
tap.test('browser: should handle query parameters', async () => {
const options: ICoreRequestOptions = {
queryParams: {
userId: '2'
}
userId: '2',
},
};
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts', options);
const request = new CoreRequest(
'https://jsonplaceholder.typicode.com/posts',
options,
);
const response = await request.fire();
expect(response.status).toEqual(200);
const data = await response.json();
expect(Array.isArray(data)).toBeTrue();
// Verify we got posts filtered by userId 2
@@ -102,4 +116,4 @@ tap.test('browser: should handle query parameters', async () => {
}
});
export default tap.start();
export default tap.start();

View File

@@ -51,7 +51,10 @@ tap.test('client: should set headers correctly', async () => {
// Check if the header exists (headers might be lowercase)
const headers = body.headers;
const headerFound = headers[customHeader] || headers[customHeader.toLowerCase()] || headers['x-custom-header'];
const headerFound =
headers[customHeader] ||
headers[customHeader.toLowerCase()] ||
headers['x-custom-header'];
expect(headerFound).toEqual(headerValue);
});
@@ -81,7 +84,7 @@ tap.test('client: should handle timeout configuration', async () => {
const response = await client.get();
expect(response).toHaveProperty('ok');
expect(response.ok).toBeTrue();
// Consume the body to prevent socket hanging
await response.text();
});
@@ -95,34 +98,40 @@ tap.test('client: should handle retry configuration', async () => {
const response = await client.get();
expect(response).toHaveProperty('ok');
expect(response.ok).toBeTrue();
// Consume the body to prevent socket hanging
await response.text();
});
tap.test('client: should support keepAlive option for connection reuse', async () => {
// Simple test
const response = await SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.options({ keepAlive: true })
.get();
expect(response.ok).toBeTrue();
await response.text();
});
tap.test(
'client: should support keepAlive option for connection reuse',
async () => {
// Simple test
const response = await SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.options({ keepAlive: true })
.get();
tap.test('client: should handle 429 rate limiting with default config', async () => {
// Test that handle429Backoff can be configured without errors
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.handle429Backoff();
expect(response.ok).toBeTrue();
await response.text();
},
);
const response = await client.get();
expect(response.status).toEqual(200);
// Consume the body to prevent socket hanging
await response.text();
});
tap.test(
'client: should handle 429 rate limiting with default config',
async () => {
// Test that handle429Backoff can be configured without errors
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.handle429Backoff();
const response = await client.get();
expect(response.status).toEqual(200);
// Consume the body to prevent socket hanging
await response.text();
},
);
tap.test('client: should handle 429 with custom config', async () => {
let rateLimitCallbackCalled = false;
@@ -139,65 +148,74 @@ tap.test('client: should handle 429 with custom config', async () => {
rateLimitCallbackCalled = true;
attemptCount = attempt;
waitTimeReceived = waitTime;
}
},
});
const response = await client.get();
expect(response.status).toEqual(200);
// The callback should not have been called for a 200 response
expect(rateLimitCallbackCalled).toBeFalse();
// Consume the body to prevent socket hanging
await response.text();
});
tap.test('client: should respect Retry-After header format (seconds)', async () => {
// Test the configuration works - actual 429 testing would require a mock server
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.handle429Backoff({
maxRetries: 1,
respectRetryAfter: true
});
tap.test(
'client: should respect Retry-After header format (seconds)',
async () => {
// Test the configuration works - actual 429 testing would require a mock server
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.handle429Backoff({
maxRetries: 1,
respectRetryAfter: true,
});
const response = await client.get();
expect(response.ok).toBeTrue();
// Consume the body to prevent socket hanging
await response.text();
});
const response = await client.get();
expect(response.ok).toBeTrue();
tap.test('client: should handle rate limiting with exponential backoff', async () => {
// Test exponential backoff configuration
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.handle429Backoff({
maxRetries: 3,
fallbackDelay: 100,
backoffFactor: 2,
maxWaitTime: 1000
});
// Consume the body to prevent socket hanging
await response.text();
},
);
const response = await client.get();
expect(response.status).toEqual(200);
// Consume the body to prevent socket hanging
await response.text();
});
tap.test(
'client: should handle rate limiting with exponential backoff',
async () => {
// Test exponential backoff configuration
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.handle429Backoff({
maxRetries: 3,
fallbackDelay: 100,
backoffFactor: 2,
maxWaitTime: 1000,
});
tap.test('client: should not retry non-429 errors with rate limit handler', async () => {
// Test that 404 errors are not retried by rate limit handler
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/999999')
.handle429Backoff();
const response = await client.get();
expect(response.status).toEqual(200);
const response = await client.get();
expect(response.status).toEqual(404);
expect(response.ok).toBeFalse();
// Consume the body to prevent socket hanging
await response.text();
});
// Consume the body to prevent socket hanging
await response.text();
},
);
tap.test(
'client: should not retry non-429 errors with rate limit handler',
async () => {
// Test that 404 errors are not retried by rate limit handler
const client = SmartRequest.create()
.url('https://jsonplaceholder.typicode.com/posts/999999')
.handle429Backoff();
const response = await client.get();
expect(response.status).toEqual(404);
expect(response.ok).toBeFalse();
// Consume the body to prevent socket hanging
await response.text();
},
);
tap.start();

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartrequest',
version: '2.1.0',
version: '4.2.2',
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
}

View File

@@ -1,6 +1,10 @@
import { type CoreResponse } from '../../core/index.js';
import type { ICoreResponse } from '../../core_base/types.js';
import { type TPaginationConfig, PaginationStrategy, type TPaginatedResponse } from '../types/pagination.js';
import {
type TPaginationConfig,
PaginationStrategy,
type TPaginatedResponse,
} from '../types/pagination.js';
/**
* Creates a paginated response from a regular response
@@ -9,15 +13,17 @@ export async function createPaginatedResponse<T>(
response: ICoreResponse<any>,
paginationConfig: TPaginationConfig,
queryParams: Record<string, string>,
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
fetchNextPage: (
params: Record<string, string>,
) => Promise<TPaginatedResponse<T>>,
): Promise<TPaginatedResponse<T>> {
// Parse response body first
const body = await response.json() as any;
const body = (await response.json()) as any;
// Default to response.body for items if response is JSON
let items: T[] = Array.isArray(body)
? body
: (body?.items || body?.data || body?.results || []);
: body?.items || body?.data || body?.results || [];
let hasNextPage = false;
let nextPageParams: Record<string, string> = {};
@@ -26,8 +32,14 @@ export async function createPaginatedResponse<T>(
switch (paginationConfig.strategy) {
case PaginationStrategy.OFFSET: {
const config = paginationConfig;
const currentPage = parseInt(queryParams[config.pageParam || 'page'] || String(config.startPage || 1));
const limit = parseInt(queryParams[config.limitParam || 'limit'] || String(config.pageSize || 20));
const currentPage = parseInt(
queryParams[config.pageParam || 'page'] ||
String(config.startPage || 1),
);
const limit = parseInt(
queryParams[config.limitParam || 'limit'] ||
String(config.pageSize || 20),
);
const total = getValueByPath(body, config.totalPath || 'total') || 0;
hasNextPage = currentPage * limit < total;
@@ -35,7 +47,7 @@ export async function createPaginatedResponse<T>(
if (hasNextPage) {
nextPageParams = {
...queryParams,
[config.pageParam || 'page']: String(currentPage + 1)
[config.pageParam || 'page']: String(currentPage + 1),
};
}
break;
@@ -43,7 +55,10 @@ export async function createPaginatedResponse<T>(
case PaginationStrategy.CURSOR: {
const config = paginationConfig;
const nextCursor = getValueByPath(body, config.cursorPath || 'nextCursor');
const nextCursor = getValueByPath(
body,
config.cursorPath || 'nextCursor',
);
const hasMore = getValueByPath(body, config.hasMorePath || 'hasMore');
hasNextPage = !!nextCursor || !!hasMore;
@@ -51,7 +66,7 @@ export async function createPaginatedResponse<T>(
if (hasNextPage && nextCursor) {
nextPageParams = {
...queryParams,
[config.cursorParam || 'cursor']: nextCursor
[config.cursorParam || 'cursor']: nextCursor,
};
}
break;
@@ -60,7 +75,9 @@ export async function createPaginatedResponse<T>(
case PaginationStrategy.LINK_HEADER: {
const linkHeader = response.headers['link'] || '';
// Handle both string and string[] types for the link header
const headerValue = Array.isArray(linkHeader) ? linkHeader[0] : linkHeader;
const headerValue = Array.isArray(linkHeader)
? linkHeader[0]
: linkHeader;
const links = parseLinkHeader(headerValue);
hasNextPage = !!links.next;
@@ -100,7 +117,13 @@ export async function createPaginatedResponse<T>(
// Create a function to fetch all remaining pages
const getAllPages = async (): Promise<T[]> => {
const allItems = [...items];
let currentPage: TPaginatedResponse<T> = { items, hasNextPage, getNextPage, getAllPages, response };
let currentPage: TPaginatedResponse<T> = {
items,
hasNextPage,
getNextPage,
getAllPages,
response,
};
while (currentPage.hasNextPage) {
try {
@@ -119,7 +142,7 @@ export async function createPaginatedResponse<T>(
hasNextPage,
getNextPage,
getAllPages,
response
response,
};
}
@@ -166,11 +189,15 @@ export function getValueByPath(obj: any, path?: string): any {
let current = obj;
for (const key of keys) {
if (current === null || current === undefined || typeof current !== 'object') {
if (
current === null ||
current === undefined ||
typeof current !== 'object'
) {
return undefined;
}
current = current[key];
}
return current;
}
}

View File

@@ -5,15 +5,22 @@ export { SmartRequest } from './smartrequest.js';
export { CoreResponse } from '../core/index.js';
// Export types
export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig, RateLimitConfig } from './types/common.js';
export {
export type {
HttpMethod,
ResponseType,
FormField,
RetryConfig,
TimeoutConfig,
RateLimitConfig,
} from './types/common.js';
export {
PaginationStrategy,
type TPaginationConfig as PaginationConfig,
type OffsetPaginationConfig,
type CursorPaginationConfig,
type LinkPaginationConfig,
type CustomPaginationConfig,
type TPaginatedResponse as PaginatedResponse
type TPaginatedResponse as PaginatedResponse,
} from './types/pagination.js';
// Convenience factory functions
@@ -45,4 +52,4 @@ export function createBinaryClient<T = any>() {
*/
export function createStreamClient() {
return SmartRequest.create().accept('stream');
}
}

View File

@@ -1,6 +1,4 @@
// plugins for client module
import FormData from 'form-data';
export {
FormData as formData
};
export { FormData as formData };

View File

@@ -3,14 +3,19 @@ import type { ICoreResponse } from '../core_base/types.js';
import * as plugins from './plugins.js';
import type { ICoreRequestOptions } from '../core_base/types.js';
import type { HttpMethod, ResponseType, FormField, RateLimitConfig } from './types/common.js';
import type {
HttpMethod,
ResponseType,
FormField,
RateLimitConfig,
} from './types/common.js';
import {
type TPaginationConfig,
PaginationStrategy,
type OffsetPaginationConfig,
type CursorPaginationConfig,
type CustomPaginationConfig,
type TPaginatedResponse
type TPaginatedResponse,
} from './types/pagination.js';
import { createPaginatedResponse } from './features/pagination.js';
@@ -22,21 +27,21 @@ import { createPaginatedResponse } from './features/pagination.js';
function parseRetryAfter(retryAfter: string | string[]): number {
// Handle array of values (take first)
const value = Array.isArray(retryAfter) ? retryAfter[0] : retryAfter;
if (!value) return 0;
// Try to parse as seconds (number)
const seconds = parseInt(value, 10);
if (!isNaN(seconds)) {
return seconds * 1000;
}
// Try to parse as HTTP date
const retryDate = new Date(value);
if (!isNaN(retryDate.getTime())) {
return Math.max(0, retryDate.getTime() - Date.now());
}
return 0;
}
@@ -96,7 +101,7 @@ export class SmartRequest<T = any> {
if (Buffer.isBuffer(item.value)) {
form.append(item.name, item.value, {
filename: item.filename || 'file',
contentType: item.contentType || 'application/octet-stream'
contentType: item.contentType || 'application/octet-stream',
});
} else {
form.append(item.name, item.value);
@@ -109,7 +114,7 @@ export class SmartRequest<T = any> {
this._options.headers = {
...this._options.headers,
...form.getHeaders()
...form.getHeaders(),
};
this._options.requestBody = form;
@@ -143,7 +148,7 @@ export class SmartRequest<T = any> {
maxWaitTime: config?.maxWaitTime ?? 60000,
fallbackDelay: config?.fallbackDelay ?? 1000,
backoffFactor: config?.backoffFactor ?? 2,
onRateLimit: config?.onRateLimit
onRateLimit: config?.onRateLimit,
};
return this;
}
@@ -157,7 +162,7 @@ export class SmartRequest<T = any> {
}
this._options.headers = {
...this._options.headers,
...headers
...headers,
};
return this;
}
@@ -179,7 +184,7 @@ export class SmartRequest<T = any> {
query(params: Record<string, string>): this {
this._queryParams = {
...this._queryParams,
...params
...params,
};
return this;
}
@@ -190,7 +195,7 @@ export class SmartRequest<T = any> {
options(options: Partial<ICoreRequestOptions>): this {
this._options = {
...this._options,
...options
...options,
};
return this;
}
@@ -210,12 +215,12 @@ export class SmartRequest<T = any> {
accept(type: ResponseType): this {
// Map response types to Accept header values
const acceptHeaders: Record<ResponseType, string> = {
'json': 'application/json',
'text': 'text/plain',
'binary': 'application/octet-stream',
'stream': '*/*'
json: 'application/json',
text: 'text/plain',
binary: 'application/octet-stream',
stream: '*/*',
};
return this.header('Accept', acceptHeaders[type]);
}
@@ -230,20 +235,26 @@ export class SmartRequest<T = any> {
/**
* Configure offset-based pagination (page & limit)
*/
withOffsetPagination(config: Omit<OffsetPaginationConfig, 'strategy'> = {}): this {
withOffsetPagination(
config: Omit<OffsetPaginationConfig, 'strategy'> = {},
): this {
this._paginationConfig = {
strategy: PaginationStrategy.OFFSET,
pageParam: config.pageParam || 'page',
limitParam: config.limitParam || 'limit',
startPage: config.startPage || 1,
pageSize: config.pageSize || 20,
totalPath: config.totalPath || 'total'
totalPath: config.totalPath || 'total',
};
// Add initial pagination parameters
this.query({
[this._paginationConfig.pageParam]: String(this._paginationConfig.startPage),
[this._paginationConfig.limitParam]: String(this._paginationConfig.pageSize)
[this._paginationConfig.pageParam]: String(
this._paginationConfig.startPage,
),
[this._paginationConfig.limitParam]: String(
this._paginationConfig.pageSize,
),
});
return this;
@@ -252,12 +263,14 @@ export class SmartRequest<T = any> {
/**
* Configure cursor-based pagination
*/
withCursorPagination(config: Omit<CursorPaginationConfig, 'strategy'> = {}): this {
withCursorPagination(
config: Omit<CursorPaginationConfig, 'strategy'> = {},
): this {
this._paginationConfig = {
strategy: PaginationStrategy.CURSOR,
cursorParam: config.cursorParam || 'cursor',
cursorPath: config.cursorPath || 'nextCursor',
hasMorePath: config.hasMorePath || 'hasMore'
hasMorePath: config.hasMorePath || 'hasMore',
};
return this;
}
@@ -267,7 +280,7 @@ export class SmartRequest<T = any> {
*/
withLinkPagination(): this {
this._paginationConfig = {
strategy: PaginationStrategy.LINK_HEADER
strategy: PaginationStrategy.LINK_HEADER,
};
return this;
}
@@ -279,7 +292,7 @@ export class SmartRequest<T = any> {
this._paginationConfig = {
strategy: PaginationStrategy.CUSTOM,
hasNextPage: config.hasNextPage,
getNextPageParams: config.getNextPageParams
getNextPageParams: config.getNextPageParams,
};
return this;
}
@@ -324,7 +337,9 @@ export class SmartRequest<T = any> {
*/
async getPaginated<ItemType = T>(): Promise<TPaginatedResponse<ItemType>> {
if (!this._paginationConfig) {
throw new Error('Pagination not configured. Call one of the pagination methods first.');
throw new Error(
'Pagination not configured. Call one of the pagination methods first.',
);
}
// Default to GET if no method specified
@@ -345,7 +360,7 @@ export class SmartRequest<T = any> {
nextClient._queryParams = nextPageParams;
return nextClient.getPaginated<ItemType>();
}
},
);
}
@@ -375,8 +390,8 @@ export class SmartRequest<T = any> {
for (let attempt = 0; attempt <= this._retries; attempt++) {
try {
const request = new CoreRequest(this._url, this._options as any);
const response = await request.fire() as ICoreResponse<R>;
const response = (await request.fire()) as ICoreResponse<R>;
// Check for 429 status if rate limit handling is enabled
if (this._rateLimitConfig && response.status === 429) {
if (rateLimitAttempt >= this._rateLimitConfig.maxRetries) {
@@ -385,18 +400,22 @@ export class SmartRequest<T = any> {
}
let waitTime: number;
if (this._rateLimitConfig.respectRetryAfter && response.headers['retry-after']) {
if (
this._rateLimitConfig.respectRetryAfter &&
response.headers['retry-after']
) {
// Parse Retry-After header
waitTime = parseRetryAfter(response.headers['retry-after']);
// Cap wait time to maxWaitTime
waitTime = Math.min(waitTime, this._rateLimitConfig.maxWaitTime);
} else {
// Use exponential backoff
waitTime = Math.min(
this._rateLimitConfig.fallbackDelay * Math.pow(this._rateLimitConfig.backoffFactor, rateLimitAttempt),
this._rateLimitConfig.maxWaitTime
this._rateLimitConfig.fallbackDelay *
Math.pow(this._rateLimitConfig.backoffFactor, rateLimitAttempt),
this._rateLimitConfig.maxWaitTime,
);
}
@@ -406,14 +425,14 @@ export class SmartRequest<T = any> {
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise((resolve) => setTimeout(resolve, waitTime));
rateLimitAttempt++;
// Decrement attempt to retry this attempt
attempt--;
continue;
}
// Success or non-429 error response
return response;
} catch (error) {
@@ -425,11 +444,11 @@ export class SmartRequest<T = any> {
}
// Otherwise, wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
// This should never be reached due to the throw in the loop above
throw lastError;
}
}
}

View File

@@ -1,7 +1,14 @@
/**
* HTTP Methods supported by the client
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
export type HttpMethod =
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH'
| 'HEAD'
| 'OPTIONS';
/**
* Response types supported by the client
@@ -30,11 +37,11 @@ export interface UrlEncodedField {
* Retry configuration
*/
export interface RetryConfig {
attempts: number; // Number of retry attempts
initialDelay?: number; // Initial delay in ms
maxDelay?: number; // Maximum delay in ms
factor?: number; // Backoff factor
statusCodes?: number[]; // Status codes to retry on
attempts: number; // Number of retry attempts
initialDelay?: number; // Initial delay in ms
maxDelay?: number; // Maximum delay in ms
factor?: number; // Backoff factor
statusCodes?: number[]; // Status codes to retry on
shouldRetry?: (error: Error, attemptCount: number) => boolean;
}
@@ -42,20 +49,20 @@ export interface RetryConfig {
* Timeout configuration
*/
export interface TimeoutConfig {
request?: number; // Overall request timeout in ms
connection?: number; // Connection timeout in ms
socket?: number; // Socket idle timeout in ms
response?: number; // Response timeout in ms
request?: number; // Overall request timeout in ms
connection?: number; // Connection timeout in ms
socket?: number; // Socket idle timeout in ms
response?: number; // Response timeout in ms
}
/**
* Rate limit configuration for handling 429 responses
*/
export interface RateLimitConfig {
maxRetries?: number; // Maximum number of retries (default: 3)
respectRetryAfter?: boolean; // Respect Retry-After header (default: true)
maxWaitTime?: number; // Max wait time in ms (default: 60000)
fallbackDelay?: number; // Delay when no Retry-After header (default: 1000)
backoffFactor?: number; // Exponential backoff factor (default: 2)
maxRetries?: number; // Maximum number of retries (default: 3)
respectRetryAfter?: boolean; // Respect Retry-After header (default: true)
maxWaitTime?: number; // Max wait time in ms (default: 60000)
fallbackDelay?: number; // Delay when no Retry-After header (default: 1000)
backoffFactor?: number; // Exponential backoff factor (default: 2)
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
}
}

View File

@@ -5,10 +5,10 @@ import type { ICoreResponse } from '../../core_base/types.js';
* Pagination strategy options
*/
export enum PaginationStrategy {
OFFSET = 'offset', // Uses page & limit parameters
CURSOR = 'cursor', // Uses a cursor/token for next page
LINK_HEADER = 'link', // Uses Link headers
CUSTOM = 'custom' // Uses a custom pagination handler
OFFSET = 'offset', // Uses page & limit parameters
CURSOR = 'cursor', // Uses a cursor/token for next page
LINK_HEADER = 'link', // Uses Link headers
CUSTOM = 'custom', // Uses a custom pagination handler
}
/**
@@ -16,11 +16,11 @@ export enum PaginationStrategy {
*/
export interface OffsetPaginationConfig {
strategy: PaginationStrategy.OFFSET;
pageParam?: string; // Parameter name for page number (default: "page")
limitParam?: string; // Parameter name for page size (default: "limit")
startPage?: number; // Starting page number (default: 1)
pageSize?: number; // Number of items per page (default: 20)
totalPath?: string; // JSON path to total item count (default: "total")
pageParam?: string; // Parameter name for page number (default: "page")
limitParam?: string; // Parameter name for page size (default: "limit")
startPage?: number; // Starting page number (default: 1)
pageSize?: number; // Number of items per page (default: 20)
totalPath?: string; // JSON path to total item count (default: "total")
}
/**
@@ -28,9 +28,9 @@ export interface OffsetPaginationConfig {
*/
export interface CursorPaginationConfig {
strategy: PaginationStrategy.CURSOR;
cursorParam?: string; // Parameter name for cursor (default: "cursor")
cursorPath?: string; // JSON path to next cursor (default: "nextCursor")
hasMorePath?: string; // JSON path to check if more items exist (default: "hasMore")
cursorParam?: string; // Parameter name for cursor (default: "cursor")
cursorPath?: string; // JSON path to next cursor (default: "nextCursor")
hasMorePath?: string; // JSON path to check if more items exist (default: "hasMore")
}
/**
@@ -47,21 +47,28 @@ export interface LinkPaginationConfig {
export interface CustomPaginationConfig {
strategy: PaginationStrategy.CUSTOM;
hasNextPage: (response: ICoreResponse<any>) => boolean;
getNextPageParams: (response: ICoreResponse<any>, currentParams: Record<string, string>) => Record<string, string>;
getNextPageParams: (
response: ICoreResponse<any>,
currentParams: Record<string, string>,
) => Record<string, string>;
}
/**
* Union type of all pagination configurations
*/
export type TPaginationConfig = OffsetPaginationConfig | CursorPaginationConfig | LinkPaginationConfig | CustomPaginationConfig;
export type TPaginationConfig =
| OffsetPaginationConfig
| CursorPaginationConfig
| LinkPaginationConfig
| CustomPaginationConfig;
/**
* Interface for a paginated response
*/
export interface TPaginatedResponse<T> {
items: T[]; // Current page items
hasNextPage: boolean; // Whether there are more pages
getNextPage: () => Promise<TPaginatedResponse<T>>; // Function to get the next page
getAllPages: () => Promise<T[]>; // Function to get all remaining pages and combine
response: ICoreResponse<any>; // Original response
}
items: T[]; // Current page items
hasNextPage: boolean; // Whether there are more pages
getNextPage: () => Promise<TPaginatedResponse<T>>; // Function to get the next page
getAllPages: () => Promise<T[]>; // Function to get all remaining pages and combine
response: ICoreResponse<any>; // Original response
}

View File

@@ -13,8 +13,8 @@ if (smartenvInstance.isNode) {
// In Node.js, load the node implementation
const modulePath = plugins.smartpath.join(
plugins.smartpath.dirname(import.meta.url),
'../core_node/index.js'
)
'../core_node/index.js',
);
console.log(modulePath);
const impl = await smartenvInstance.getSafeNodeModule(modulePath);
CoreRequest = impl.CoreRequest;

View File

@@ -1,4 +1,4 @@
// Core base exports - abstract classes and platform-agnostic types
export * from './types.js';
export * from './request.js';
export * from './response.js';
export * from './response.js';

View File

@@ -3,7 +3,10 @@ import * as types from './types.js';
/**
* Abstract Core Request class that defines the interface for all HTTP/HTTPS requests
*/
export abstract class CoreRequest<TOptions extends types.ICoreRequestOptions = types.ICoreRequestOptions, TResponse = any> {
export abstract class CoreRequest<
TOptions extends types.ICoreRequestOptions = types.ICoreRequestOptions,
TResponse = any,
> {
/**
* Tests if a URL is a unix socket
*/
@@ -41,5 +44,4 @@ export abstract class CoreRequest<TOptions extends types.ICoreRequestOptions = t
* Fire the request and return the raw response (platform-specific)
*/
abstract fireCore(): Promise<any>;
}
}

View File

@@ -37,9 +37,9 @@ export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> {
* Get response as ArrayBuffer
*/
abstract arrayBuffer(): Promise<ArrayBuffer>;
/**
* Get response as a web-style ReadableStream
*/
abstract stream(): ReadableStream<Uint8Array> | null;
}
}

View File

@@ -1,7 +1,14 @@
/**
* HTTP Methods supported
*/
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
export type THttpMethod =
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH'
| 'HEAD'
| 'OPTIONS';
/**
* Response types supported
@@ -39,14 +46,14 @@ export interface ICoreRequestOptions {
timeout?: number;
hardDataCuttingTimeout?: number;
autoDrain?: boolean; // Auto-drain unconsumed responses (Node.js only, default: true)
// Node.js specific options (ignored in fetch implementation)
agent?: any;
socketPath?: string;
hostname?: string;
port?: number;
path?: string;
// Fetch API specific options (ignored in Node.js implementation)
credentials?: RequestCredentials;
mode?: RequestMode;
@@ -73,10 +80,10 @@ export interface ICoreResponse<T = any> {
statusText: string;
headers: Headers;
url: string;
// Methods
json(): Promise<T>;
text(): Promise<string>;
arrayBuffer(): Promise<ArrayBuffer>;
stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream
}
}

View File

@@ -1,3 +1,3 @@
// Core fetch exports - native fetch implementation
export * from './response.js';
export { CoreRequest } from './request.js';
export { CoreRequest } from './request.js';

View File

@@ -5,13 +5,18 @@ import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
/**
* Fetch-based implementation of Core Request class
*/
export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions, CoreResponse> {
export class CoreRequest extends AbstractCoreRequest<
types.ICoreRequestOptions,
CoreResponse
> {
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');
throw new Error(
'Node.js specific options (agent, socketPath) are not supported in browser/fetch implementation',
);
}
}
@@ -19,7 +24,10 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
* Build the full URL with query parameters
*/
private buildUrl(): string {
if (!this.options.queryParams || Object.keys(this.options.queryParams).length === 0) {
if (
!this.options.queryParams ||
Object.keys(this.options.queryParams).length === 0
) {
return this.url;
}
@@ -50,11 +58,13 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
// Handle request body
if (this.options.requestBody !== undefined) {
if (typeof this.options.requestBody === 'string' ||
this.options.requestBody instanceof ArrayBuffer ||
this.options.requestBody instanceof FormData ||
this.options.requestBody instanceof URLSearchParams ||
this.options.requestBody instanceof ReadableStream) {
if (
typeof this.options.requestBody === 'string' ||
this.options.requestBody instanceof ArrayBuffer ||
this.options.requestBody instanceof FormData ||
this.options.requestBody instanceof URLSearchParams ||
this.options.requestBody instanceof ReadableStream
) {
fetchOptions.body = this.options.requestBody;
} else {
// Convert objects to JSON
@@ -66,7 +76,10 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
if (!fetchOptions.headers.has('Content-Type')) {
fetchOptions.headers.set('Content-Type', 'application/json');
}
} else if (typeof fetchOptions.headers === 'object' && !Array.isArray(fetchOptions.headers)) {
} 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';
@@ -77,7 +90,8 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
// Handle timeout
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
const timeout = this.options.hardDataCuttingTimeout || this.options.timeout;
const timeout =
this.options.hardDataCuttingTimeout || this.options.timeout;
const controller = new AbortController();
setTimeout(() => controller.abort(), timeout);
fetchOptions.signal = controller.signal;
@@ -100,7 +114,7 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
async fireCore(): Promise<Response> {
const url = this.buildUrl();
const options = this.buildFetchOptions();
try {
const response = await fetch(url, options);
return response;
@@ -117,7 +131,7 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
*/
static async create(
url: string,
options: types.ICoreRequestOptions = {}
options: types.ICoreRequestOptions = {},
): Promise<CoreResponse> {
const request = new CoreRequest(url, options);
return request.fire();
@@ -128,4 +142,4 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
* Convenience exports for backward compatibility
*/
export const isUnixSocket = CoreRequest.isUnixSocket;
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;

View File

@@ -4,7 +4,10 @@ import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
/**
* Fetch-based implementation of Core Response class
*/
export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements types.IFetchResponse<T> {
export class CoreResponse<T = any>
extends AbstractCoreResponse<T>
implements types.IFetchResponse<T>
{
private response: Response;
private responseClone: Response;
@@ -20,12 +23,12 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
// Clone the response so we can read the body multiple times if needed
this.response = response;
this.responseClone = response.clone();
this.ok = response.ok;
this.status = response.status;
this.statusText = response.statusText;
this.url = response.url;
// Convert Headers to plain object
this.headers = {};
response.headers.forEach((value, key) => {
@@ -73,13 +76,15 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
* Node.js stream method - not available in browser
*/
streamNode(): never {
throw new Error('streamNode() is not available in browser/fetch environment. Use stream() for web-style ReadableStream.');
throw new Error(
'streamNode() is not available in browser/fetch environment. Use stream() for web-style ReadableStream.',
);
}
/**
* Get the raw Response object
*/
raw(): Response {
return this.responseClone;
}
}
}

View File

@@ -9,7 +9,7 @@ export * from '../core_base/types.js';
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

@@ -1,3 +1,3 @@
// Core exports
export * from './response.js';
export { CoreRequest } from './request.js';
export { CoreRequest } from './request.js';

View File

@@ -17,4 +17,4 @@ import { HttpAgent, HttpsAgent } from 'agentkeepalive';
const agentkeepalive = { HttpAgent, HttpsAgent };
import formData from 'form-data';
export { agentkeepalive, formData };
export { agentkeepalive, formData };

View File

@@ -29,21 +29,33 @@ const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
/**
* Node.js implementation of Core Request class that handles all HTTP/HTTPS requests
*/
export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions, CoreResponse> {
export class CoreRequest extends AbstractCoreRequest<
types.ICoreRequestOptions,
CoreResponse
> {
private requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null;
constructor(
url: string,
options: types.ICoreRequestOptions = {},
requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null
requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null,
) {
super(url, options);
this.requestDataFunc = requestDataFunc;
// Check for unsupported fetch-specific options
if (options.credentials || options.mode || options.cache || options.redirect ||
options.referrer || options.referrerPolicy || options.integrity) {
throw new Error('Fetch API specific options (credentials, mode, cache, redirect, referrer, referrerPolicy, integrity) are not supported in Node.js implementation');
if (
options.credentials ||
options.mode ||
options.cache ||
options.redirect ||
options.referrer ||
options.referrerPolicy ||
options.integrity
) {
throw new Error(
'Fetch API specific options (credentials, mode, cache, redirect, referrer, referrerPolicy, integrity) are not supported in Node.js implementation',
);
}
}
@@ -65,7 +77,7 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.url, {
searchParams: this.options.queryParams || {},
});
this.options.hostname = parsedUrl.hostname;
if (parsedUrl.port) {
this.options.port = parseInt(parsedUrl.port, 10);
@@ -74,7 +86,9 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
// Handle unix socket URLs
if (CoreRequest.isUnixSocket(this.url)) {
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(this.options.path);
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(
this.options.path,
);
this.options.socketPath = socketPath;
this.options.path = path;
}
@@ -83,18 +97,25 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
if (!this.options.agent) {
// Only use keep-alive agents if explicitly requested
if (this.options.keepAlive === true) {
this.options.agent = parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent;
this.options.agent =
parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent;
} else if (this.options.keepAlive === false) {
this.options.agent = parsedUrl.protocol === 'https:' ? httpsAgentKeepAliveFalse : httpAgentKeepAliveFalse;
this.options.agent =
parsedUrl.protocol === 'https:'
? httpsAgentKeepAliveFalse
: httpAgentKeepAliveFalse;
}
// If keepAlive is undefined, don't set any agent (more fetch-like behavior)
}
// Determine request module
const requestModule = parsedUrl.protocol === 'https:' ? plugins.https : plugins.http;
const requestModule =
parsedUrl.protocol === 'https:' ? plugins.https : plugins.http;
if (!requestModule) {
throw new Error(`The request to ${this.url} is missing a viable protocol. Must be http or https`);
throw new Error(
`The request to ${this.url} is missing a viable protocol. Must be http or https`,
);
}
// Perform the request
@@ -119,11 +140,12 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
});
} else {
// Write body as-is - caller is responsible for serialization
const bodyData = typeof this.options.requestBody === 'string'
? this.options.requestBody
: this.options.requestBody instanceof Buffer
const bodyData =
typeof this.options.requestBody === 'string'
? this.options.requestBody
: JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility
: this.options.requestBody instanceof Buffer
? this.options.requestBody
: JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility
request.write(bodyData);
request.end();
}
@@ -155,7 +177,7 @@ export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions,
*/
static async create(
url: string,
options: types.ICoreRequestOptions = {}
options: types.ICoreRequestOptions = {},
): Promise<CoreResponse> {
const request = new CoreRequest(url, options);
return request.fire();

View File

@@ -5,7 +5,10 @@ import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
/**
* Node.js implementation of Core Response class that provides a fetch-like API
*/
export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements types.INodeResponse<T> {
export class CoreResponse<T = any>
extends AbstractCoreResponse<T>
implements types.INodeResponse<T>
{
private incomingMessage: plugins.http.IncomingMessage;
private bodyBufferPromise: Promise<Buffer> | null = null;
private _autoDrainTimeout: NodeJS.Immediate | null = null;
@@ -17,7 +20,11 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
public readonly headers: plugins.http.IncomingHttpHeaders;
public readonly url: string;
constructor(incomingMessage: plugins.http.IncomingMessage, url: string, options: types.ICoreRequestOptions = {}) {
constructor(
incomingMessage: plugins.http.IncomingMessage,
url: string,
options: types.ICoreRequestOptions = {},
) {
super();
this.incomingMessage = incomingMessage;
this.url = url;
@@ -25,14 +32,16 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
this.statusText = incomingMessage.statusMessage || '';
this.ok = this.status >= 200 && this.status < 300;
this.headers = incomingMessage.headers;
// Auto-drain unconsumed streams to prevent socket hanging
// This prevents keep-alive sockets from timing out when response bodies aren't consumed
// Default to true if not specified
if (options.autoDrain !== false) {
this._autoDrainTimeout = setImmediate(() => {
if (!this.consumed && !this.incomingMessage.readableEnded) {
console.log(`Auto-draining unconsumed response body for ${this.url} (status: ${this.status})`);
console.log(
`Auto-draining unconsumed response body for ${this.url} (status: ${this.status})`,
);
this.incomingMessage.resume(); // Drain without processing
}
});
@@ -48,7 +57,7 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
clearImmediate(this._autoDrainTimeout);
this._autoDrainTimeout = null;
}
super.ensureNotConsumed();
}
@@ -57,22 +66,22 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
*/
private async collectBody(): Promise<Buffer> {
this.ensureNotConsumed();
if (this.bodyBufferPromise) {
return this.bodyBufferPromise;
}
this.bodyBufferPromise = new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
this.incomingMessage.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
this.incomingMessage.on('end', () => {
resolve(Buffer.concat(chunks));
});
this.incomingMessage.on('error', reject);
});
@@ -85,7 +94,7 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
async json(): Promise<T> {
const buffer = await this.collectBody();
const text = buffer.toString('utf-8');
try {
return JSON.parse(text);
} catch (error) {
@@ -106,7 +115,10 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
*/
async arrayBuffer(): Promise<ArrayBuffer> {
const buffer = await this.collectBody();
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
);
}
/**
@@ -114,13 +126,13 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
*/
stream(): ReadableStream<Uint8Array> | null {
this.ensureNotConsumed();
// Convert Node.js stream to web stream
// In Node.js 16.5+ we can use Readable.toWeb()
if (this.incomingMessage.readableEnded || this.incomingMessage.destroyed) {
return null;
}
// Create a web ReadableStream from the Node.js stream
const nodeStream = this.incomingMessage;
return new ReadableStream<Uint8Array>({
@@ -128,22 +140,22 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
nodeStream.on('data', (chunk) => {
controller.enqueue(new Uint8Array(chunk));
});
nodeStream.on('end', () => {
controller.close();
});
nodeStream.on('error', (err) => {
controller.error(err);
});
},
cancel() {
nodeStream.destroy();
}
},
});
}
/**
* Get response as a Node.js readable stream
*/
@@ -158,5 +170,4 @@ export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements ty
raw(): plugins.http.IncomingMessage {
return this.incomingMessage;
}
}
}

View File

@@ -7,7 +7,8 @@ export * from '../core_base/types.js';
/**
* Extended IncomingMessage with body property (legacy compatibility)
*/
export interface IExtendedIncomingMessage<T = any> extends plugins.http.IncomingMessage {
export interface IExtendedIncomingMessage<T = any>
extends plugins.http.IncomingMessage {
body: T;
}
@@ -17,7 +18,7 @@ export interface IExtendedIncomingMessage<T = any> extends plugins.http.Incoming
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;
}
}

View File

@@ -7,4 +7,4 @@ export type { ICoreRequestOptions, ICoreResponse } from './core_base/types.js';
// Default export for easier importing
import { SmartRequest } from './client/smartrequest.js';
export default SmartRequest;
export default SmartRequest;

View File

@@ -6,9 +6,9 @@
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": [
"dist_*/**/*.d.ts"
]
"exclude": ["dist_*/**/*.d.ts"]
}