Compare commits

...

17 Commits

Author SHA1 Message Date
2dc82bd730 BREAKING CHANGE(core): major architectural refactoring with cross-platform support and SmartRequest rename
Some checks failed
Default (tags) / security (push) Failing after 24s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-28 23:20:52 +00:00
8e75047d1f update 2025-07-28 22:50:12 +00:00
eb2ccd8d9f update 2025-07-28 22:37:36 +00:00
bc99aa3569 update 2025-07-28 17:23:48 +00:00
94bf23ad55 update 2025-07-28 17:15:35 +00:00
ea54a8aeda update 2025-07-28 17:07:24 +00:00
18d8ab0278 update 2025-07-28 17:01:34 +00:00
b8d707b363 update 2025-07-28 16:51:30 +00:00
7dcc5f3fe2 update 2025-07-28 15:12:11 +00:00
8f5c88b47e update 2025-07-28 15:12:04 +00:00
28a56b87bc update 2025-07-28 15:00:42 +00:00
d627bc870e update 2025-07-28 14:45:47 +00:00
2cded974a8 update 2025-07-28 14:38:09 +00:00
31c25c8333 update 2025-07-28 14:30:27 +00:00
01bbfa4a06 fix tests 2025-07-28 14:21:42 +00:00
0ebd47d1b2 update readme.md 2025-07-28 07:45:37 +00:00
bbb57004d9 BREAKING CHANGE(core): major architectural refactoring with fetch-like API
Some checks failed
Default (tags) / security (push) Failing after 24s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-27 21:23:20 +00:00
38 changed files with 3308 additions and 3298 deletions

View File

