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