Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
b4769e7feb | |||
4cbca08f43 | |||
cf24bf94b9 | |||
3e24f1c5a8 | |||
2dc82bd730 | |||
8e75047d1f | |||
eb2ccd8d9f | |||
bc99aa3569 | |||
94bf23ad55 | |||
ea54a8aeda | |||
18d8ab0278 | |||
b8d707b363 | |||
7dcc5f3fe2 | |||
8f5c88b47e | |||
28a56b87bc | |||
d627bc870e | |||
2cded974a8 | |||
31c25c8333 | |||
01bbfa4a06 | |||
0ebd47d1b2 |
61
changelog.md
61
changelog.md
@@ -1,5 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
- Added `RateLimitConfig` interface with customizable retry behavior
|
||||
- Optional callback for monitoring rate limit events
|
||||
- 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:
|
||||
- Added abstraction layer with `core_base` containing abstract classes
|
||||
- Split implementations into `core_node` (Node.js) and `core_fetch` (browser)
|
||||
- Dynamic implementation selection based on environment
|
||||
- Response streaming API changes:
|
||||
- `stream()` now always returns web-style `ReadableStream<Uint8Array>`
|
||||
- Added `streamNode()` for Node.js streams (throws error in browser)
|
||||
- Unified type system with single `ICoreRequestOptions` interface
|
||||
- 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
|
||||
- Web Streams API support in both environments
|
||||
- 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
|
||||
|
||||
|
18
package.json
18
package.json
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "@push.rocks/smartrequest",
|
||||
"version": "3.0.0",
|
||||
"version": "4.2.0",
|
||||
"private": false,
|
||||
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||
"exports": {
|
||||
".": "./dist_ts_web/index.js",
|
||||
"./legacy": "./dist_ts/legacy/index.js"
|
||||
".": "./dist_ts/index.js",
|
||||
"./core_node": "./dist_ts/core_node/index.js",
|
||||
"./core_fetch": "./dist_ts/core_fetch/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"test": "(tstest test/ --verbose --timeout 60)",
|
||||
"build": "(tsbuild --web)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
@@ -38,16 +39,17 @@
|
||||
},
|
||||
"homepage": "https://code.foss.global/push.rocks/smartrequest",
|
||||
"dependencies": {
|
||||
"@push.rocks/smartenv": "^5.0.13",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.0.4",
|
||||
"@push.rocks/smarturl": "^3.1.0",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"form-data": "^4.0.1"
|
||||
"form-data": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.0",
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@pushrocks/tapbundle": "^5.0.8",
|
||||
"@git.zone/tstest": "^2.3.2",
|
||||
"@types/node": "^22.9.0"
|
||||
},
|
||||
"files": [
|
||||
|
4220
pnpm-lock.yaml
generated
4220
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
## Core Features
|
||||
- supports http
|
||||
- supports https
|
||||
- supports https
|
||||
- supports unix socks
|
||||
- supports formData
|
||||
- supports file uploads
|
||||
@@ -11,44 +11,69 @@
|
||||
- written in TypeScript
|
||||
- continuously updated
|
||||
- uses node native http and https modules
|
||||
- supports both Node.js and browser environments
|
||||
- used in modules like @push.rocks/smartproxy and @api.global/typedrequest
|
||||
|
||||
## Architecture Overview (as of latest refactoring)
|
||||
- The project is now structured with a clean separation between core functionality and API layers
|
||||
- Core module (ts/core/) contains the essential HTTP request logic using Node.js http/https modules
|
||||
- **Core always returns raw streams** - no parsing or body collection happens in the core request function
|
||||
- Modern API (ts/modern/) provides a fluent, chainable interface with fetch-like Response objects
|
||||
- Legacy API is maintained through a thin adapter layer for backward compatibility
|
||||
## 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
|
||||
- Legacy API has been completely removed in v3.0.0
|
||||
|
||||
## Key Components
|
||||
|
||||
### Core Module (ts/core/)
|
||||
- `request.ts`: Core HTTP/HTTPS request logic with unix socket support and keep-alive agents
|
||||
- `coreRequest()` always returns a raw Node.js IncomingMessage stream
|
||||
- No response parsing or body collection happens here
|
||||
- `response.ts`: SmartResponse class providing fetch-like API
|
||||
- Methods like `json()`, `text()`, `arrayBuffer()` handle all parsing and body collection
|
||||
- Response body is streamed and collected only when these methods are called
|
||||
### 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
|
||||
- Body can only be consumed once (throws error on second attempt)
|
||||
- `types.ts`: Core TypeScript interfaces and types
|
||||
- `plugins.ts`: Centralized dependencies
|
||||
- `types.ts`: Unified TypeScript interfaces and types
|
||||
- Single `ICoreRequestOptions` interface for all implementations
|
||||
- Implementations handle unsupported options by throwing errors
|
||||
|
||||
### Modern API
|
||||
- SmartRequestClient: Fluent API with method chaining
|
||||
- Returns SmartResponse objects with fetch-like methods
|
||||
### 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
|
||||
- `response.ts`: Node.js CoreResponse implementation
|
||||
- `stream()` method converts Node.js stream to web ReadableStream
|
||||
- `streamNode()` method returns native Node.js stream
|
||||
- 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
|
||||
- `response.ts`: Fetch-based CoreResponse implementation
|
||||
- `stream()` returns native web ReadableStream from response.body
|
||||
- `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
|
||||
|
||||
### Binary Request Handling
|
||||
- Binary requests are handled correctly when `responseType: 'binary'` is set
|
||||
- Response body is kept as Buffer without string conversion
|
||||
- No automatic transformations applied to binary data
|
||||
### 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
|
||||
|
||||
### Legacy Compatibility
|
||||
- All legacy functions (getJson, postJson, etc.) are maintained through adapter.ts
|
||||
- Legacy API returns IExtendedIncomingMessage for backward compatibility
|
||||
- Modern API can be accessed alongside legacy API
|
||||
### 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
|
||||
- Modern API tests use the new SmartResponse methods (response.json(), response.text())
|
||||
- Legacy API tests continue to use the body property directly
|
||||
- Tests use @git.zone/tstest/tapbundle for assertions
|
||||
- Separate test files for Node.js (test.node.ts) and browser (test.browser.ts)
|
||||
- Browser tests run in headless Chromium via puppeteer
|
||||
|
541
readme.md
541
readme.md
@@ -1,170 +1,53 @@
|
||||
# @push.rocks/smartrequest
|
||||
A modern HTTP/HTTPS request library for Node.js with support for form data, file uploads, JSON, binary data, streams, and unix sockets. Features both a legacy API for backward compatibility and a modern fetch-like API for new projects.
|
||||
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
|
||||
To install `@push.rocks/smartrequest`, use one of the following commands:
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @push.rocks/smartrequest --save
|
||||
|
||||
# Using pnpm
|
||||
# Using pnpm
|
||||
pnpm add @push.rocks/smartrequest
|
||||
|
||||
# Using yarn
|
||||
yarn add @push.rocks/smartrequest
|
||||
```
|
||||
|
||||
This will add `@push.rocks/smartrequest` to your project's dependencies.
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🚀 **Modern Fetch-like API** - Familiar response methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`)
|
||||
- 🔄 **Two API Styles** - Legacy function-based API and modern fluent chainable API
|
||||
- 🌐 **Unix Socket Support** - Connect to local services like Docker
|
||||
- 🌐 **Cross-Platform** - Works in both Node.js and browsers with a unified API
|
||||
- 🔌 **Unix Socket Support** - Connect to local services like Docker (Node.js only)
|
||||
- 📦 **Form Data & File Uploads** - Built-in support for multipart/form-data
|
||||
- 🔁 **Pagination Support** - Multiple strategies (offset, cursor, Link headers)
|
||||
- ⚡ **Keep-Alive Connections** - Efficient connection pooling
|
||||
- ⚡ **Keep-Alive Connections** - Efficient connection pooling in Node.js
|
||||
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
|
||||
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
|
||||
- 🔌 **Streaming Support** - Handle large files and real-time data
|
||||
- 📡 **Streaming Support** - Handle large files and real-time data
|
||||
- 🔧 **Highly Configurable** - Timeouts, retries, headers, and more
|
||||
|
||||
## Architecture
|
||||
|
||||
SmartRequest v3.0 features a multi-layer architecture that provides consistent behavior across platforms:
|
||||
|
||||
- **Core Base** - Abstract classes and unified types shared across implementations
|
||||
- **Core Node** - Node.js implementation using native http/https modules
|
||||
- **Core Fetch** - Browser implementation using the Fetch API
|
||||
- **Core** - Dynamic implementation selection based on environment
|
||||
- **Client** - High-level fluent API for everyday use
|
||||
|
||||
## Usage
|
||||
|
||||
`@push.rocks/smartrequest` is designed as a versatile, modern HTTP client library for making HTTP/HTTPS requests in Node.js environments. It provides a clean, type-safe API inspired by the native fetch API but with additional features needed for server-side applications.
|
||||
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for modern applications.
|
||||
|
||||
The library provides two distinct APIs:
|
||||
|
||||
1. **Legacy API** - Simple function-based API for quick requests and backward compatibility
|
||||
2. **Modern Fluent API** - A chainable, fetch-like API for more complex scenarios
|
||||
|
||||
Below we will cover key usage scenarios of `@push.rocks/smartrequest`, showcasing its capabilities and providing you with a solid starting point to integrate it into your projects.
|
||||
|
||||
### Import Guide
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
// Modern API (recommended for new projects)
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
|
||||
// Legacy API (for backward compatibility)
|
||||
import { getJson, postJson, request } from '@push.rocks/smartrequest/legacy';
|
||||
```
|
||||
|
||||
### Simple GET Request
|
||||
|
||||
For fetching data from a REST API or any web service that returns JSON:
|
||||
|
||||
```typescript
|
||||
import { getJson } from '@push.rocks/smartrequest/legacy';
|
||||
|
||||
async function fetchGitHubUserInfo(username: string) {
|
||||
const response = await getJson(`https://api.github.com/users/${username}`);
|
||||
console.log(response.body); // The body contains the JSON response
|
||||
}
|
||||
|
||||
fetchGitHubUserInfo('octocat');
|
||||
```
|
||||
|
||||
The `getJson` function simplifies the process of sending a GET request and parsing the JSON response.
|
||||
|
||||
### POST Requests with JSON
|
||||
|
||||
When you need to send JSON data to a server, for example, creating a new resource:
|
||||
|
||||
```typescript
|
||||
import { postJson } from '@push.rocks/smartrequest/legacy';
|
||||
|
||||
async function createTodoItem(todoDetails: { title: string; completed: boolean }) {
|
||||
const response = await postJson('https://jsonplaceholder.typicode.com/todos', {
|
||||
requestBody: todoDetails
|
||||
});
|
||||
console.log(response.body); // Log the created todo item
|
||||
}
|
||||
|
||||
createTodoItem({ title: 'Implement smartrequest', completed: false });
|
||||
```
|
||||
|
||||
`postJson` handles setting the appropriate content-type header and stringifies the JSON body.
|
||||
|
||||
### Handling Form Data and File Uploads
|
||||
|
||||
`@push.rocks/smartrequest` simplifies the process of uploading files and submitting form data to a server:
|
||||
|
||||
```typescript
|
||||
import { postFormData, IFormField } from '@push.rocks/smartrequest/legacy';
|
||||
|
||||
async function uploadProfilePicture(formDataFields: IFormField[]) {
|
||||
await postFormData('https://api.example.com/upload', {}, formDataFields);
|
||||
}
|
||||
|
||||
uploadProfilePicture([
|
||||
{ name: 'avatar', type: 'filePath', payload: './path/to/avatar.jpg', fileName: 'avatar.jpg', contentType: 'image/jpeg' },
|
||||
{ name: 'user_id', type: 'string', payload: '12345' }
|
||||
]);
|
||||
```
|
||||
|
||||
### Streaming Support
|
||||
|
||||
For cases when dealing with large datasets or streaming APIs, `@push.rocks/smartrequest` provides streaming capabilities:
|
||||
|
||||
```typescript
|
||||
import { getStream } from '@push.rocks/smartrequest/legacy';
|
||||
|
||||
async function streamLargeFile(url: string) {
|
||||
const stream = await getStream(url);
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
console.log('Received chunk of data.');
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
console.log('Stream ended.');
|
||||
});
|
||||
}
|
||||
|
||||
streamLargeFile('https://example.com/largefile');
|
||||
```
|
||||
|
||||
`getStream` allows you to handle data as it's received, which can be beneficial for performance and scalability.
|
||||
|
||||
### Advanced Options and Customization
|
||||
|
||||
`@push.rocks/smartrequest` is built to be flexible, allowing you to specify additional options to tailor requests to your needs:
|
||||
|
||||
```typescript
|
||||
import { request, ISmartRequestOptions } from '@push.rocks/smartrequest/legacy';
|
||||
|
||||
async function customRequestExample() {
|
||||
const options: ISmartRequestOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Custom-Header': 'Value'
|
||||
},
|
||||
keepAlive: true // Enables connection keep-alive
|
||||
};
|
||||
|
||||
const response = await request('https://example.com/data', options);
|
||||
console.log(response.body);
|
||||
}
|
||||
|
||||
customRequestExample();
|
||||
```
|
||||
|
||||
`request` is the underlying function that powers the simpler `getJson`, `postJson`, etc., and provides you with full control over the HTTP request.
|
||||
|
||||
## Modern Fluent API
|
||||
|
||||
In addition to the legacy API shown above, `@push.rocks/smartrequest` provides a modern, fluent API with a fetch-like response interface that offers a more chainable and TypeScript-friendly approach to making HTTP requests.
|
||||
|
||||
### Basic Usage with the Modern API
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Simple GET request
|
||||
async function fetchUserData(userId: number) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(`https://jsonplaceholder.typicode.com/users/${userId}`)
|
||||
.get();
|
||||
|
||||
@@ -175,7 +58,7 @@ async function fetchUserData(userId: number) {
|
||||
|
||||
// POST request with JSON body
|
||||
async function createPost(title: string, body: string, userId: number) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts')
|
||||
.json({ title, body, userId })
|
||||
.post();
|
||||
@@ -185,13 +68,34 @@ async function createPost(title: string, body: string, userId: number) {
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Core API Usage
|
||||
|
||||
For advanced use cases, you can use the Core API directly:
|
||||
|
||||
```typescript
|
||||
import { CoreRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function directCoreRequest() {
|
||||
const request = new CoreRequest('https://api.example.com/data', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const response = await request.fire();
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Headers and Query Parameters
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function searchRepositories(query: string, perPage: number = 10) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.github.com/search/repositories')
|
||||
.header('Accept', 'application/vnd.github.v3+json')
|
||||
.query({
|
||||
@@ -208,10 +112,10 @@ async function searchRepositories(query: string, perPage: number = 10) {
|
||||
### Handling Timeouts and Retries
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function fetchWithRetry(url: string) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(5000) // 5 seconds timeout
|
||||
.retry(3) // Retry up to 3 times on failure
|
||||
@@ -221,16 +125,35 @@ async function fetchWithRetry(url: string) {
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Different Response Types
|
||||
### Setting Request Options
|
||||
|
||||
The modern API provides a fetch-like interface for handling different response types:
|
||||
Use the `options()` method to set any request options supported by the underlying implementation:
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Set various options
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.options({
|
||||
keepAlive: true, // Enable connection reuse (Node.js)
|
||||
timeout: 10000, // 10 second timeout
|
||||
hardDataCuttingTimeout: 15000, // 15 second hard timeout
|
||||
// Platform-specific options are also supported
|
||||
})
|
||||
.get();
|
||||
```
|
||||
|
||||
### Working with Different Response Types
|
||||
|
||||
The API provides a fetch-like interface for handling different response types:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// JSON response (default)
|
||||
async function fetchJson(url: string) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
@@ -239,7 +162,7 @@ async function fetchJson(url: string) {
|
||||
|
||||
// Text response
|
||||
async function fetchText(url: string) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
@@ -248,7 +171,7 @@ async function fetchText(url: string) {
|
||||
|
||||
// Binary data
|
||||
async function downloadImage(url: string) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.accept('binary') // Optional: hints to server we want binary
|
||||
.get();
|
||||
@@ -257,36 +180,113 @@ async function downloadImage(url: string) {
|
||||
return Buffer.from(buffer); // Convert ArrayBuffer to Buffer if needed
|
||||
}
|
||||
|
||||
// Streaming response
|
||||
// Streaming response (Web Streams API)
|
||||
async function streamLargeFile(url: string) {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
// Get the underlying Node.js stream
|
||||
// Get a web-style ReadableStream (works in both Node.js and browsers)
|
||||
const stream = response.stream();
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
if (stream) {
|
||||
const reader = stream.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
console.log(`Received ${value.length} bytes of data`);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Node.js specific stream (only in Node.js environment)
|
||||
async function streamWithNodeApi(url: string) {
|
||||
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`);
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('end', resolve);
|
||||
stream.on('error', reject);
|
||||
nodeStream.on('end', resolve);
|
||||
nodeStream.on('error', reject);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Response Object Methods
|
||||
|
||||
The response object provides these methods:
|
||||
|
||||
- `json<T>(): Promise<T>` - Parse response as JSON
|
||||
- `text(): Promise<string>` - Get response as text
|
||||
- `arrayBuffer(): Promise<ArrayBuffer>` - Get response as ArrayBuffer
|
||||
- `stream(): ReadableStream<Uint8Array> | null` - Get web-style ReadableStream (cross-platform)
|
||||
- `streamNode(): NodeJS.ReadableStream` - Get Node.js stream (Node.js only, throws in browser)
|
||||
- `raw(): Response | http.IncomingMessage` - Get the underlying platform response
|
||||
|
||||
Each body method can only be called once per response, similar to the fetch API.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Form Data with File Uploads
|
||||
|
||||
```typescript
|
||||
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 => ({
|
||||
name: 'files',
|
||||
value: fs.readFileSync(file.path),
|
||||
filename: file.name,
|
||||
contentType: 'application/octet-stream'
|
||||
}));
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/upload')
|
||||
.formData(formFields)
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Unix Socket Support (Node.js only)
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Connect to a service via Unix socket
|
||||
async function queryViaUnixSocket() {
|
||||
const response = await SmartRequest.create()
|
||||
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination Support
|
||||
|
||||
The modern API includes built-in support for various pagination strategies:
|
||||
The library includes built-in support for various pagination strategies:
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient, PaginationStrategy } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Offset-based pagination (page & limit)
|
||||
async function fetchAllUsers() {
|
||||
const client = SmartRequestClient.create()
|
||||
const client = SmartRequest.create()
|
||||
.url('https://api.example.com/users')
|
||||
.withOffsetPagination({
|
||||
pageParam: 'page',
|
||||
@@ -314,7 +314,7 @@ async function fetchAllUsers() {
|
||||
|
||||
// Cursor-based pagination
|
||||
async function fetchAllPosts() {
|
||||
const allPosts = await SmartRequestClient.create()
|
||||
const allPosts = await SmartRequest.create()
|
||||
.url('https://api.example.com/posts')
|
||||
.withCursorPagination({
|
||||
cursorParam: 'cursor',
|
||||
@@ -328,7 +328,7 @@ async function fetchAllPosts() {
|
||||
|
||||
// Link header-based pagination (GitHub API style)
|
||||
async function fetchAllIssues(repo: string) {
|
||||
const paginatedResponse = await SmartRequestClient.create()
|
||||
const paginatedResponse = await SmartRequest.create()
|
||||
.url(`https://api.github.com/repos/${repo}/issues`)
|
||||
.header('Accept', 'application/vnd.github.v3+json')
|
||||
.withLinkPagination()
|
||||
@@ -338,104 +338,132 @@ async function fetchAllIssues(repo: string) {
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
#### Unix Socket Support
|
||||
### Keep-Alive Connections (Node.js)
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
|
||||
// Connect to a service via Unix socket
|
||||
async function queryViaUnixSocket() {
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
#### Form Data with File Uploads
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
|
||||
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'
|
||||
}));
|
||||
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('https://api.example.com/upload')
|
||||
.formData(formFields)
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
#### Keep-Alive Connections
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Enable keep-alive for better performance with multiple requests
|
||||
async function performMultipleRequests() {
|
||||
const client = SmartRequestClient.create()
|
||||
.header('Connection', 'keep-alive');
|
||||
// Note: keepAlive is NOT enabled by default
|
||||
const response1 = await SmartRequest.create()
|
||||
.url('https://api.example.com/endpoint1')
|
||||
.options({ keepAlive: true })
|
||||
.get();
|
||||
|
||||
// Requests will reuse the same connection
|
||||
const results = await Promise.all([
|
||||
client.url('https://api.example.com/endpoint1').get(),
|
||||
client.url('https://api.example.com/endpoint2').get(),
|
||||
client.url('https://api.example.com/endpoint3').get()
|
||||
]);
|
||||
const response2 = await SmartRequest.create()
|
||||
.url('https://api.example.com/endpoint2')
|
||||
.options({ keepAlive: true })
|
||||
.get();
|
||||
|
||||
return Promise.all(results.map(r => r.json()));
|
||||
// Connections are pooled and reused when keepAlive is enabled
|
||||
return [await response1.json(), await response2.json()];
|
||||
}
|
||||
```
|
||||
|
||||
### Response Object Methods
|
||||
### Rate Limiting (429 Too Many Requests) Handling
|
||||
|
||||
The modern API returns a `SmartResponse` object with the following methods:
|
||||
|
||||
- `json<T>(): Promise<T>` - Parse response as JSON
|
||||
- `text(): Promise<string>` - Get response as text
|
||||
- `arrayBuffer(): Promise<ArrayBuffer>` - Get response as ArrayBuffer
|
||||
- `stream(): NodeJS.ReadableStream` - Get the underlying Node.js stream
|
||||
- `raw(): http.IncomingMessage` - Get the raw http.IncomingMessage
|
||||
|
||||
Each body method can only be called once per response, similar to the fetch API.
|
||||
|
||||
Through its comprehensive set of features tailored for modern web development, `@push.rocks/smartrequest` aims to provide developers with a powerful tool for handling HTTP/HTTPS requests efficiently. Whether it's a simple API call, handling form data, processing streams, or working with paginated APIs, `@push.rocks/smartrequest` delivers a robust, type-safe solution to fit your project's requirements.
|
||||
|
||||
## Migration Guide: Legacy API to Modern API
|
||||
|
||||
If you're currently using the legacy API and want to migrate to the modern fluent API, here's a quick reference guide:
|
||||
|
||||
| Legacy API | Modern API |
|
||||
|------------|------------|
|
||||
| `getJson(url)` | `SmartRequestClient.create().url(url).get()` |
|
||||
| `postJson(url, { requestBody: data })` | `SmartRequestClient.create().url(url).json(data).post()` |
|
||||
| `putJson(url, { requestBody: data })` | `SmartRequestClient.create().url(url).json(data).put()` |
|
||||
| `delJson(url)` | `SmartRequestClient.create().url(url).delete()` |
|
||||
| `postFormData(url, {}, fields)` | `SmartRequestClient.create().url(url).formData(fields).post()` |
|
||||
| `getStream(url)` | `SmartRequestClient.create().url(url).accept('stream').get()` |
|
||||
| `request(url, options)` | `SmartRequestClient.create().url(url).[...configure options...].get()` |
|
||||
|
||||
The modern API provides more flexibility and better TypeScript integration, making it the recommended approach for new projects.
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Building a REST API Client
|
||||
|
||||
Here's a complete example of building a typed API client using smartrequest:
|
||||
The library includes built-in support for handling HTTP 429 (Too Many Requests) responses with intelligent backoff:
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient, type SmartResponse } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Simple usage - handle 429 with defaults
|
||||
async function fetchWithRateLimitHandling() {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.handle429Backoff() // Automatically retry on 429
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Advanced usage with custom configuration
|
||||
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)
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
console.log(`Rate limited. Attempt ${attempt}, waiting ${waitTime}ms`);
|
||||
}
|
||||
})
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Example: API client with rate limit handling
|
||||
class RateLimitedApiClient {
|
||||
private async request(path: string) {
|
||||
return SmartRequest.create()
|
||||
.url(`https://api.example.com${path}`)
|
||||
.handle429Backoff({
|
||||
maxRetries: 3,
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
console.log(`API rate limit hit. Waiting ${waitTime}ms before retry ${attempt}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchData(id: string) {
|
||||
const response = await this.request(`/data/${id}`).get();
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
- Allows custom callbacks for monitoring rate limit events
|
||||
- Caps maximum wait time to prevent excessive delays
|
||||
|
||||
## Platform-Specific Features
|
||||
|
||||
### Browser-Specific Options
|
||||
|
||||
When running in a browser, you can use browser-specific fetch options:
|
||||
|
||||
```typescript
|
||||
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'
|
||||
})
|
||||
.get();
|
||||
```
|
||||
|
||||
### Node.js-Specific Options
|
||||
|
||||
When running in Node.js, you can use Node-specific options:
|
||||
|
||||
```typescript
|
||||
import { Agent } from 'https';
|
||||
|
||||
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
|
||||
})
|
||||
.get();
|
||||
```
|
||||
|
||||
## Complete Example: Building a REST API Client
|
||||
|
||||
Here's a complete example of building a typed API client:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest, type CoreResponse } from '@push.rocks/smartrequest';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -454,7 +482,7 @@ class BlogApiClient {
|
||||
private baseUrl = 'https://jsonplaceholder.typicode.com';
|
||||
|
||||
private async request(path: string) {
|
||||
return SmartRequestClient.create()
|
||||
return SmartRequest.create()
|
||||
.url(`${this.baseUrl}${path}`)
|
||||
.header('Accept', 'application/json');
|
||||
}
|
||||
@@ -497,14 +525,14 @@ const user = await api.getUser(1);
|
||||
const posts = await api.getAllPosts(user.id);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function fetchWithErrorHandling(url: string) {
|
||||
try {
|
||||
const response = await SmartRequestClient.create()
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(5000)
|
||||
.retry(2)
|
||||
@@ -530,6 +558,8 @@ async function fetchWithErrorHandling(url: string) {
|
||||
console.error('Connection refused - is the server running?');
|
||||
} else if (error.code === 'ETIMEDOUT') {
|
||||
console.error('Request timed out');
|
||||
} else if (error.name === 'AbortError') {
|
||||
console.error('Request was aborted');
|
||||
} else {
|
||||
console.error('Request failed:', error.message);
|
||||
}
|
||||
@@ -538,9 +568,18 @@ async function fetchWithErrorHandling(url: string) {
|
||||
}
|
||||
```
|
||||
|
||||
## Migrating from v2.x to v3.x
|
||||
|
||||
Version 3.0 brings significant architectural improvements and a more consistent API:
|
||||
|
||||
1. **Legacy API Removed**: The function-based API (getJson, postJson, etc.) has been removed. Use SmartRequest instead.
|
||||
2. **Unified Response API**: All responses now use the same fetch-like interface regardless of platform.
|
||||
3. **Stream Changes**: The `stream()` method now returns a web-style ReadableStream on all platforms. Use `streamNode()` for Node.js streams.
|
||||
4. **Cross-Platform by Default**: The library now works in browsers out of the box with automatic platform detection.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -550,9 +589,9 @@ This project is owned and maintained by Task Venture Capital GmbH. The names and
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Task Venture Capital GmbH
|
||||
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.
|
@@ -1,53 +0,0 @@
|
||||
# Smartrequest Refactoring Plan
|
||||
|
||||
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
|
||||
|
||||
## Objective
|
||||
Refactor smartrequest to use native fetch-like API with a streamlined core that supports unix sockets and keep-alive.
|
||||
|
||||
## Architecture Overview
|
||||
- Rename `legacy/` to `core/` and remove "smartrequest." prefix from filenames
|
||||
- Create a modern Response class similar to fetch API
|
||||
- Use core as foundation for modern API, not as legacy adapter
|
||||
- Maintain unix socket and keep-alive support
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [x] Reread /home/philkunz/.claude/CLAUDE.md
|
||||
- [x] Create ts/core directory structure with request.ts, types.ts, and plugins.ts
|
||||
- [x] Migrate core request logic from legacy to core/request.ts
|
||||
- [x] Create modern Response class with fetch-like API
|
||||
- [x] Update modern API to use new core module
|
||||
- [x] Create legacy adapter for backward compatibility
|
||||
- [x] Update exports in ts/index.ts
|
||||
- [x] Run tests and fix any issues
|
||||
- [x] Clean up old legacy files
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Core Module Structure
|
||||
```
|
||||
ts/core/
|
||||
├── request.ts # Core HTTP/HTTPS request logic with unix socket support
|
||||
├── types.ts # Core interfaces and types
|
||||
├── plugins.ts # Dependencies (http, https, agentkeepalive, etc.)
|
||||
└── response.ts # Modern Response class
|
||||
```
|
||||
|
||||
### Response Class API
|
||||
The new Response class will provide fetch-like methods:
|
||||
- `json()`: Promise<T> - Parse response as JSON
|
||||
- `text()`: Promise<string> - Get response as text
|
||||
- `arrayBuffer()`: Promise<ArrayBuffer> - Get response as ArrayBuffer
|
||||
- `stream()`: ReadableStream - Get response as stream
|
||||
- `ok`: boolean - Status is 2xx
|
||||
- `status`: number - HTTP status code
|
||||
- `statusText`: string - HTTP status text
|
||||
- `headers`: Headers - Response headers
|
||||
|
||||
### Migration Strategy
|
||||
1. Move core request logic without breaking changes
|
||||
2. Create Response wrapper that provides modern API
|
||||
3. Update SmartRequestClient to use new core
|
||||
4. Add legacy adapter for backward compatibility
|
||||
5. Ensure all tests pass throughout migration
|
105
test/test.browser.ts
Normal file
105
test/test.browser.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
// For browser tests, we need to import from a browser-safe path
|
||||
// that doesn't trigger Node.js module imports
|
||||
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 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);
|
||||
expect(data).toHaveProperty('title');
|
||||
});
|
||||
|
||||
tap.test('browser: should handle CORS requests', async () => {
|
||||
const options: ICoreRequestOptions = {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
tap.test('browser: should handle request timeouts', async () => {
|
||||
let timedOut = false;
|
||||
|
||||
const options: ICoreRequestOptions = {
|
||||
timeout: 100 // Very short timeout
|
||||
};
|
||||
|
||||
try {
|
||||
// Use a URL that will likely take longer than 100ms
|
||||
const request = new CoreRequest('https://jsonplaceholder.typicode.com/photos', options);
|
||||
await request.fire();
|
||||
} catch (error) {
|
||||
timedOut = true;
|
||||
// Different browsers might have different timeout error messages
|
||||
expect(error.message.toLowerCase()).toMatch(/timeout|timed out|aborted/i);
|
||||
}
|
||||
|
||||
expect(timedOut).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('browser: should handle POST requests with JSON', async () => {
|
||||
const testData = {
|
||||
title: 'foo',
|
||||
body: 'bar',
|
||||
userId: 1
|
||||
};
|
||||
|
||||
const options: ICoreRequestOptions = {
|
||||
method: 'POST',
|
||||
requestBody: testData
|
||||
};
|
||||
|
||||
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);
|
||||
expect(responseData.body).toEqual(testData.body);
|
||||
expect(responseData.userId).toEqual(testData.userId);
|
||||
});
|
||||
|
||||
tap.test('browser: should handle query parameters', async () => {
|
||||
const options: ICoreRequestOptions = {
|
||||
queryParams: {
|
||||
userId: '2'
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
if (data.length > 0) {
|
||||
expect(data[0]).toHaveProperty('userId');
|
||||
expect(data[0].userId).toEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -1,45 +0,0 @@
|
||||
import { tap, expect, expectAsync } from '@pushrocks/tapbundle';
|
||||
|
||||
import * as smartrequest from '../ts/legacy/index.js';
|
||||
|
||||
tap.test('should request a html document over https', async () => {
|
||||
await expectAsync(smartrequest.getJson('https://encrypted.google.com/')).toHaveProperty('body');
|
||||
});
|
||||
|
||||
tap.test('should request a JSON document over https', async () => {
|
||||
await expectAsync(smartrequest.getJson('https://jsonplaceholder.typicode.com/posts/1'))
|
||||
.property('body')
|
||||
.property('id')
|
||||
.toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('should post a JSON document over http', async () => {
|
||||
const testData = { text: 'example_text' };
|
||||
await expectAsync(smartrequest.postJson('https://httpbin.org/post', { requestBody: testData }))
|
||||
.property('body')
|
||||
.property('json')
|
||||
.property('text')
|
||||
.toEqual('example_text');
|
||||
});
|
||||
|
||||
tap.test('should safe get stuff', async () => {
|
||||
smartrequest.safeGet('http://coffee.link/');
|
||||
smartrequest.safeGet('https://coffee.link/');
|
||||
});
|
||||
|
||||
tap.skip.test('should deal with unix socks', async () => {
|
||||
const socketResponse = await smartrequest.request(
|
||||
'http://unix:/var/run/docker.sock:/containers/json',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Host: 'docker.sock',
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log(socketResponse.body);
|
||||
});
|
||||
|
||||
tap.skip.test('should correctly upload a file using formData', async () => {});
|
||||
|
||||
tap.start();
|
@@ -1,95 +0,0 @@
|
||||
import { tap, expect } from '@pushrocks/tapbundle';
|
||||
|
||||
import { SmartRequestClient } from '../ts/modern/index.js';
|
||||
|
||||
tap.test('modern: should request a html document over https', async () => {
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('https://encrypted.google.com/')
|
||||
.get();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response).toHaveProperty('status');
|
||||
expect(response.status).toBeGreaterThan(0);
|
||||
const text = await response.text();
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('modern: should request a JSON document over https', async () => {
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(body.id).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('modern: should post a JSON document over http', async () => {
|
||||
const testData = { text: 'example_text' };
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.json(testData)
|
||||
.post();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('json');
|
||||
expect(body.json).toHaveProperty('text');
|
||||
expect(body.json.text).toEqual('example_text');
|
||||
});
|
||||
|
||||
tap.test('modern: should set headers correctly', async () => {
|
||||
const customHeader = 'X-Custom-Header';
|
||||
const headerValue = 'test-value';
|
||||
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('https://httpbin.org/headers')
|
||||
.header(customHeader, headerValue)
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('headers');
|
||||
|
||||
// Check if the header exists (case-sensitive)
|
||||
expect(body.headers).toHaveProperty(customHeader);
|
||||
expect(body.headers[customHeader]).toEqual(headerValue);
|
||||
});
|
||||
|
||||
tap.test('modern: should handle query parameters', async () => {
|
||||
const params = { param1: 'value1', param2: 'value2' };
|
||||
|
||||
const response = await SmartRequestClient.create()
|
||||
.url('https://httpbin.org/get')
|
||||
.query(params)
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('args');
|
||||
expect(body.args).toHaveProperty('param1');
|
||||
expect(body.args.param1).toEqual('value1');
|
||||
expect(body.args).toHaveProperty('param2');
|
||||
expect(body.args.param2).toEqual('value2');
|
||||
});
|
||||
|
||||
tap.test('modern: should handle timeout configuration', async () => {
|
||||
// This test just verifies that the timeout method doesn't throw
|
||||
const client = SmartRequestClient.create()
|
||||
.url('https://httpbin.org/get')
|
||||
.timeout(5000);
|
||||
|
||||
const response = await client.get();
|
||||
expect(response).toHaveProperty('ok');
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('modern: should handle retry configuration', async () => {
|
||||
// This test just verifies that the retry method doesn't throw
|
||||
const client = SmartRequestClient.create()
|
||||
.url('https://httpbin.org/get')
|
||||
.retry(1);
|
||||
|
||||
const response = await client.get();
|
||||
expect(response).toHaveProperty('ok');
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
204
test/test.node.ts
Normal file
204
test/test.node.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import { SmartRequest } from '../ts/client/index.js';
|
||||
|
||||
tap.test('client: should request a html document over https', async () => {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://encrypted.google.com/')
|
||||
.get();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response).toHaveProperty('status');
|
||||
expect(response.status).toBeGreaterThan(0);
|
||||
const text = await response.text();
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('client: should request a JSON document over https', async () => {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(body.id).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('client: should post a JSON document over http', async () => {
|
||||
const testData = { title: 'example_text', body: 'test body', userId: 1 };
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts')
|
||||
.json(testData)
|
||||
.post();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('title');
|
||||
expect(body.title).toEqual('example_text');
|
||||
expect(body).toHaveProperty('id'); // jsonplaceholder returns an id for created posts
|
||||
});
|
||||
|
||||
tap.test('client: should set headers correctly', async () => {
|
||||
const customHeader = 'X-Custom-Header';
|
||||
const headerValue = 'test-value';
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://echo.zuplo.io/')
|
||||
.header(customHeader, headerValue)
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('headers');
|
||||
|
||||
// Check if the header exists (headers might be lowercase)
|
||||
const headers = body.headers;
|
||||
const headerFound = headers[customHeader] || headers[customHeader.toLowerCase()] || headers['x-custom-header'];
|
||||
expect(headerFound).toEqual(headerValue);
|
||||
});
|
||||
|
||||
tap.test('client: should handle query parameters', async () => {
|
||||
const params = { userId: '1' };
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts')
|
||||
.query(params)
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(Array.isArray(body)).toBeTrue();
|
||||
// Check that we got posts for userId 1
|
||||
if (body.length > 0) {
|
||||
expect(body[0]).toHaveProperty('userId');
|
||||
expect(body[0].userId).toEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('client: should handle timeout configuration', async () => {
|
||||
// This test just verifies that the timeout method doesn't throw
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.timeout(5000);
|
||||
|
||||
const response = await client.get();
|
||||
expect(response).toHaveProperty('ok');
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('client: should handle retry configuration', async () => {
|
||||
// This test just verifies that the retry method doesn't throw
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.retry(1);
|
||||
|
||||
const response = await client.get();
|
||||
expect(response).toHaveProperty('ok');
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('client: should support keepAlive option for connection reuse', async () => {
|
||||
// Test basic keepAlive functionality
|
||||
const responses = [];
|
||||
|
||||
// Make multiple requests with keepAlive enabled
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.options({ keepAlive: true })
|
||||
.header('X-Request-Number', String(i))
|
||||
.get();
|
||||
|
||||
expect(response.ok).toBeTrue();
|
||||
responses.push(response);
|
||||
}
|
||||
|
||||
// Verify all requests succeeded
|
||||
expect(responses).toHaveLength(3);
|
||||
|
||||
// Also test that keepAlive: false works
|
||||
const responseNoKeepAlive = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/2')
|
||||
.options({ keepAlive: false })
|
||||
.get();
|
||||
|
||||
expect(responseNoKeepAlive.ok).toBeTrue();
|
||||
|
||||
// Verify we can parse the responses
|
||||
const data = await responses[0].json();
|
||||
expect(data).toHaveProperty('id');
|
||||
expect(data.id).toEqual(1);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
tap.test('client: should handle 429 with custom config', async () => {
|
||||
let rateLimitCallbackCalled = false;
|
||||
let attemptCount = 0;
|
||||
let waitTimeReceived = 0;
|
||||
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.handle429Backoff({
|
||||
maxRetries: 2,
|
||||
fallbackDelay: 500,
|
||||
maxWaitTime: 5000,
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
const response = await client.get();
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
tap.start();
|
@@ -1,17 +1,18 @@
|
||||
import { type SmartResponse } from '../../core/index.js';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Creates a paginated response from a regular response
|
||||
*/
|
||||
export async function createPaginatedResponse<T>(
|
||||
response: SmartResponse<any>,
|
||||
response: ICoreResponse<any>,
|
||||
paginationConfig: TPaginationConfig,
|
||||
queryParams: Record<string, string>,
|
||||
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
|
||||
): Promise<TPaginatedResponse<T>> {
|
||||
// Parse response body first
|
||||
const body = await response.json();
|
||||
const body = await response.json() as any;
|
||||
|
||||
// Default to response.body for items if response is JSON
|
||||
let items: T[] = Array.isArray(body)
|
@@ -1,11 +1,11 @@
|
||||
// Export the main client
|
||||
export { SmartRequestClient } from './smartrequestclient.js';
|
||||
export { SmartRequest } from './smartrequest.js';
|
||||
|
||||
// Export response type from core
|
||||
export { SmartResponse } from '../core/index.js';
|
||||
export { CoreResponse } from '../core/index.js';
|
||||
|
||||
// Export types
|
||||
export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig } from './types/common.js';
|
||||
export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig, RateLimitConfig } from './types/common.js';
|
||||
export {
|
||||
PaginationStrategy,
|
||||
type TPaginationConfig as PaginationConfig,
|
||||
@@ -17,32 +17,32 @@ export {
|
||||
} from './types/pagination.js';
|
||||
|
||||
// Convenience factory functions
|
||||
import { SmartRequestClient } from './smartrequestclient.js';
|
||||
import { SmartRequest } from './smartrequest.js';
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for JSON requests
|
||||
*/
|
||||
export function createJsonClient<T = any>() {
|
||||
return SmartRequestClient.create<T>();
|
||||
return SmartRequest.create<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for form data requests
|
||||
*/
|
||||
export function createFormClient<T = any>() {
|
||||
return SmartRequestClient.create<T>();
|
||||
return SmartRequest.create<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for binary data
|
||||
*/
|
||||
export function createBinaryClient<T = any>() {
|
||||
return SmartRequestClient.create<T>().accept('binary');
|
||||
return SmartRequest.create<T>().accept('binary');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for streaming
|
||||
*/
|
||||
export function createStreamClient() {
|
||||
return SmartRequestClient.create().accept('stream');
|
||||
return SmartRequest.create().accept('stream');
|
||||
}
|
6
ts/client/plugins.ts
Normal file
6
ts/client/plugins.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// plugins for client module
|
||||
import FormData from 'form-data';
|
||||
|
||||
export {
|
||||
FormData as formData
|
||||
};
|
@@ -1,7 +1,9 @@
|
||||
import { request, SmartResponse, type ICoreRequestOptions } from '../core/index.js';
|
||||
import * as plugins from '../core/plugins.js';
|
||||
import { CoreRequest, CoreResponse } from '../core/index.js';
|
||||
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 } from './types/common.js';
|
||||
import type { HttpMethod, ResponseType, FormField, RateLimitConfig } from './types/common.js';
|
||||
import {
|
||||
type TPaginationConfig,
|
||||
PaginationStrategy,
|
||||
@@ -12,21 +14,48 @@ import {
|
||||
} from './types/pagination.js';
|
||||
import { createPaginatedResponse } from './features/pagination.js';
|
||||
|
||||
/**
|
||||
* Parse Retry-After header value to milliseconds
|
||||
* @param retryAfter - The Retry-After header value (seconds or HTTP date)
|
||||
* @returns Delay in milliseconds
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern fluent client for making HTTP requests
|
||||
*/
|
||||
export class SmartRequestClient<T = any> {
|
||||
export class SmartRequest<T = any> {
|
||||
private _url: string;
|
||||
private _options: ICoreRequestOptions = {};
|
||||
private _retries: number = 0;
|
||||
private _queryParams: Record<string, string> = {};
|
||||
private _paginationConfig?: TPaginationConfig;
|
||||
private _rateLimitConfig?: RateLimitConfig;
|
||||
|
||||
/**
|
||||
* Create a new SmartRequestClient instance
|
||||
* Create a new SmartRequest instance
|
||||
*/
|
||||
static create<T = any>(): SmartRequestClient<T> {
|
||||
return new SmartRequestClient<T>();
|
||||
static create<T = any>(): SmartRequest<T> {
|
||||
return new SmartRequest<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +133,21 @@ export class SmartRequestClient<T = any> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable automatic 429 (Too Many Requests) handling with configurable backoff
|
||||
*/
|
||||
handle429Backoff(config?: RateLimitConfig): this {
|
||||
this._rateLimitConfig = {
|
||||
maxRetries: config?.maxRetries ?? 3,
|
||||
respectRetryAfter: config?.respectRetryAfter ?? true,
|
||||
maxWaitTime: config?.maxWaitTime ?? 60000,
|
||||
fallbackDelay: config?.fallbackDelay ?? 1000,
|
||||
backoffFactor: config?.backoffFactor ?? 2,
|
||||
onRateLimit: config?.onRateLimit
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set HTTP headers
|
||||
*/
|
||||
@@ -140,6 +184,17 @@ export class SmartRequestClient<T = any> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional request options
|
||||
*/
|
||||
options(options: Partial<ICoreRequestOptions>): this {
|
||||
this._options = {
|
||||
...this._options,
|
||||
...options
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Accept header to indicate what content type is expected
|
||||
*/
|
||||
@@ -223,35 +278,35 @@ export class SmartRequestClient<T = any> {
|
||||
/**
|
||||
* Make a GET request
|
||||
*/
|
||||
async get<R = T>(): Promise<SmartResponse<R>> {
|
||||
async get<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
*/
|
||||
async post<R = T>(): Promise<SmartResponse<R>> {
|
||||
async post<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
*/
|
||||
async put<R = T>(): Promise<SmartResponse<R>> {
|
||||
async put<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('PUT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
*/
|
||||
async delete<R = T>(): Promise<SmartResponse<R>> {
|
||||
async delete<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a PATCH request
|
||||
*/
|
||||
async patch<R = T>(): Promise<SmartResponse<R>> {
|
||||
async patch<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('PATCH');
|
||||
}
|
||||
|
||||
@@ -276,7 +331,7 @@ export class SmartRequestClient<T = any> {
|
||||
this._queryParams,
|
||||
(nextPageParams) => {
|
||||
// Create a new client with the same configuration but updated query params
|
||||
const nextClient = new SmartRequestClient<ItemType>();
|
||||
const nextClient = new SmartRequest<ItemType>();
|
||||
Object.assign(nextClient, this);
|
||||
nextClient._queryParams = nextPageParams;
|
||||
|
||||
@@ -296,20 +351,62 @@ export class SmartRequestClient<T = any> {
|
||||
/**
|
||||
* Execute the HTTP request
|
||||
*/
|
||||
private async execute<R = T>(method?: HttpMethod): Promise<SmartResponse<R>> {
|
||||
private async execute<R = T>(method?: HttpMethod): Promise<ICoreResponse<R>> {
|
||||
if (method) {
|
||||
this._options.method = method;
|
||||
}
|
||||
|
||||
this._options.queryParams = this._queryParams;
|
||||
|
||||
// Handle retry logic
|
||||
// Track rate limit attempts separately
|
||||
let rateLimitAttempt = 0;
|
||||
let lastError: Error;
|
||||
|
||||
// Main retry loop
|
||||
for (let attempt = 0; attempt <= this._retries; attempt++) {
|
||||
try {
|
||||
const response = await request(this._url, this._options);
|
||||
return response as SmartResponse<R>;
|
||||
const request = new CoreRequest(this._url, this._options as any);
|
||||
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) {
|
||||
// Max rate limit retries reached, return the 429 response
|
||||
return response;
|
||||
}
|
||||
|
||||
let waitTime: number;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// Call rate limit callback if provided
|
||||
if (this._rateLimitConfig.onRateLimit) {
|
||||
this._rateLimitConfig.onRateLimit(rateLimitAttempt + 1, waitTime);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
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) {
|
||||
lastError = error as Error;
|
||||
|
@@ -46,4 +46,16 @@ export interface TimeoutConfig {
|
||||
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)
|
||||
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import { type SmartResponse } from '../../core/index.js';
|
||||
import { type CoreResponse } from '../../core/index.js';
|
||||
import type { ICoreResponse } from '../../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Pagination strategy options
|
||||
@@ -45,8 +46,8 @@ export interface LinkPaginationConfig {
|
||||
*/
|
||||
export interface CustomPaginationConfig {
|
||||
strategy: PaginationStrategy.CUSTOM;
|
||||
hasNextPage: (response: SmartResponse<any>) => boolean;
|
||||
getNextPageParams: (response: SmartResponse<any>, currentParams: Record<string, string>) => Record<string, string>;
|
||||
hasNextPage: (response: ICoreResponse<any>) => boolean;
|
||||
getNextPageParams: (response: ICoreResponse<any>, currentParams: Record<string, string>) => Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,5 +63,5 @@ export interface TPaginatedResponse<T> {
|
||||
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: SmartResponse<any>; // Original response
|
||||
response: ICoreResponse<any>; // Original response
|
||||
}
|
@@ -1,4 +1,30 @@
|
||||
// Core exports
|
||||
export * from './types.js';
|
||||
export * from './response.js';
|
||||
export { request, coreRequest, isUnixSocket, parseUnixSocketUrl } from './request.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// Export all base types - these are the public API
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
const smartenvInstance = new plugins.smartenv.Smartenv();
|
||||
|
||||
// Dynamically load the appropriate implementation
|
||||
let CoreRequest: any;
|
||||
let CoreResponse: any;
|
||||
|
||||
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'
|
||||
)
|
||||
console.log(modulePath);
|
||||
const impl = await smartenvInstance.getSafeNodeModule(modulePath);
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
} else {
|
||||
// In browser, load the fetch implementation
|
||||
const impl = await import('../core_fetch/index.js');
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
}
|
||||
|
||||
// Export the loaded implementations
|
||||
export { CoreRequest, CoreResponse };
|
||||
|
@@ -1,19 +1,4 @@
|
||||
// node native scope
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
import * as smartenv from '@push.rocks/smartenv';
|
||||
import * as smartpath from '@push.rocks/smartpath/iso';
|
||||
|
||||
export { http, https, fs, path };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smarturl from '@push.rocks/smarturl';
|
||||
|
||||
export { smartpromise, smarturl };
|
||||
|
||||
// third party scope
|
||||
import agentkeepalive from 'agentkeepalive';
|
||||
import formData from 'form-data';
|
||||
|
||||
export { agentkeepalive, formData };
|
||||
export { smartenv, smartpath };
|
||||
|
@@ -1,159 +0,0 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { SmartResponse } from './response.js';
|
||||
|
||||
// Keep-alive agents for connection pooling
|
||||
const httpAgent = new plugins.agentkeepalive({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpAgentKeepAliveFalse = new plugins.agentkeepalive({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests if a URL is a unix socket
|
||||
*/
|
||||
export const isUnixSocket = (url: string): boolean => {
|
||||
const unixRegex = /^(http:\/\/|https:\/\/|)unix:/;
|
||||
return unixRegex.test(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses socket path and route from unix socket URL
|
||||
*/
|
||||
export const parseUnixSocketUrl = (url: string): { socketPath: string; path: string } => {
|
||||
const parseRegex = /(.*):(.*)/;
|
||||
const result = parseRegex.exec(url);
|
||||
return {
|
||||
socketPath: result[1],
|
||||
path: result[2],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Core request function that handles all HTTP/HTTPS requests
|
||||
*/
|
||||
export async function coreRequest(
|
||||
urlArg: string,
|
||||
optionsArg: types.ICoreRequestOptions = {},
|
||||
requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null
|
||||
): Promise<plugins.http.IncomingMessage> {
|
||||
const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();
|
||||
|
||||
// No defaults - let users explicitly set options to match fetch behavior
|
||||
|
||||
// Parse URL
|
||||
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg, {
|
||||
searchParams: optionsArg.queryParams || {},
|
||||
});
|
||||
|
||||
optionsArg.hostname = parsedUrl.hostname;
|
||||
if (parsedUrl.port) {
|
||||
optionsArg.port = parseInt(parsedUrl.port, 10);
|
||||
}
|
||||
optionsArg.path = parsedUrl.path;
|
||||
|
||||
// Handle unix socket URLs
|
||||
if (isUnixSocket(urlArg)) {
|
||||
const { socketPath, path } = parseUnixSocketUrl(optionsArg.path);
|
||||
optionsArg.socketPath = socketPath;
|
||||
optionsArg.path = path;
|
||||
}
|
||||
|
||||
// Determine agent based on protocol and keep-alive setting
|
||||
if (!optionsArg.agent) {
|
||||
// Only use keep-alive agents if explicitly requested
|
||||
if (optionsArg.keepAlive === true) {
|
||||
optionsArg.agent = parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent;
|
||||
} else if (optionsArg.keepAlive === false) {
|
||||
optionsArg.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;
|
||||
|
||||
if (!requestModule) {
|
||||
throw new Error(`The request to ${urlArg} is missing a viable protocol. Must be http or https`);
|
||||
}
|
||||
|
||||
// Perform the request
|
||||
const request = requestModule.request(optionsArg, async (response) => {
|
||||
// Handle hard timeout
|
||||
if (optionsArg.hardDataCuttingTimeout) {
|
||||
setTimeout(() => {
|
||||
response.destroy();
|
||||
done.reject(new Error('Request timed out'));
|
||||
}, optionsArg.hardDataCuttingTimeout);
|
||||
}
|
||||
|
||||
// Always return the raw stream
|
||||
done.resolve(response);
|
||||
});
|
||||
|
||||
// Write request body
|
||||
if (optionsArg.requestBody) {
|
||||
if (optionsArg.requestBody instanceof plugins.formData) {
|
||||
optionsArg.requestBody.pipe(request).on('finish', () => {
|
||||
request.end();
|
||||
});
|
||||
} else {
|
||||
// Write body as-is - caller is responsible for serialization
|
||||
const bodyData = typeof optionsArg.requestBody === 'string'
|
||||
? optionsArg.requestBody
|
||||
: optionsArg.requestBody instanceof Buffer
|
||||
? optionsArg.requestBody
|
||||
: JSON.stringify(optionsArg.requestBody); // Still stringify for backward compatibility
|
||||
request.write(bodyData);
|
||||
request.end();
|
||||
}
|
||||
} else if (requestDataFunc) {
|
||||
requestDataFunc(request);
|
||||
} else {
|
||||
request.end();
|
||||
}
|
||||
|
||||
// Handle request errors
|
||||
request.on('error', (e) => {
|
||||
console.error(e);
|
||||
request.destroy();
|
||||
done.reject(e);
|
||||
});
|
||||
|
||||
// Get response and handle response errors
|
||||
const response = await done.promise;
|
||||
response.on('error', (err) => {
|
||||
console.error(err);
|
||||
response.destroy();
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern request function that returns a SmartResponse
|
||||
*/
|
||||
export async function request(
|
||||
urlArg: string,
|
||||
optionsArg: types.ICoreRequestOptions = {}
|
||||
): Promise<SmartResponse> {
|
||||
const response = await coreRequest(urlArg, optionsArg);
|
||||
return new SmartResponse(response, urlArg);
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
/**
|
||||
* Core request options extending Node.js RequestOptions
|
||||
*/
|
||||
export interface ICoreRequestOptions extends plugins.https.RequestOptions {
|
||||
keepAlive?: boolean;
|
||||
requestBody?: any;
|
||||
queryParams?: { [key: string]: string };
|
||||
hardDataCuttingTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Methods supported
|
||||
*/
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/**
|
||||
* Response types supported
|
||||
*/
|
||||
export type ResponseType = 'json' | 'text' | 'binary' | 'stream';
|
||||
|
||||
/**
|
||||
* Extended IncomingMessage with body property (legacy compatibility)
|
||||
*/
|
||||
export interface IExtendedIncomingMessage<T = any> extends plugins.http.IncomingMessage {
|
||||
body: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form field data for multipart/form-data requests
|
||||
*/
|
||||
export interface IFormField {
|
||||
name: string;
|
||||
value: string | Buffer;
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encoded form field
|
||||
*/
|
||||
export interface IUrlEncodedField {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core response object that provides fetch-like API
|
||||
*/
|
||||
export interface ICoreResponse<T = any> {
|
||||
// Properties
|
||||
ok: boolean;
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: plugins.http.IncomingHttpHeaders;
|
||||
url: string;
|
||||
|
||||
// Methods
|
||||
json(): Promise<T>;
|
||||
text(): Promise<string>;
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
stream(): NodeJS.ReadableStream;
|
||||
|
||||
// Legacy compatibility
|
||||
raw(): plugins.http.IncomingMessage;
|
||||
}
|
4
ts/core_base/index.ts
Normal file
4
ts/core_base/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Core base exports - abstract classes and platform-agnostic types
|
||||
export * from './types.js';
|
||||
export * from './request.js';
|
||||
export * from './response.js';
|
45
ts/core_base/request.ts
Normal file
45
ts/core_base/request.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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> {
|
||||
/**
|
||||
* Tests if a URL is a unix socket
|
||||
*/
|
||||
static isUnixSocket(url: string): boolean {
|
||||
const unixRegex = /^(http:\/\/|https:\/\/|)unix:/;
|
||||
return unixRegex.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses socket path and route from unix socket URL
|
||||
*/
|
||||
static parseUnixSocketUrl(url: string): { socketPath: string; path: string } {
|
||||
const parseRegex = /(.*):(.*)/;
|
||||
const result = parseRegex.exec(url);
|
||||
return {
|
||||
socketPath: result[1],
|
||||
path: result[2],
|
||||
};
|
||||
}
|
||||
|
||||
protected url: string;
|
||||
protected options: TOptions;
|
||||
|
||||
constructor(url: string, options?: TOptions) {
|
||||
this.url = url;
|
||||
this.options = options || ({} as TOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a response
|
||||
*/
|
||||
abstract fire(): Promise<TResponse>;
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw response (platform-specific)
|
||||
*/
|
||||
abstract fireCore(): Promise<any>;
|
||||
|
||||
}
|
45
ts/core_base/response.ts
Normal file
45
ts/core_base/response.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as types from './types.js';
|
||||
|
||||
/**
|
||||
* Abstract Core Response class that provides a fetch-like API
|
||||
*/
|
||||
export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> {
|
||||
protected consumed = false;
|
||||
|
||||
// Public properties
|
||||
public abstract readonly ok: boolean;
|
||||
public abstract readonly status: number;
|
||||
public abstract readonly statusText: string;
|
||||
public abstract readonly headers: types.Headers;
|
||||
public abstract readonly url: string;
|
||||
|
||||
/**
|
||||
* Ensures the body can only be consumed once
|
||||
*/
|
||||
protected ensureNotConsumed(): void {
|
||||
if (this.consumed) {
|
||||
throw new Error('Body has already been consumed');
|
||||
}
|
||||
this.consumed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
abstract json(): Promise<T>;
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
abstract text(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
abstract arrayBuffer(): Promise<ArrayBuffer>;
|
||||
|
||||
/**
|
||||
* Get response as a web-style ReadableStream
|
||||
*/
|
||||
abstract stream(): ReadableStream<Uint8Array> | null;
|
||||
}
|
81
ts/core_base/types.ts
Normal file
81
ts/core_base/types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* HTTP Methods supported
|
||||
*/
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/**
|
||||
* Response types supported
|
||||
*/
|
||||
export type ResponseType = 'json' | 'text' | 'binary' | 'stream';
|
||||
|
||||
/**
|
||||
* Form field data for multipart/form-data requests
|
||||
*/
|
||||
export interface IFormField {
|
||||
name: string;
|
||||
value: string | Buffer;
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encoded form field
|
||||
*/
|
||||
export interface IUrlEncodedField {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core request options - unified interface for all implementations
|
||||
*/
|
||||
export interface ICoreRequestOptions {
|
||||
// Common options
|
||||
method?: THttpMethod | string; // Allow string for compatibility
|
||||
headers?: any; // Allow any for platform compatibility
|
||||
keepAlive?: boolean;
|
||||
requestBody?: any;
|
||||
queryParams?: { [key: string]: string };
|
||||
timeout?: number;
|
||||
hardDataCuttingTimeout?: number;
|
||||
|
||||
// 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;
|
||||
cache?: RequestCache;
|
||||
redirect?: RequestRedirect;
|
||||
referrer?: string;
|
||||
referrerPolicy?: ReferrerPolicy;
|
||||
integrity?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response headers - platform agnostic
|
||||
*/
|
||||
export type Headers = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Core response interface - platform agnostic
|
||||
*/
|
||||
export interface ICoreResponse<T = any> {
|
||||
// Properties
|
||||
ok: boolean;
|
||||
status: number;
|
||||
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
|
||||
}
|
3
ts/core_fetch/index.ts
Normal file
3
ts/core_fetch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Core fetch exports - native fetch implementation
|
||||
export * from './response.js';
|
||||
export { CoreRequest } from './request.js';
|
131
ts/core_fetch/request.ts
Normal file
131
ts/core_fetch/request.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
|
||||
|
||||
/**
|
||||
* Fetch-based implementation of Core Request class
|
||||
*/
|
||||
export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions, CoreResponse> {
|
||||
constructor(url: string, options: types.ICoreRequestOptions = {}) {
|
||||
super(url, options);
|
||||
|
||||
// Check for unsupported Node.js-specific options
|
||||
if (options.agent || options.socketPath) {
|
||||
throw new Error('Node.js specific options (agent, socketPath) are not supported in browser/fetch implementation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full URL with query parameters
|
||||
*/
|
||||
private buildUrl(): string {
|
||||
if (!this.options.queryParams || Object.keys(this.options.queryParams).length === 0) {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
const url = new URL(this.url);
|
||||
Object.entries(this.options.queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our options to fetch RequestInit
|
||||
*/
|
||||
private buildFetchOptions(): RequestInit {
|
||||
const fetchOptions: RequestInit = {
|
||||
method: this.options.method,
|
||||
headers: this.options.headers,
|
||||
credentials: this.options.credentials,
|
||||
mode: this.options.mode,
|
||||
cache: this.options.cache,
|
||||
redirect: this.options.redirect,
|
||||
referrer: this.options.referrer,
|
||||
referrerPolicy: this.options.referrerPolicy,
|
||||
integrity: this.options.integrity,
|
||||
keepalive: this.options.keepAlive,
|
||||
signal: this.options.signal,
|
||||
};
|
||||
|
||||
// Handle request body
|
||||
if (this.options.requestBody !== undefined) {
|
||||
if (typeof this.options.requestBody === 'string' ||
|
||||
this.options.requestBody instanceof ArrayBuffer ||
|
||||
this.options.requestBody instanceof FormData ||
|
||||
this.options.requestBody instanceof URLSearchParams ||
|
||||
this.options.requestBody instanceof ReadableStream) {
|
||||
fetchOptions.body = this.options.requestBody;
|
||||
} else {
|
||||
// Convert objects to JSON
|
||||
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
||||
// Set content-type if not already set
|
||||
if (!fetchOptions.headers) {
|
||||
fetchOptions.headers = { 'Content-Type': 'application/json' };
|
||||
} else if (fetchOptions.headers instanceof Headers) {
|
||||
if (!fetchOptions.headers.has('Content-Type')) {
|
||||
fetchOptions.headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
} else if (typeof fetchOptions.headers === 'object' && !Array.isArray(fetchOptions.headers)) {
|
||||
const headersObj = fetchOptions.headers as Record<string, string>;
|
||||
if (!headersObj['Content-Type']) {
|
||||
headersObj['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
||||
const timeout = this.options.hardDataCuttingTimeout || this.options.timeout;
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
fetchOptions.signal = controller.signal;
|
||||
}
|
||||
|
||||
return fetchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const response = await this.fireCore();
|
||||
return new CoreResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw Response
|
||||
*/
|
||||
async fireCore(): Promise<Response> {
|
||||
const url = this.buildUrl();
|
||||
const options = this.buildFetchOptions();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.ICoreRequestOptions = {}
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience exports for backward compatibility
|
||||
*/
|
||||
export const isUnixSocket = CoreRequest.isUnixSocket;
|
||||
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;
|
85
ts/core_fetch/response.ts
Normal file
85
ts/core_fetch/response.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as types from './types.js';
|
||||
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> {
|
||||
private response: Response;
|
||||
private responseClone: Response;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly headers: types.Headers;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(response: Response) {
|
||||
super();
|
||||
// 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) => {
|
||||
this.headers[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
async json(): Promise<T> {
|
||||
this.ensureNotConsumed();
|
||||
try {
|
||||
return await this.response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a readable stream (Web Streams API)
|
||||
*/
|
||||
stream(): ReadableStream<Uint8Array> | null {
|
||||
this.ensureNotConsumed();
|
||||
return this.response.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw Response object
|
||||
*/
|
||||
raw(): Response {
|
||||
return this.responseClone;
|
||||
}
|
||||
}
|
15
ts/core_fetch/types.ts
Normal file
15
ts/core_fetch/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as baseTypes from '../core_base/types.js';
|
||||
|
||||
// Re-export base types
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Fetch-specific response extensions
|
||||
*/
|
||||
export interface IFetchResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Node.js stream method that throws in browser
|
||||
streamNode(): never;
|
||||
|
||||
// Access to raw Response object
|
||||
raw(): Response;
|
||||
}
|
3
ts/core_node/index.ts
Normal file
3
ts/core_node/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Core exports
|
||||
export * from './response.js';
|
||||
export { CoreRequest } from './request.js';
|
20
ts/core_node/plugins.ts
Normal file
20
ts/core_node/plugins.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// node native scope
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
|
||||
export { http, https, fs, path };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smarturl from '@push.rocks/smarturl';
|
||||
|
||||
export { smartpromise, smarturl };
|
||||
|
||||
// third party scope
|
||||
import { HttpAgent, HttpsAgent } from 'agentkeepalive';
|
||||
const agentkeepalive = { HttpAgent, HttpsAgent };
|
||||
import formData from 'form-data';
|
||||
|
||||
export { agentkeepalive, formData };
|
163
ts/core_node/request.ts
Normal file
163
ts/core_node/request.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
|
||||
|
||||
// Keep-alive agents for connection pooling
|
||||
const httpAgent = new plugins.agentkeepalive.HttpAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpAgentKeepAliveFalse = new plugins.agentkeepalive.HttpAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Node.js implementation of Core Request class that handles all HTTP/HTTPS requests
|
||||
*/
|
||||
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
|
||||
) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const incomingMessage = await this.fireCore();
|
||||
return new CoreResponse(incomingMessage, this.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw IncomingMessage
|
||||
*/
|
||||
async fireCore(): Promise<plugins.http.IncomingMessage> {
|
||||
const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();
|
||||
|
||||
// Parse URL
|
||||
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);
|
||||
}
|
||||
this.options.path = parsedUrl.path;
|
||||
|
||||
// Handle unix socket URLs
|
||||
if (CoreRequest.isUnixSocket(this.url)) {
|
||||
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(this.options.path);
|
||||
this.options.socketPath = socketPath;
|
||||
this.options.path = path;
|
||||
}
|
||||
|
||||
// Determine agent based on protocol and keep-alive setting
|
||||
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;
|
||||
} else if (this.options.keepAlive === false) {
|
||||
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;
|
||||
|
||||
if (!requestModule) {
|
||||
throw new Error(`The request to ${this.url} is missing a viable protocol. Must be http or https`);
|
||||
}
|
||||
|
||||
// Perform the request
|
||||
const request = requestModule.request(this.options, async (response) => {
|
||||
// Handle hard timeout
|
||||
if (this.options.hardDataCuttingTimeout) {
|
||||
setTimeout(() => {
|
||||
response.destroy();
|
||||
done.reject(new Error('Request timed out'));
|
||||
}, this.options.hardDataCuttingTimeout);
|
||||
}
|
||||
|
||||
// Always return the raw stream
|
||||
done.resolve(response);
|
||||
});
|
||||
|
||||
// Write request body
|
||||
if (this.options.requestBody) {
|
||||
if (this.options.requestBody instanceof plugins.formData) {
|
||||
this.options.requestBody.pipe(request).on('finish', () => {
|
||||
request.end();
|
||||
});
|
||||
} 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
|
||||
? this.options.requestBody
|
||||
: JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility
|
||||
request.write(bodyData);
|
||||
request.end();
|
||||
}
|
||||
} else if (this.requestDataFunc) {
|
||||
this.requestDataFunc(request);
|
||||
} else {
|
||||
request.end();
|
||||
}
|
||||
|
||||
// Handle request errors
|
||||
request.on('error', (e) => {
|
||||
console.error(e);
|
||||
request.destroy();
|
||||
done.reject(e);
|
||||
});
|
||||
|
||||
// Get response and handle response errors
|
||||
const response = await done.promise;
|
||||
response.on('error', (err) => {
|
||||
console.error(err);
|
||||
response.destroy();
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.ICoreRequestOptions = {}
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
|
||||
|
||||
/**
|
||||
* Modern Response class that provides a fetch-like API
|
||||
* Node.js implementation of Core Response class that provides a fetch-like API
|
||||
*/
|
||||
export class SmartResponse<T = any> implements types.ICoreResponse<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 consumed = false;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
@@ -17,6 +17,7 @@ export class SmartResponse<T = any> implements types.ICoreResponse<T> {
|
||||
public readonly url: string;
|
||||
|
||||
constructor(incomingMessage: plugins.http.IncomingMessage, url: string) {
|
||||
super();
|
||||
this.incomingMessage = incomingMessage;
|
||||
this.url = url;
|
||||
this.status = incomingMessage.statusCode || 0;
|
||||
@@ -25,16 +26,6 @@ export class SmartResponse<T = any> implements types.ICoreResponse<T> {
|
||||
this.headers = incomingMessage.headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the body can only be consumed once
|
||||
*/
|
||||
private ensureNotConsumed(): void {
|
||||
if (this.consumed) {
|
||||
throw new Error('Body has already been consumed');
|
||||
}
|
||||
this.consumed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the body as a buffer
|
||||
*/
|
||||
@@ -93,9 +84,44 @@ export class SmartResponse<T = any> implements types.ICoreResponse<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a readable stream
|
||||
* Get response as a web-style ReadableStream
|
||||
*/
|
||||
stream(): NodeJS.ReadableStream {
|
||||
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>({
|
||||
start(controller) {
|
||||
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
|
||||
*/
|
||||
streamNode(): NodeJS.ReadableStream {
|
||||
this.ensureNotConsumed();
|
||||
return this.incomingMessage;
|
||||
}
|
23
ts/core_node/types.ts
Normal file
23
ts/core_node/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as baseTypes from '../core_base/types.js';
|
||||
|
||||
// Re-export base types
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Extended IncomingMessage with body property (legacy compatibility)
|
||||
*/
|
||||
export interface IExtendedIncomingMessage<T = any> extends plugins.http.IncomingMessage {
|
||||
body: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js specific response extensions
|
||||
*/
|
||||
export interface INodeResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Node.js specific methods
|
||||
streamNode(): NodeJS.ReadableStream; // Returns Node.js style stream
|
||||
|
||||
// Legacy compatibility
|
||||
raw(): plugins.http.IncomingMessage;
|
||||
}
|
14
ts/index.ts
14
ts/index.ts
@@ -1,12 +1,10 @@
|
||||
// Legacy API exports (for backward compatibility)
|
||||
export * from './legacy/index.js';
|
||||
|
||||
// Modern API exports
|
||||
export * from './modern/index.js';
|
||||
// Client API exports
|
||||
export * from './client/index.js';
|
||||
|
||||
// Core exports for advanced usage
|
||||
export { SmartResponse, type ICoreRequestOptions, type ICoreResponse } from './core/index.js';
|
||||
export { CoreResponse } from './core/index.js';
|
||||
export type { ICoreRequestOptions, ICoreResponse } from './core_base/types.js';
|
||||
|
||||
// Default export for easier importing
|
||||
import { SmartRequestClient } from './modern/smartrequestclient.js';
|
||||
export default SmartRequestClient;
|
||||
import { SmartRequest } from './client/smartrequest.js';
|
||||
export default SmartRequest;
|
@@ -1,242 +0,0 @@
|
||||
/**
|
||||
* Legacy adapter that provides backward compatibility
|
||||
* Maps legacy API to the new core module
|
||||
*/
|
||||
|
||||
import * as core from '../core/index.js';
|
||||
import * as plugins from '../core/plugins.js';
|
||||
|
||||
const smartpromise = plugins.smartpromise;
|
||||
|
||||
// Re-export types for backward compatibility
|
||||
export { type IExtendedIncomingMessage } from '../core/types.js';
|
||||
export interface ISmartRequestOptions extends core.ICoreRequestOptions {
|
||||
autoJsonParse?: boolean;
|
||||
responseType?: 'json' | 'text' | 'binary' | 'stream';
|
||||
}
|
||||
|
||||
// Re-export interface for form fields
|
||||
export interface IFormField {
|
||||
name: string;
|
||||
type: 'string' | 'filePath' | 'Buffer';
|
||||
payload: string | Buffer;
|
||||
fileName?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert stream to IExtendedIncomingMessage for legacy compatibility
|
||||
*/
|
||||
async function streamToExtendedMessage(
|
||||
stream: plugins.http.IncomingMessage,
|
||||
autoJsonParse = true
|
||||
): Promise<core.IExtendedIncomingMessage> {
|
||||
const done = smartpromise.defer<core.IExtendedIncomingMessage>();
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
const extendedMessage = stream as core.IExtendedIncomingMessage;
|
||||
|
||||
if (autoJsonParse) {
|
||||
const text = buffer.toString('utf-8');
|
||||
try {
|
||||
extendedMessage.body = JSON.parse(text);
|
||||
} catch (err) {
|
||||
extendedMessage.body = text;
|
||||
}
|
||||
} else {
|
||||
extendedMessage.body = buffer;
|
||||
}
|
||||
|
||||
done.resolve(extendedMessage);
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
return done.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy request function that returns IExtendedIncomingMessage
|
||||
*/
|
||||
export async function request(
|
||||
urlArg: string,
|
||||
optionsArg: ISmartRequestOptions = {},
|
||||
responseStreamArg = false,
|
||||
requestDataFunc?: (req: plugins.http.ClientRequest) => void
|
||||
): Promise<core.IExtendedIncomingMessage> {
|
||||
const stream = await core.coreRequest(urlArg, optionsArg, requestDataFunc);
|
||||
|
||||
if (responseStreamArg) {
|
||||
// For stream responses, just cast and return
|
||||
return stream as core.IExtendedIncomingMessage;
|
||||
}
|
||||
|
||||
// Convert stream to IExtendedIncomingMessage
|
||||
const autoJsonParse = optionsArg.autoJsonParse !== false;
|
||||
return streamToExtendedMessage(stream, autoJsonParse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe GET request
|
||||
*/
|
||||
export async function safeGet(urlArg: string): Promise<core.IExtendedIncomingMessage | null> {
|
||||
const agentToUse = urlArg.startsWith('http://')
|
||||
? new plugins.http.Agent()
|
||||
: new plugins.https.Agent();
|
||||
|
||||
try {
|
||||
const response = await request(urlArg, {
|
||||
method: 'GET',
|
||||
agent: agentToUse,
|
||||
timeout: 5000,
|
||||
hardDataCuttingTimeout: 5000,
|
||||
autoJsonParse: false,
|
||||
});
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET JSON request
|
||||
*/
|
||||
export async function getJson(urlArg: string, optionsArg: ISmartRequestOptions = {}) {
|
||||
optionsArg.method = 'GET';
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST JSON request
|
||||
*/
|
||||
export async function postJson(urlArg: string, optionsArg: ISmartRequestOptions = {}) {
|
||||
optionsArg.method = 'POST';
|
||||
if (
|
||||
typeof optionsArg.requestBody === 'object' &&
|
||||
(!optionsArg.headers || !optionsArg.headers['Content-Type'])
|
||||
) {
|
||||
// make sure headers exist
|
||||
if (!optionsArg.headers) {
|
||||
optionsArg.headers = {};
|
||||
}
|
||||
|
||||
// assign the right Content-Type, leaving all other headers in place
|
||||
optionsArg.headers = {
|
||||
...optionsArg.headers,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT JSON request
|
||||
*/
|
||||
export async function putJson(urlArg: string, optionsArg: ISmartRequestOptions = {}) {
|
||||
optionsArg.method = 'PUT';
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE JSON request
|
||||
*/
|
||||
export async function delJson(urlArg: string, optionsArg: ISmartRequestOptions = {}) {
|
||||
optionsArg.method = 'DELETE';
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET binary data
|
||||
*/
|
||||
export async function getBinary(urlArg: string, optionsArg: ISmartRequestOptions = {}) {
|
||||
optionsArg = {
|
||||
...optionsArg,
|
||||
autoJsonParse: false,
|
||||
responseType: 'binary'
|
||||
};
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST form data
|
||||
*/
|
||||
export async function postFormData(urlArg: string, formFields: IFormField[], optionsArg: ISmartRequestOptions = {}) {
|
||||
const form = new plugins.formData();
|
||||
|
||||
for (const formField of formFields) {
|
||||
if (formField.type === 'filePath') {
|
||||
const fileData = plugins.fs.readFileSync(
|
||||
plugins.path.isAbsolute(formField.payload as string)
|
||||
? formField.payload as string
|
||||
: plugins.path.join(process.cwd(), formField.payload as string)
|
||||
);
|
||||
form.append(formField.name, fileData, {
|
||||
filename: formField.fileName || plugins.path.basename(formField.payload as string),
|
||||
contentType: formField.contentType
|
||||
});
|
||||
} else if (formField.type === 'Buffer') {
|
||||
form.append(formField.name, formField.payload, {
|
||||
filename: formField.fileName,
|
||||
contentType: formField.contentType
|
||||
});
|
||||
} else {
|
||||
form.append(formField.name, formField.payload);
|
||||
}
|
||||
}
|
||||
|
||||
optionsArg.method = 'POST';
|
||||
optionsArg.requestBody = form;
|
||||
if (!optionsArg.headers) {
|
||||
optionsArg.headers = {};
|
||||
}
|
||||
optionsArg.headers = {
|
||||
...optionsArg.headers,
|
||||
...form.getHeaders()
|
||||
};
|
||||
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST URL encoded form data
|
||||
*/
|
||||
export async function postFormDataUrlEncoded(
|
||||
urlArg: string,
|
||||
formFields: { key: string; content: string }[],
|
||||
optionsArg: ISmartRequestOptions = {}
|
||||
) {
|
||||
optionsArg.method = 'POST';
|
||||
if (!optionsArg.headers) {
|
||||
optionsArg.headers = {};
|
||||
}
|
||||
optionsArg.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
|
||||
const urlEncodedBody = formFields
|
||||
.map(field => `${encodeURIComponent(field.key)}=${encodeURIComponent(field.content)}`)
|
||||
.join('&');
|
||||
|
||||
optionsArg.requestBody = urlEncodedBody;
|
||||
|
||||
return request(urlArg, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET stream
|
||||
*/
|
||||
export async function getStream(
|
||||
urlArg: string,
|
||||
optionsArg: ISmartRequestOptions = {}
|
||||
): Promise<plugins.http.IncomingMessage> {
|
||||
optionsArg.method = 'GET';
|
||||
const response = await request(urlArg, optionsArg, true);
|
||||
return response;
|
||||
}
|
@@ -1,2 +0,0 @@
|
||||
// Export everything from the legacy adapter
|
||||
export * from './adapter.js';
|
Reference in New Issue
Block a user