@@ -1,5 +1,61 @@
# Changelog
## 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
**Breaking Changes:**
- Legacy API functions are now imported from `@push.rocks/smartrequest/legacy` instead of the main export
- Modern API response objects now use fetch-like methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`) instead of direct `.body` access
- Renamed `responseType()` method to `accept()` in modern API
- Removed automatic defaults:
- No default keepAlive (must be explicitly set)
- No default timeouts
- No automatic JSON parsing in core
- Complete internal architecture refactoring:
- Core module now always returns raw streams
- Response parsing happens in SmartResponse methods
- Legacy API is now just an adapter over the core module
**Features:**
- New fetch-like response API with single-use body consumption
- Better TypeScript support and type safety
- Cleaner separation of concerns between request and response
- More predictable behavior aligned with fetch API standards
**Documentation:**
- Updated all examples to show correct import paths
- Added comprehensive examples for the new response API
- Enhanced migration guide
## 2025-04-03 - 2.1.0 - feat(docs)
Enhance documentation and tests with modern API usage examples and migration guide

View File

@@ -1,13 +1,16 @@
{
"name": "@push.rocks/smartrequest",
"version": "2.1.0",
"version": "4.0.0",
"private": false,
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"exports": {
".": "./dist_ts_web/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)",
"build": "(tsbuild --web)",
"buildDocs": "tsdoc"
},
@@ -29,23 +32,24 @@
"modern web requests",
"drop-in replacement"
],
"author": "Lossless GmbH",
"author": "Task Venture Capital GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/push.rocks/smartrequest/issues"
},
"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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,8 @@
# SmartRequest Architecture Hints
## Core Features
- supports http
- supports https
- supports https
- supports unix socks
- supports formData
- supports file uploads
@@ -8,4 +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 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 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`: Unified TypeScript interfaces and types
- Single `ICoreRequestOptions` interface for all implementations
- Implementations handle unsupported options by throwing errors
### Core Node Module (ts/core_node/)
- `request.ts`: Node.js implementation using http/https modules
- Supports unix socket connections and keep-alive agents
- Converts Node.js specific options from unified interface
- `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
### Stream Handling
- `stream()` method always returns web-style ReadableStream<Uint8Array>
- In Node.js, converts native streams to web streams
- `streamNode()` available only in Node.js environment for native streams
- Consistent API across platforms while preserving platform-specific capabilities
### Binary Request Handling
- Binary requests handled through ArrayBuffer API
- Response body kept as Buffer/ArrayBuffer without string conversion
- No automatic transformations applied to binary data
## Testing
- Use `pnpm test` to run all tests
- Tests use @git.zone/tstest/tapbundle for assertions
- Separate test files for Node.js (test.node.ts) and browser (test.browser.ts)
- Browser tests run in headless Chromium via puppeteer

521
readme.md
View File

@@ -1,170 +1,101 @@
# @push.rocks/smartrequest
A module providing a drop-in replacement for the deprecated Request library, focusing on modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, and streams. The library offers both a legacy API and a modern fluent API for maximum flexibility.
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()`)
- 🌐 **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 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
- 🔧 **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. It supports a range of features, including handling form data, file uploads, JSON requests, binary data, streaming, pagination, and much more, all within a modern, promise-based API.
The library provides two distinct APIs:
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for modern applications.
1. **Legacy API** - Simple function-based API for quick and straightforward HTTP requests
2. **Modern Fluent API** - A chainable, builder-style API for more complex scenarios and better TypeScript integration
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.
### Simple GET Request
For fetching data from a REST API or any web service that returns JSON:
### Basic Usage
```typescript
import { getJson } from '@push.rocks/smartrequest';
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';
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';
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';
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';
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 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();
console.log(response.body); // The JSON response
// Use the fetch-like response API
const userData = await response.json();
console.log(userData); // The parsed JSON response
}
// 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();
console.log(response.body); // The created post
const createdPost = await response.json();
console.log(createdPost); // The created post
}
```
### 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({
@@ -173,71 +104,170 @@ async function searchRepositories(query: string, perPage: number = 10) {
})
.get();
return response.body.items;
const data = await response.json();
return data.items;
}
```
### 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
.get();
return response.body;
return await response.json();
}
```
### Working with Different Response Types
The API provides a fetch-like interface for handling different response types:
```typescript
import { SmartRequestClient } from '@push.rocks/smartrequest';
import { SmartRequest } from '@push.rocks/smartrequest';
// JSON response (default)
async function fetchJson(url: string) {
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();
return await response.text(); // Returns response as string
}
// Binary data
async function downloadImage(url: string) {
const response = await SmartRequestClient.create()
const response = await SmartRequest.create()
.url(url)
.responseType('binary')
.accept('binary') // Optional: hints to server we want binary
.get();
// response.body is a Buffer
return response.body;
const buffer = await response.arrayBuffer();
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)
.responseType('stream')
.get();
// response is a stream
response.on('data', (chunk) => {
// 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();
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) => {
response.on('end', resolve);
response.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',
@@ -265,7 +295,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',
@@ -279,7 +309,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()
@@ -289,50 +319,183 @@ async function fetchAllIssues(repo: string) {
}
```
### Convenience Factory Functions
The library provides several factory functions for common use cases:
### Keep-Alive Connections (Node.js)
```typescript
import { createJsonClient, createBinaryClient, createStreamClient } from '@push.rocks/smartrequest';
import { SmartRequest } from '@push.rocks/smartrequest';
// Pre-configured for JSON requests
const jsonClient = createJsonClient()
// Enable keep-alive for better performance with multiple requests
async function performMultipleRequests() {
const client = SmartRequest.create()
.header('Connection', 'keep-alive');
// Requests will reuse the same connection in Node.js
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()
]);
return Promise.all(results.map(r => r.json()));
}
```
## 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')
.get();
// Pre-configured for binary data
const binaryClient = createBinaryClient()
.url('https://example.com/image.jpg')
.get();
// Pre-configured for streaming
const streamClient = createStreamClient()
.url('https://example.com/large-file')
.option({
credentials: 'include', // Include cookies
mode: 'cors', // CORS mode
cache: 'no-cache', // Cache mode
referrerPolicy: 'no-referrer'
})
.get();
```
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.
### Node.js-Specific Options
## Migration Guide: Legacy API to Modern API
When running in Node.js, you can use Node-specific options:
If you're currently using the legacy API and want to migrate to the modern fluent API, here's a quick reference guide:
```typescript
import { Agent } from 'https';
| 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).responseType('stream').get()` |
| `request(url, options)` | `SmartRequestClient.create().url(url).[...configure options...].get()` |
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.option({
agent: new Agent({ keepAlive: true }), // Custom agent
socketPath: '/var/run/api.sock', // Unix socket
})
.get();
```
The modern API provides more flexibility and better TypeScript integration, making it the recommended approach for new projects.
## 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;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
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();
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[]>();
}
}
// Usage
const api = new BlogApiClient();
const user = await api.getUser(1);
const posts = await api.getAllPosts(user.id);
```
## Error Handling
```typescript
import { SmartRequest } from '@push.rocks/smartrequest';
async function fetchWithErrorHandling(url: string) {
try {
const response = await SmartRequest.create()
.url(url)
.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/')) {
return await response.text();
} else {
return await response.arrayBuffer();
}
} catch (error) {
if (error.code === 'ECONNREFUSED') {
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);
}
throw error;
}
}
```
## 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.
@@ -342,9 +505,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.

102
test/test.browser.ts Normal file
View File

@@ -0,0 +1,102 @@
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: 1000
};
try {
const request = new CoreRequest('https://httpbin.org/delay/10', options);
await request.fire();
} catch (error) {
timedOut = true;
expect(error.message).toContain('timed out');
}
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: {
foo: 'bar',
baz: 'qux'
}
};
const request = new CoreRequest('https://httpbin.org/get', options);
const response = await request.fire();
expect(response.status).toEqual(200);
const data = await response.json();
expect(data.args).toHaveProperty('foo');
expect(data.args.foo).toEqual('bar');
expect(data.args).toHaveProperty('baz');
expect(data.args.baz).toEqual('qux');
});
export default tap.start();

View File

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

View File

@@ -1,86 +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).toHaveProperty('body');
});
tap.test('modern: should request a JSON document over https', async () => {
const response = await SmartRequestClient.create()
.url('https://jsonplaceholder.typicode.com/posts/1')
.get();
expect(response.body).toHaveProperty('id');
expect(response.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();
expect(response.body).toHaveProperty('json');
expect(response.body.json).toHaveProperty('text');
expect(response.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();
expect(response.body).toHaveProperty('headers');
// Check if the header exists (case-sensitive)
expect(response.body.headers).toHaveProperty(customHeader);
expect(response.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();
expect(response.body).toHaveProperty('args');
expect(response.body.args).toHaveProperty('param1');
expect(response.body.args.param1).toEqual('value1');
expect(response.body.args).toHaveProperty('param2');
expect(response.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('body');
});
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('body');
});
tap.start();

95
test/test.node.ts Normal file
View File

@@ -0,0 +1,95 @@
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 = { text: 'example_text' };
const response = await SmartRequest.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('client: should set headers correctly', async () => {
const customHeader = 'X-Custom-Header';
const headerValue = 'test-value';
const response = await SmartRequest.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('client: should handle query parameters', async () => {
const params = { param1: 'value1', param2: 'value2' };
const response = await SmartRequest.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('client: should handle timeout configuration', async () => {
// This test just verifies that the timeout method doesn't throw
const client = SmartRequest.create()
.url('https://httpbin.org/get')
.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://httpbin.org/get')
.retry(1);
const response = await client.get();
expect(response).toHaveProperty('ok');
expect(response.ok).toBeTrue();
});
tap.start();

View File

@@ -1,19 +1,23 @@
import { type IExtendedIncomingMessage } from '../../legacy/smartrequest.request.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 function createPaginatedResponse<T>(
response: IExtendedIncomingMessage<any>,
export async function createPaginatedResponse<T>(
response: ICoreResponse<any>,
paginationConfig: TPaginationConfig,
queryParams: Record<string, string>,
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
): TPaginatedResponse<T> {
): Promise<TPaginatedResponse<T>> {
// Parse response body first
const body = await response.json() as any;
// Default to response.body for items if response is JSON
let items: T[] = Array.isArray(response.body)
? response.body
: (response.body?.items || response.body?.data || response.body?.results || []);
let items: T[] = Array.isArray(body)
? body
: (body?.items || body?.data || body?.results || []);
let hasNextPage = false;
let nextPageParams: Record<string, string> = {};
@@ -24,7 +28,7 @@ export function createPaginatedResponse<T>(
const config = paginationConfig;
const currentPage = parseInt(queryParams[config.pageParam || 'page'] || String(config.startPage || 1));
const limit = parseInt(queryParams[config.limitParam || 'limit'] || String(config.pageSize || 20));
const total = getValueByPath(response.body, config.totalPath || 'total') || 0;
const total = getValueByPath(body, config.totalPath || 'total') || 0;
hasNextPage = currentPage * limit < total;
@@ -39,8 +43,8 @@ export function createPaginatedResponse<T>(
case PaginationStrategy.CURSOR: {
const config = paginationConfig;
const nextCursor = getValueByPath(response.body, config.cursorPath || 'nextCursor');
const hasMore = getValueByPath(response.body, config.hasMorePath || 'hasMore');
const nextCursor = getValueByPath(body, config.cursorPath || 'nextCursor');
const hasMore = getValueByPath(body, config.hasMorePath || 'hasMore');
hasNextPage = !!nextCursor || !!hasMore;

View File

@@ -1,5 +1,8 @@
// Export the main client
export { SmartRequestClient } from './smartrequestclient.js';
export { SmartRequest } from './smartrequest.js';
// Export response type from core
export { CoreResponse } from '../core/index.js';
// Export types
export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig } from './types/common.js';
@@ -14,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>().responseType('binary');
return SmartRequest.create<T>().accept('binary');
}
/**
* Create a client pre-configured for streaming
*/
export function createStreamClient() {
return SmartRequestClient.create().responseType('stream');
return SmartRequest.create().accept('stream');
}

6
ts/client/plugins.ts Normal file
View File

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

View File

@@ -1,6 +1,7 @@
import { type ISmartRequestOptions } from '../legacy/smartrequest.interfaces.js';
import { request, type IExtendedIncomingMessage } from '../legacy/smartrequest.request.js';
import * as plugins from '../legacy/smartrequest.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 {
@@ -16,20 +17,18 @@ import { createPaginatedResponse } from './features/pagination.js';
/**
* Modern fluent client for making HTTP requests
*/
export class SmartRequestClient<T = any> {
export class SmartRequest<T = any> {
private _url: string;
private _options: ISmartRequestOptions = {};
private _responseType: ResponseType = 'json';
private _timeoutMs: number = 60000;
private _options: ICoreRequestOptions = {};
private _retries: number = 0;
private _queryParams: Record<string, string> = {};
private _paginationConfig?: TPaginationConfig;
/**
* 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>();
}
/**
@@ -94,7 +93,6 @@ export class SmartRequestClient<T = any> {
* Set request timeout in milliseconds
*/
timeout(ms: number): this {
this._timeoutMs = ms;
this._options.timeout = ms;
this._options.hardDataCuttingTimeout = ms;
return this;
@@ -145,16 +143,18 @@ export class SmartRequestClient<T = any> {
}
/**
* Set response type
* Set the Accept header to indicate what content type is expected
*/
responseType(type: ResponseType): this {
this._responseType = type;
if (type === 'binary' || type === 'stream') {
this._options.autoJsonParse = false;
}
return this;
accept(type: ResponseType): this {
// Map response types to Accept header values
const acceptHeaders: Record<ResponseType, string> = {
'json': 'application/json',
'text': 'text/plain',
'binary': 'application/octet-stream',
'stream': '*/*'
};
return this.header('Accept', acceptHeaders[type]);
}
/**
@@ -225,35 +225,35 @@ export class SmartRequestClient<T = any> {
/**
* Make a GET request
*/
async get<R = T>(): Promise<IExtendedIncomingMessage<R>> {
async get<R = T>(): Promise<ICoreResponse<R>> {
return this.execute<R>('GET');
}
/**
* Make a POST request
*/
async post<R = T>(): Promise<IExtendedIncomingMessage<R>> {
async post<R = T>(): Promise<ICoreResponse<R>> {
return this.execute<R>('POST');
}
/**
* Make a PUT request
*/
async put<R = T>(): Promise<IExtendedIncomingMessage<R>> {
async put<R = T>(): Promise<ICoreResponse<R>> {
return this.execute<R>('PUT');
}
/**
* Make a DELETE request
*/
async delete<R = T>(): Promise<IExtendedIncomingMessage<R>> {
async delete<R = T>(): Promise<ICoreResponse<R>> {
return this.execute<R>('DELETE');
}
/**
* Make a PATCH request
*/
async patch<R = T>(): Promise<IExtendedIncomingMessage<R>> {
async patch<R = T>(): Promise<ICoreResponse<R>> {
return this.execute<R>('PATCH');
}
@@ -272,13 +272,13 @@ export class SmartRequestClient<T = any> {
const response = await this.execute();
return createPaginatedResponse<ItemType>(
return await createPaginatedResponse<ItemType>(
response,
this._paginationConfig,
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;
@@ -298,7 +298,7 @@ export class SmartRequestClient<T = any> {
/**
* Execute the HTTP request
*/
private async execute<R = T>(method?: HttpMethod): Promise<IExtendedIncomingMessage<R>> {
private async execute<R = T>(method?: HttpMethod): Promise<ICoreResponse<R>> {
if (method) {
this._options.method = method;
}
@@ -310,28 +310,9 @@ export class SmartRequestClient<T = any> {
for (let attempt = 0; attempt <= this._retries; attempt++) {
try {
if (this._responseType === 'stream') {
return await request(this._url, this._options, true) as IExtendedIncomingMessage<R>;
} else if (this._responseType === 'binary') {
const response = await request(this._url, this._options, true);
// Handle binary response
const dataPromise = plugins.smartpromise.defer<Buffer>();
const chunks: Buffer[] = [];
response.on('data', (chunk: Buffer) => chunks.push(chunk));
response.on('end', () => {
const buffer = Buffer.concat(chunks);
(response as IExtendedIncomingMessage<R>).body = buffer as any;
dataPromise.resolve();
});
await dataPromise.promise;
return response as IExtendedIncomingMessage<R>;
} else {
// Handle JSON or text response
return await request(this._url, this._options) as IExtendedIncomingMessage<R>;
}
const request = new CoreRequest(this._url, this._options as any);
const response = await request.fire();
return response as ICoreResponse<R>;
} catch (error) {
lastError = error as Error;

View File

@@ -1,4 +1,5 @@
import { type IExtendedIncomingMessage } from '../../legacy/smartrequest.request.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: IExtendedIncomingMessage<any>) => boolean;
getNextPageParams: (response: IExtendedIncomingMessage<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: IExtendedIncomingMessage<any>; // Original response
response: ICoreResponse<any>; // Original response
}

30
ts/core/index.ts Normal file
View File

@@ -0,0 +1,30 @@
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 };

4
ts/core/plugins.ts Normal file
View File

@@ -0,0 +1,4 @@
import * as smartenv from '@push.rocks/smartenv';
import * as smartpath from '@push.rocks/smartpath/iso';
export { smartenv, smartpath };

4
ts/core_base/index.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

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

View File

@@ -13,7 +13,8 @@ import * as smarturl from '@push.rocks/smarturl';
export { smartpromise, smarturl };
// third party scope
import agentkeepalive from 'agentkeepalive';
import { HttpAgent, HttpsAgent } from 'agentkeepalive';
const agentkeepalive = { HttpAgent, HttpsAgent };
import formData from 'form-data';
export { agentkeepalive, formData };
export { agentkeepalive, formData };

163
ts/core_node/request.ts Normal file
View 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();
}
}

136
ts/core_node/response.ts Normal file
View File

@@ -0,0 +1,136 @@
import * as plugins from './plugins.js';
import * as types from './types.js';
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
/**
* Node.js implementation of Core Response class that provides a fetch-like API
*/
export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements types.INodeResponse<T> {
private incomingMessage: plugins.http.IncomingMessage;
private bodyBufferPromise: Promise<Buffer> | null = null;
// Public properties
public readonly ok: boolean;
public readonly status: number;
public readonly statusText: string;
public readonly headers: plugins.http.IncomingHttpHeaders;
public readonly url: string;
constructor(incomingMessage: plugins.http.IncomingMessage, url: string) {
super();
this.incomingMessage = incomingMessage;
this.url = url;
this.status = incomingMessage.statusCode || 0;
this.statusText = incomingMessage.statusMessage || '';
this.ok = this.status >= 200 && this.status < 300;
this.headers = incomingMessage.headers;
}
/**
* Collects the body as a buffer
*/
private async collectBody(): Promise<Buffer> {
this.ensureNotConsumed();
if (this.bodyBufferPromise) {
return this.bodyBufferPromise;
}
this.bodyBufferPromise = new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
this.incomingMessage.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
this.incomingMessage.on('end', () => {
resolve(Buffer.concat(chunks));
});
this.incomingMessage.on('error', reject);
});
return this.bodyBufferPromise;
}
/**
* Parse response as JSON
*/
async json(): Promise<T> {
const buffer = await this.collectBody();
const text = buffer.toString('utf-8');
try {
return JSON.parse(text);
} catch (error) {
throw new Error(`Failed to parse JSON: ${error.message}`);
}
}
/**
* Get response as text
*/
async text(): Promise<string> {
const buffer = await this.collectBody();
return buffer.toString('utf-8');
}
/**
* Get response as ArrayBuffer
*/
async arrayBuffer(): Promise<ArrayBuffer> {
const buffer = await this.collectBody();
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
/**
* Get response as a web-style 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;
}
/**
* Get the raw IncomingMessage (for legacy compatibility)
*/
raw(): plugins.http.IncomingMessage {
return this.incomingMessage;
}
}

23
ts/core_node/types.ts Normal file
View 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;
}

View File

@@ -1,16 +1,10 @@
// Legacy API exports (for backward compatibility)
export { request, safeGet } from './legacy/smartrequest.request.js';
export type { IExtendedIncomingMessage } from './legacy/smartrequest.request.js';
export type { ISmartRequestOptions } from './legacy/smartrequest.interfaces.js';
// Client API exports
export * from './client/index.js';
export * from './legacy/smartrequest.jsonrest.js';
export * from './legacy/smartrequest.binaryrest.js';
export * from './legacy/smartrequest.formdata.js';
export * from './legacy/smartrequest.stream.js';
// Modern API exports
export * from './modern/index.js';
import { SmartRequestClient } from './modern/smartrequestclient.js';
// Core exports for advanced usage
export { CoreResponse } from './core/index.js';
export type { ICoreRequestOptions, ICoreResponse } from './core_base/types.js';
// Default export for easier importing
export default SmartRequestClient;
import { SmartRequest } from './client/smartrequest.js';
export default SmartRequest;

View File

@@ -1,8 +0,0 @@
export { request, safeGet } from './smartrequest.request.js';
export type { IExtendedIncomingMessage } from './smartrequest.request.js';
export type { ISmartRequestOptions } from './smartrequest.interfaces.js';
export * from './smartrequest.jsonrest.js';
export * from './smartrequest.binaryrest.js';
export * from './smartrequest.formdata.js';
export * from './smartrequest.stream.js';

View File

@@ -1,33 +0,0 @@
// this file implements methods to get and post binary data.
import * as interfaces from './smartrequest.interfaces.js';
import { request, type IExtendedIncomingMessage } from './smartrequest.request.js';
import * as plugins from './smartrequest.plugins.js';
export const getBinary = async (
domainArg: string,
optionsArg: interfaces.ISmartRequestOptions = {}
) => {
optionsArg = {
...optionsArg,
autoJsonParse: false,
};
const done = plugins.smartpromise.defer();
const response = await request(domainArg, optionsArg, true);
const data: Array<Buffer> = [];
response
.on('data', function (chunk: Buffer) {
data.push(chunk);
})
.on('end', function () {
//at this point data is an array of Buffers
//so Buffer.concat() can make us a new Buffer
//of all of them together
const buffer = Buffer.concat(data);
response.body = buffer;
done.resolve();
});
await done.promise;
return response as IExtendedIncomingMessage<Buffer>;
};

View File

@@ -1,99 +0,0 @@
import * as plugins from './smartrequest.plugins.js';
import * as interfaces from './smartrequest.interfaces.js';
import { request } from './smartrequest.request.js';
/**
* the interfae for FormFieldData
*/
export interface IFormField {
name: string;
type: 'string' | 'filePath' | 'Buffer';
payload: string | Buffer;
fileName?: string;
contentType?: string;
}
const appendFormField = async (formDataArg: plugins.formData, formDataField: IFormField) => {
switch (formDataField.type) {
case 'string':
formDataArg.append(formDataField.name, formDataField.payload);
break;
case 'filePath':
if (typeof formDataField.payload !== 'string') {
throw new Error(
`Payload for key ${
formDataField.name
} must be of type string. Got ${typeof formDataField.payload} instead.`
);
}
const fileData = plugins.fs.readFileSync(
plugins.path.join(process.cwd(), formDataField.payload)
);
formDataArg.append('file', fileData, {
filename: formDataField.fileName ? formDataField.fileName : 'upload.pdf',
contentType: 'application/pdf',
});
break;
case 'Buffer':
formDataArg.append(formDataField.name, formDataField.payload, {
filename: formDataField.fileName ? formDataField.fileName : 'upload.pdf',
contentType: formDataField.contentType ? formDataField.contentType : 'application/pdf',
});
break;
}
};
export const postFormData = async (
urlArg: string,
optionsArg: interfaces.ISmartRequestOptions = {},
payloadArg: IFormField[]
) => {
const form = new plugins.formData();
for (const formField of payloadArg) {
await appendFormField(form, formField);
}
const requestOptions = {
...optionsArg,
method: 'POST',
headers: {
...optionsArg.headers,
...form.getHeaders(),
},
requestBody: form,
};
// lets fire the actual request for sending the formdata
const response = await request(urlArg, requestOptions);
return response;
};
export const postFormDataUrlEncoded = async (
urlArg: string,
optionsArg: interfaces.ISmartRequestOptions = {},
payloadArg: { key: string; content: string }[]
) => {
let resultString = '';
for (const keyContentPair of payloadArg) {
if (resultString) {
resultString += '&';
}
resultString += `${encodeURIComponent(keyContentPair.key)}=${encodeURIComponent(
keyContentPair.content
)}`;
}
const requestOptions: interfaces.ISmartRequestOptions = {
...optionsArg,
method: 'POST',
headers: {
...optionsArg.headers,
'content-type': 'application/x-www-form-urlencoded',
},
requestBody: resultString,
};
// lets fire the actual request for sending the formdata
const response = await request(urlArg, requestOptions);
return response;
};

View File

@@ -1,10 +0,0 @@
import * as plugins from './smartrequest.plugins.js';
import * as https from 'https';
export interface ISmartRequestOptions extends https.RequestOptions {
keepAlive?: boolean;
requestBody?: any;
autoJsonParse?: boolean;
queryParams?: { [key: string]: string };
hardDataCuttingTimeout?: number;
}

View File

@@ -1,63 +0,0 @@
// This file implements methods to get and post JSON in a simple manner.
import * as interfaces from './smartrequest.interfaces.js';
import { request } from './smartrequest.request.js';
/**
* gets Json and puts the right headers + handles response aggregation
* @param domainArg
* @param optionsArg
*/
export const getJson = async (
domainArg: string,
optionsArg: interfaces.ISmartRequestOptions = {}
) => {
optionsArg.method = 'GET';
optionsArg.headers = {
...optionsArg.headers,
};
let response = await request(domainArg, optionsArg);
return response;
};
export const postJson = async (
domainArg: string,
optionsArg: interfaces.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',
};
}
let response = await request(domainArg, optionsArg);
return response;
};
export const putJson = async (
domainArg: string,
optionsArg: interfaces.ISmartRequestOptions = {}
) => {
optionsArg.method = 'PUT';
let response = await request(domainArg, optionsArg);
return response;
};
export const delJson = async (
domainArg: string,
optionsArg: interfaces.ISmartRequestOptions = {}
) => {
optionsArg.method = 'DELETE';
let response = await request(domainArg, optionsArg);
return response;
};

View File

@@ -1,231 +0,0 @@
import * as plugins from './smartrequest.plugins.js';
import * as interfaces from './smartrequest.interfaces.js';
export interface IExtendedIncomingMessage<T = any> extends plugins.http.IncomingMessage {
body: T;
}
const buildUtf8Response = (
incomingMessageArg: plugins.http.IncomingMessage,
autoJsonParse = true
): Promise<IExtendedIncomingMessage> => {
const done = plugins.smartpromise.defer<IExtendedIncomingMessage>();
// Continuously update stream with data
let body = '';
incomingMessageArg.on('data', (chunkArg) => {
body += chunkArg;
});
incomingMessageArg.on('end', () => {
if (autoJsonParse) {
try {
(incomingMessageArg as IExtendedIncomingMessage).body = JSON.parse(body);
} catch (err) {
(incomingMessageArg as IExtendedIncomingMessage).body = body;
}
} else {
(incomingMessageArg as IExtendedIncomingMessage).body = body;
}
done.resolve(incomingMessageArg as IExtendedIncomingMessage);
});
return done.promise;
};
/**
* determine wether a url is a unix sock
* @param urlArg
*/
const testForUnixSock = (urlArg: string): boolean => {
const unixRegex = /^(http:\/\/|https:\/\/|)unix:/;
return unixRegex.test(urlArg);
};
/**
* determine socketPath and path for unixsock
*/
const parseSocketPathAndRoute = (stringToParseArg: string) => {
const parseRegex = /(.*):(.*)/;
const result = parseRegex.exec(stringToParseArg);
return {
socketPath: result[1],
path: result[2],
};
};
/**
* a custom http agent to make sure we can set custom keepAlive options for speedy subsequent calls
*/
const httpAgent = new plugins.agentkeepalive({
keepAlive: true,
maxFreeSockets: 10,
maxSockets: 100,
maxTotalSockets: 1000,
timeout: 60000,
});
/**
* a custom http agent to make sure we can set custom keepAlive options for speedy subsequent calls
*/
const httpAgentKeepAliveFalse = new plugins.agentkeepalive({
keepAlive: false,
timeout: 60000,
});
/**
* a custom https agent to make sure we can set custom keepAlive options for speedy subsequent calls
*/
const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
keepAlive: true,
maxFreeSockets: 10,
maxSockets: 100,
maxTotalSockets: 1000,
timeout: 60000,
});
/**
* a custom https agent to make sure we can set custom keepAlive options for speedy subsequent calls
*/
const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
keepAlive: false,
timeout: 60000,
});
export let request = async (
urlArg: string,
optionsArg: interfaces.ISmartRequestOptions = {},
responseStreamArg: boolean = false,
requestDataFunc: (req: plugins.http.ClientRequest) => void = null
): Promise<IExtendedIncomingMessage> => {
const done = plugins.smartpromise.defer<IExtendedIncomingMessage>();
// merge options
const defaultOptions: interfaces.ISmartRequestOptions = {
// agent: agent,
autoJsonParse: true,
keepAlive: true,
};
optionsArg = {
...defaultOptions,
...optionsArg,
};
// 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;
optionsArg.queryParams = parsedUrl.searchParams;
// determine if unixsock
if (testForUnixSock(urlArg)) {
const detailedUnixPath = parseSocketPathAndRoute(optionsArg.path);
optionsArg.socketPath = detailedUnixPath.socketPath;
optionsArg.path = detailedUnixPath.path;
}
// TODO: support tcp sockets
// lets determine agent
switch (true) {
case !!optionsArg.agent:
break;
case parsedUrl.protocol === 'https:' && optionsArg.keepAlive:
optionsArg.agent = httpsAgent;
break;
case parsedUrl.protocol === 'https:' && !optionsArg.keepAlive:
optionsArg.agent = httpsAgentKeepAliveFalse;
break;
case parsedUrl.protocol === 'http:' && optionsArg.keepAlive:
optionsArg.agent = httpAgent;
break;
case parsedUrl.protocol === 'http:' && !optionsArg.keepAlive:
optionsArg.agent = httpAgentKeepAliveFalse;
break;
}
// lets determine the request module to use
const requestModule = (() => {
switch (true) {
case parsedUrl.protocol === 'https:':
return plugins.https;
case parsedUrl.protocol === 'http:':
return plugins.http;
}
})() as typeof plugins.https;
if (!requestModule) {
console.error(`The request to ${urlArg} is missing a viable protocol. Must be http or https`);
return;
}
// lets perform the actual request
const requestToFire = requestModule.request(optionsArg, async (resArg) => {
if (optionsArg.hardDataCuttingTimeout) {
setTimeout(() => {
resArg.destroy();
done.reject(new Error('Request timed out'));
}, optionsArg.hardDataCuttingTimeout)
}
if (responseStreamArg) {
done.resolve(resArg as IExtendedIncomingMessage);
} else {
const builtResponse = await buildUtf8Response(resArg, optionsArg.autoJsonParse);
done.resolve(builtResponse);
}
});
// lets write the requestBody
if (optionsArg.requestBody) {
if (optionsArg.requestBody instanceof plugins.formData) {
optionsArg.requestBody.pipe(requestToFire).on('finish', (event: any) => {
requestToFire.end();
});
} else {
if (typeof optionsArg.requestBody !== 'string') {
optionsArg.requestBody = JSON.stringify(optionsArg.requestBody);
}
requestToFire.write(optionsArg.requestBody);
requestToFire.end();
}
} else if (requestDataFunc) {
requestDataFunc(requestToFire);
} else {
requestToFire.end();
}
// lets handle an error
requestToFire.on('error', (e) => {
console.error(e);
requestToFire.destroy();
});
const response = await done.promise;
response.on('error', (err) => {
console.log(err);
response.destroy();
});
return response;
};
export const safeGet = async (urlArg: string) => {
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.log(err);
return null;
}
};

View File

@@ -1,17 +0,0 @@
import * as plugins from './smartrequest.plugins.js';
import * as interfaces from './smartrequest.interfaces.js';
import { request } from './smartrequest.request.js';
export const getStream = async (
urlArg: string,
optionsArg: interfaces.ISmartRequestOptions = {}
): Promise<plugins.http.IncomingMessage> => {
try {
// Call the existing request function with responseStreamArg set to true.
const responseStream = await request(urlArg, optionsArg, true);
return responseStream;
} catch (err) {
console.error('An error occurred while getting the stream:', err);
throw err; // Rethrow the error to be handled by the caller.
}
};