Compare commits

..

14 Commits

Author SHA1 Message Date
1305b92ebe v4.4.2
Some checks failed
Default (tags) / security (push) Successful in 5s
Default (tags) / test (push) Failing after 1m41s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-16 23:41:24 +00:00
8b52ca1021 fix(core_base/request): Strip unix: prefix when parsing unix socket URLs so socketPath is a clean filesystem path 2025-11-16 23:41:24 +00:00
e14800f077 v4.4.1
Some checks failed
Default (tags) / security (push) Successful in 13s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-16 23:22:58 +00:00
9f3503704b fix(core_node): Fix unix socket URL parsing and handling in CoreRequest 2025-11-16 23:22:58 +00:00
f3ba77050a v4.4.0
Some checks failed
Default (tags) / security (push) Successful in 13s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-16 22:50:19 +00:00
6211acd60b feat(core): Add Bun and Deno runtime support, unify core loader, unix-socket support and cross-runtime streaming/tests 2025-11-16 22:50:19 +00:00
32332309dc v4.3.8
Some checks failed
Default (tags) / security (push) Successful in 13s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-16 21:55:40 +00:00
9d29bd92da fix(core): Ensure correct ArrayBuffer return, fix fetch body typing, reorganize node-only tests, and bump tsbuild devDependency 2025-11-16 21:55:40 +00:00
6d148bb59e 4.3.7
Some checks failed
Default (tags) / security (push) Successful in 10s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-01 15:53:47 +00:00
e0f586693c fix(ci): Update dependencies, add deno.lock, and reorganize tests for browser and Node environments 2025-11-01 15:53:47 +00:00
df28cd4778 4.3.6
Some checks failed
Default (tags) / security (push) Successful in 4s
Default (tags) / test (push) Successful in 43s
Default (tags) / release (push) Failing after 29s
Default (tags) / metadata (push) Successful in 54s
2025-10-26 14:12:38 +00:00
f49cbd2b6a fix(ci): Use .npmrc for registry authentication in Gitea workflow and add conditional npmjs publish 2025-10-26 14:12:37 +00:00
984b53cba2 4.3.5
Some checks failed
Default (tags) / security (push) Successful in 11s
Default (tags) / test (push) Successful in 47s
Default (tags) / release (push) Failing after 29s
Default (tags) / metadata (push) Successful in 57s
2025-10-26 14:07:55 +00:00
4c55243646 fix(workflows): Remove npmci wrappers from CI workflows and use pnpm/npm CLI directly 2025-10-26 14:07:55 +00:00
34 changed files with 11135 additions and 3514 deletions

View File

@@ -23,24 +23,16 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
npm config set registry https://registry.npmjs.org
pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
npm config set registry https://registry.npmjs.org
pnpm audit --audit-level=high --dev
continue-on-error: true
test:
@@ -55,12 +47,10 @@ jobs:
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
pnpm install
pnpm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
pnpm install
pnpm build

View File

@@ -23,22 +23,16 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
npm config set registry https://registry.npmjs.org
pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
npm config set registry https://registry.npmjs.org
pnpm audit --audit-level=high --dev
continue-on-error: true
test:
@@ -51,23 +45,15 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
pnpm install
pnpm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
pnpm install
pnpm build
release:
needs: test
@@ -79,28 +65,26 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
run: |
npmci node install stable
npmci npm install
pnpm install
# Extract server host from GITHUB_SERVER_URL (remove https://)
GITEA_HOST="${GITHUB_SERVER_URL#https://}"
GITEA_REGISTRY="$GITHUB_SERVER_URL/api/packages/$GITHUB_REPOSITORY_OWNER/npm/"
# Configure Gitea npm registry
npmci command npm config set @${GITHUB_REPOSITORY_OWNER}:registry "$GITEA_REGISTRY"
npmci command npm config set "//${GITEA_HOST}/api/packages/${GITHUB_REPOSITORY_OWNER}/npm/:_authToken" "$GITEA_TOKEN"
# Create .npmrc for Gitea authentication
echo "@${GITHUB_REPOSITORY_OWNER}:registry=${GITEA_REGISTRY}" > .npmrc
echo "//${GITEA_HOST}/api/packages/${GITHUB_REPOSITORY_OWNER}/npm/:_authToken=${GITEA_TOKEN}" >> .npmrc
# Publish to Gitea
npmci command npm publish
pnpm publish --no-git-checks
# Conditionally publish to npmjs.org if token exists
if [ -n "$NPMCI_TOKEN_NPM" ]; then
npmci command npm config set registry https://registry.npmjs.org
npmci npm publish
# Update .npmrc for npmjs.org
echo "registry=https://registry.npmjs.org/" > .npmrc
echo "//registry.npmjs.org/:_authToken=${NPMCI_TOKEN_NPM}" >> .npmrc
pnpm publish --no-git-checks
fi
metadata:
@@ -114,24 +98,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality
run: |
npmci command npm install -g typescript
npmci npm install
- name: Trigger
run: npmci trigger
npm install -g typescript
pnpm install
- name: Build docs and upload artifacts
run: |
npmci node install stable
npmci npm install
pnpm install
pnpm install -g @git.zone/tsdoc
npmci command tsdoc
tsdoc
continue-on-error: true

View File

@@ -1,5 +1,66 @@
# Changelog
## 2025-11-16 - 4.4.2 - fix(core_base/request)
Strip 'unix:' prefix when parsing unix socket URLs so socketPath is a clean filesystem path
- CoreRequest.parseUnixSocketUrl now removes a leading 'unix:' prefix and returns socketPath as a filesystem path (e.g., /var/run/docker.sock)
- Updated tests for Bun, Deno and Node to expect socketPath without the 'unix:' prefix
- Adjusted comments/documentation in core_base/request.ts to clarify returned socketPath format
## 2025-11-16 - 4.4.1 - fix(core_node)
Fix unix socket URL parsing and handling in CoreRequest
- CoreRequest.parseUnixSocketUrl now strips http:// and https:// prefixes so it correctly parses both full URLs (e.g. http://unix:/path/to/socket:/route) and already-stripped unix: paths.
- Node.js CoreRequest now passes the original request URL to parseUnixSocketUrl instead of options.path, preventing incorrect socketPath/path extraction.
- Fixes connection failures when using unix socket URLs (for example when targeting Docker via http://unix:/var/run/docker.sock:/v1.24/...).
## 2025-11-16 - 4.4.0 - feat(core)
Add Bun and Deno runtime support, unify core loader, unix-socket support and cross-runtime streaming/tests
- package.json: expose ./core_bun and ./core_deno in exports and add runtime-related keywords
- Core dynamic loader (ts/core/index.ts): detect Bun and Deno at runtime and load corresponding implementations
- New runtime modules: added ts/core_bun/* and ts/core_deno/* (response, types, index) to provide Bun and Deno CoreResponse/CoreRequest wrappers
- Client streaming: SmartRequest no longer immediately deletes temporary __nodeStream and __rawStreamFunc props — CoreRequest implementations handle them; temporary properties are cleaned up after CoreRequest is created
- Node.js request: core_node/request.ts converts web ReadableStream to Node.js Readable via stream.Readable.fromWeb and pipes it; also supports passing requestDataFunc for raw streaming
- core_node/plugins: export stream helper and rework third-party exports (agentkeepalive, form-data) for Node implementation
- CoreResponse for Bun/Deno: new implementations wrap native fetch Response and expose raw(), stream(), and streamNode() behavior (streamNode() throws in Bun/Deno with guidance to use web streams)
- Tests: added unified cross-runtime streaming tests and separate unix-socket tests for Node/Bun/Deno with Docker-socket availability checks; removed old node-only streaming test
- Docs/readme: updated to describe Node, Bun, Deno, and browser support, unix socket behavior per runtime, and new test conventions
## 2025-11-16 - 4.3.8 - fix(core)
Ensure correct ArrayBuffer return, fix fetch body typing, reorganize node-only tests, and bump tsbuild devDependency
- core_node: Fix arrayBuffer() to ensure an ArrayBuffer is returned (avoid returning SharedArrayBuffer) to improve interoperability when consuming binary responses.
- core_fetch: Cast request body to BodyInit when assigning to fetch options and preserve duplex = 'half' for ReadableStream bodies to satisfy typings and streaming behavior.
- tests: Reorganize tests into Node-only variants (rename/remove multi-platform test files to test.*.node.ts) to separate platform-specific test coverage.
- chore: Bump devDependency @git.zone/tsbuild from ^2.6.8 to ^2.7.1.
## 2025-11-01 - 4.3.7 - fix(ci)
Update dependencies, add deno.lock, and reorganize tests for browser and Node environments
- Add deno.lock with resolved npm package versions for deterministic Deno/npm usage
- Bump @push.rocks/smartenv dependency to ^6.0.0
- Bump devDependencies: @git.zone/tsbuild -> ^2.6.8, @git.zone/tsrun -> ^1.6.2, @git.zone/tstest -> ^2.7.0
- Reorganize tests: move browser tests to chromium variants and add environment-specific test files for node, bun, deno (streaming, timeout, streamNode, etc.)
- Update package.json dependency ranges to match upgraded lockfile and test tooling
## 2025-10-26 - 4.3.6 - fix(ci)
Use .npmrc for registry authentication in Gitea workflow and add conditional npmjs publish
- Replace npm config set commands with creating a .npmrc file for Gitea registry authentication in .gitea/workflows/default_tags.yaml
- Add conditional update of .npmrc and publishing to npmjs.org when NPMCI_TOKEN_NPM is provided
- Keep pnpm publish --no-git-checks; improve CI credential handling to be file-based
## 2025-10-26 - 4.3.5 - fix(workflows)
Remove npmci wrappers from CI workflows and use pnpm/npm CLI directly
- Removed global npmci installation and npmci npm prepare steps from Gitea workflow files
- Use pnpm install/test/build instead of npmci-wrapped commands in test jobs
- Replace npmci command npm config set ... with direct npm config set calls for registry/auth configuration
- Use pnpm publish --no-git-checks for Gitea publishing and use pnpm publish for conditional npmjs publish when token present
- Simplified dependency auditing to run pnpm audit and set registry via npm config set
- Install tsdoc globally and run tsdoc during docs build step (replacing npmci command usage)
## 2025-10-25 - 4.3.4 - fix(ci)
Fix Gitea workflow publish invocation to run npm publish via npmci command

7188
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
{
"name": "@push.rocks/smartrequest",
"version": "4.3.4",
"version": "4.4.2",
"private": false,
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
"exports": {
".": "./dist_ts/index.js",
"./core_node": "./dist_ts/core_node/index.js",
"./core_fetch": "./dist_ts/core_fetch/index.js"
"./core_fetch": "./dist_ts/core_fetch/index.js",
"./core_bun": "./dist_ts/core_bun/index.js",
"./core_deno": "./dist_ts/core_deno/index.js"
},
"type": "module",
"scripts": {
@@ -30,7 +32,11 @@
"keepAlive",
"TypeScript",
"modern web requests",
"drop-in replacement"
"drop-in replacement",
"Bun",
"Deno",
"Node.js",
"unix sockets"
],
"author": "Task Venture Capital GmbH",
"license": "MIT",
@@ -39,7 +45,7 @@
},
"homepage": "https://code.foss.global/push.rocks/smartrequest#readme",
"dependencies": {
"@push.rocks/smartenv": "^5.0.13",
"@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smarturl": "^3.1.0",
@@ -47,9 +53,9 @@
"form-data": "^4.0.4"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.4",
"@git.zone/tsbuild": "^2.7.1",
"@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.7.0",
"@types/node": "^22.9.0"
},
"files": [

5734
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,26 +4,28 @@
- supports http
- supports https
- supports unix socks
- supports unix sockets on Node.js, Bun, and Deno
- supports formData
- supports file uploads
- supports best practice keepAlive
- dedicated functions for working with JSON request/response cycles
- written in TypeScript
- continuously updated
- uses node native http and https modules
- supports both Node.js and browser environments
- supports Node.js, Bun, Deno, and browser environments with automatic runtime detection
- runtime-specific implementations using native APIs (Node http/https, Bun fetch, Deno fetch with HttpClient, browser fetch)
- used in modules like @push.rocks/smartproxy and @api.global/typedrequest
## Architecture Overview (as of v3.0.0 major refactoring)
## Architecture Overview (as of v4.x with Bun and Deno support)
- The project now has a multi-layer architecture with platform abstraction
- The project has a multi-layer architecture with runtime 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
- Node.js implementation (ts/core_node/) uses native http/https modules with unix socket support
- Bun implementation (ts/core_bun/) uses Bun's native fetch with unix socket support via `unix` option
- Deno implementation (ts/core_deno/) uses Deno's fetch with unix socket support via HttpClient proxy
- Browser implementation (ts/core_fetch/) uses standard Fetch API for browser compatibility
- Core module (ts/core/) uses @push.rocks/smartenv to detect runtime and dynamically load appropriate implementation
- Client API (ts/client/) provides a fluent, chainable interface that works across all runtimes
- Runtime detection order: Deno → Bun → Node.js → Browser (following smartenv detection best practices)
## Key Components
@@ -56,6 +58,31 @@
- `stream()` returns native web ReadableStream from response.body
- `streamNode()` throws error explaining it's not available in browser
### Core Bun Module (ts/core_bun/)
- `request.ts`: Bun implementation using native fetch with unix socket support
- Uses Bun's `unix` fetch option for unix socket connections
- Supports both `unix` and `socketPath` options (converts socketPath to unix)
- Handles URL parsing for `http://unix:/path/to/socket:/http/path` format
- Throws errors for Node.js specific options (agent)
- `response.ts`: Bun-based CoreResponse implementation
- `stream()` returns native web ReadableStream from response.body
- `streamNode()` throws error (Bun uses web streams; users should use stream() instead)
- `types.ts`: Extends base types with IBunRequestOptions including `unix` option
### Core Deno Module (ts/core_deno/)
- `request.ts`: Deno implementation using fetch with HttpClient proxy for unix sockets
- Creates and caches Deno.HttpClient instances per socket path
- Supports both explicit `client` option and automatic client creation from `socketPath`
- HttpClient cache prevents creating multiple clients for same socket
- Provides `clearClientCache()` static method for cleanup
- Throws errors for Node.js specific options (agent)
- `response.ts`: Deno-based CoreResponse implementation
- `stream()` returns native web ReadableStream from response.body
- `streamNode()` throws error (Deno uses web streams, not Node.js streams)
- `types.ts`: Extends base types with IDenoRequestOptions including `client` option
### Core Module (ts/core/)
- Dynamically loads appropriate implementation based on environment
@@ -70,10 +97,15 @@
### Stream Handling
- `stream()` method always returns web-style ReadableStream<Uint8Array>
- `stream()` method always returns web-style ReadableStream<Uint8Array> across all platforms
- 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
- `streamNode()` availability by runtime:
- **Node.js**: Returns native Node.js ReadableStream (only runtime that supports this)
- **Bun**: Throws error (use web streams via stream() instead)
- **Deno**: Throws error (Deno uses web streams only)
- **Browser**: Throws error (browsers use web streams only)
- Consistent API across platforms with web streams as the common denominator
- Only Node.js provides native Node.js streams via streamNode()
### Binary Request Handling
@@ -85,5 +117,11 @@
- 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)
- Test file naming conventions:
- `test.node.ts` - Node.js only tests
- `test.bun.ts` - Bun only tests
- `test.deno.ts` - Deno only tests
- `test.node+bun+deno.ts` - Server-side runtime tests (all three)
- `test.browser.ts` or `test.chrome.ts` - Browser tests
- Unix socket tests check for Docker socket availability and skip if not present
- Browser tests run in headless Chromium via puppeteer

206
readme.md
View File

@@ -1,6 +1,6 @@
# @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.
A modern, cross-platform HTTP/HTTPS request library for Node.js, Bun, Deno, and browsers with a unified API, supporting form data, file uploads, JSON, binary data, streams, and unix sockets.
## Install
@@ -18,26 +18,30 @@ yarn add @push.rocks/smartrequest
## 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)
- 🌐 **Cross-Platform** - Works in Node.js, Bun, Deno, and browsers with a unified API
- 🔌 **Unix Socket Support** - Connect to local services like Docker (Node.js, Bun, and Deno)
- 📦 **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** - Stream buffers, files, and custom data without loading into memory
- 🔧 **Highly Configurable** - Timeouts, retries, headers, and more
- 🔧 **Highly Configurable** - Timeouts, retries, headers, rate limiting, and more
## Architecture
SmartRequest v3.0 features a multi-layer architecture that provides consistent behavior across platforms:
SmartRequest 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 Node** - Node.js implementation using native http/https modules with unix socket support
- **Core Bun** - Bun implementation using native fetch with unix socket support via `unix` option
- **Core Deno** - Deno implementation using fetch with unix socket support via HttpClient proxy
- **Core Fetch** - Browser implementation using the Fetch API
- **Core** - Dynamic implementation selection based on environment
- **Core** - Dynamic runtime detection and implementation selection using @push.rocks/smartenv
- **Client** - High-level fluent API for everyday use
The library automatically detects the runtime environment (Deno, Bun, Node.js, or browser) and loads the appropriate implementation, ensuring optimal performance and native feature support for each platform.
## Usage
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for modern applications.
@@ -204,7 +208,7 @@ async function streamLargeFile(url: string) {
async function streamWithNodeApi(url: string) {
const response = await SmartRequest.create().url(url).get();
// Only available in Node.js, throws error in browser
// Only available in Node.js, throws error in browser/Bun/Deno
const nodeStream = response.streamNode();
nodeStream.on('data', (chunk) => {
@@ -226,7 +230,7 @@ The response object provides these methods:
- `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)
- `streamNode(): NodeJS.ReadableStream` - Get Node.js stream (Node.js only, throws in browser/Bun/Deno)
- `raw(): Response | http.IncomingMessage` - Get the underlying platform response
Each body method can only be called once per response, similar to the fetch API.
@@ -312,37 +316,55 @@ import { SmartRequest } from '@push.rocks/smartrequest';
import * as fs from 'fs';
import { Readable } from 'stream';
// Stream a Buffer directly
// Stream a Buffer directly (works everywhere)
async function uploadBuffer() {
const buffer = Buffer.from('Hello, World!');
const response = await SmartRequest.create()
.url('https://api.example.com/upload')
.buffer(buffer, 'text/plain')
.post();
return await response.json();
}
// Stream a file using Node.js streams
// Stream using web ReadableStream (cross-platform!)
async function uploadWebStream() {
const stream = new ReadableStream({
start(controller) {
const data = new TextEncoder().encode('Stream data');
controller.enqueue(data);
controller.close();
},
});
const response = await SmartRequest.create()
.url('https://api.example.com/upload')
.stream(stream, 'text/plain')
.post();
return await response.json();
}
// Stream a file using Node.js streams (Node.js only)
async function uploadLargeFile(filePath: string) {
const fileStream = fs.createReadStream(filePath);
const response = await SmartRequest.create()
.url('https://api.example.com/upload')
.stream(fileStream, 'application/octet-stream')
.post();
return await response.json();
}
// Stream data from any readable source
// Stream data from any readable source (Node.js only)
async function streamData(dataSource: Readable) {
const response = await SmartRequest.create()
.url('https://api.example.com/stream')
.stream(dataSource)
.post();
return await response.json();
}
@@ -354,24 +376,24 @@ async function customStreaming() {
// Custom streaming logic - you have full control
request.write('chunk1');
request.write('chunk2');
// Stream from another source
someReadableStream.pipe(request);
})
.post();
return await response.json();
}
// Send Uint8Array (works in both Node.js and browser)
async function uploadBinaryData() {
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
const response = await SmartRequest.create()
.url('https://api.example.com/binary')
.buffer(data, 'application/octet-stream')
.post();
return await response.json();
}
```
@@ -379,19 +401,19 @@ async function uploadBinaryData() {
#### Streaming Methods
- **`.buffer(data, contentType?)`** - Stream a Buffer or Uint8Array directly
- `data`: Buffer (Node.js) or Uint8Array (both platforms) to send
- `data`: Buffer (Node.js) or Uint8Array (cross-platform) to send
- `contentType`: Optional content type (defaults to 'application/octet-stream')
- ✅ Works in both Node.js and browsers
- ✅ Works everywhere (Node.js, Bun, Deno, browsers)
- **`.stream(stream, contentType?)`** - Stream from ReadableStream
- `stream`: Web ReadableStream (both platforms) or Node.js stream (Node.js only)
- **`.stream(stream, contentType?)`** - Stream from ReadableStream or Node.js stream
- `stream`: Web ReadableStream (cross-platform) or Node.js stream (Node.js only)
- `contentType`: Optional content type
- ✅ Web ReadableStream works in both Node.js and browsers
- ⚠️ Node.js streams only work in Node.js environment
- ✅ Web ReadableStream works everywhere (Node.js, Bun, Deno, browsers)
- ⚠️ Node.js streams only work in Node.js (automatically converted to web streams in Bun/Deno)
- **`.raw(streamFunc)`** - Advanced control over request streaming
- `streamFunc`: Function that receives the raw request object for custom streaming
-**Node.js only** - not supported in browsers
-**Node.js only** - not supported in browsers, Bun, or Deno
- Use for advanced scenarios like chunked transfer encoding
These methods are particularly useful for:
@@ -400,12 +422,14 @@ These methods are particularly useful for:
- Proxying data between services
- Implementing chunked transfer encoding
### Unix Socket Support (Node.js only)
### Unix Socket Support (Node.js, Bun, and Deno)
SmartRequest supports unix sockets across all server-side runtimes with a unified API:
```typescript
import { SmartRequest } from '@push.rocks/smartrequest';
// Connect to a service via Unix socket
// Connect to a service via Unix socket (works on Node.js, Bun, and Deno)
async function queryViaUnixSocket() {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
@@ -413,6 +437,57 @@ async function queryViaUnixSocket() {
return await response.json();
}
// Alternative: Use socketPath option (works on all server runtimes)
async function queryWithSocketPath() {
const response = await SmartRequest.create()
.url('http://localhost/version')
.options({ socketPath: '/var/run/docker.sock' })
.get();
return await response.json();
}
```
#### Runtime-Specific Unix Socket APIs
Each runtime implements unix sockets using its native capabilities:
**Bun:**
```typescript
import { CoreRequest } from '@push.rocks/smartrequest/core_bun';
// Bun uses the native `unix` fetch option
const response = await CoreRequest.create('http://localhost/version', {
unix: '/var/run/docker.sock'
});
```
**Deno:**
```typescript
import { CoreRequest } from '@push.rocks/smartrequest/core_deno';
// Deno uses HttpClient with unix socket proxy
const client = Deno.createHttpClient({
proxy: { url: 'unix:///var/run/docker.sock' }
});
const response = await CoreRequest.create('http://localhost/version', {
client
});
// Clean up when done
client.close();
```
**Node.js:**
```typescript
import { CoreRequest } from '@push.rocks/smartrequest/core_node';
// Node.js uses native socketPath option
const response = await CoreRequest.create('http://localhost/version', {
socketPath: '/var/run/docker.sock'
});
```
### Pagination Support
@@ -599,12 +674,63 @@ const response = await SmartRequest.create()
.get();
```
### Bun-Specific Options
When running in Bun, you can use Bun-specific options:
```typescript
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.options({
unix: '/var/run/api.sock', // Unix socket (Bun's native option)
keepAlive: true, // Keep-alive support
})
.get();
// Bun uses web streams - streamNode() throws an error
const streamResponse = await SmartRequest.create()
.url('https://api.example.com/data')
.get();
const webStream = streamResponse.stream(); // ✅ Use web streams in Bun
// streamNode() is not available - throws error directing you to use stream()
```
### Deno-Specific Options
When running in Deno, you can use Deno-specific options:
```typescript
// Custom HttpClient for advanced configuration
const client = Deno.createHttpClient({
proxy: { url: 'unix:///var/run/api.sock' }
});
const response = await SmartRequest.create()
.url('https://api.example.com/data')
.options({
client, // Custom Deno HttpClient
})
.get();
// Remember to clean up clients when done
client.close();
// Deno uses web streams - streamNode() throws an error
const streamResponse = await SmartRequest.create()
.url('https://api.example.com/data')
.get();
const webStream = streamResponse.stream(); // ✅ Use web streams in Deno
// streamNode() is not available - throws error directing you to use stream()
```
## 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';
import { SmartRequest, type ICoreResponse } from '@push.rocks/smartrequest';
interface User {
id: number;
@@ -644,6 +770,9 @@ class BlogApiClient {
if (!response.ok) {
throw new Error(`Failed to delete post: ${response.statusText}`);
}
// Consume the body
await response.text();
}
async getAllPosts(userId?: number): Promise<Post[]> {
@@ -707,9 +836,20 @@ async function fetchWithErrorHandling(url: string) {
}
```
## Migrating from v2.x to v3.x
## Migrating from Earlier Versions
Version 3.0 brings significant architectural improvements and a more consistent API:
### From v3.x to v4.x
Version 4.0 adds comprehensive cross-platform support:
1. **Multi-Runtime Support**: Now works natively in Node.js, Bun, Deno, and browsers
2. **Unix Sockets Everywhere**: Unix socket support added for Bun and Deno
3. **Web Streams**: Full support for web ReadableStream across all platforms
4. **Automatic Runtime Detection**: No configuration needed - works everywhere automatically
### From v2.x to v3.x
Version 3.0 brought significant architectural improvements:
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.

View File

@@ -1,71 +1,58 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as fs from 'fs';
import { SmartRequest } from '../ts/index.js';
// Cross-platform tests using web-standard APIs only
tap.test('should send a buffer using buffer() method', async () => {
const testBuffer = Buffer.from('Hello, World!');
const smartRequest = SmartRequest.create()
.url('https://httpbin.org/post')
.buffer(testBuffer, 'text/plain')
.method('POST');
const response = await smartRequest.post();
const data = await response.json();
expect(data).toHaveProperty('data');
expect(data.data).toEqual('Hello, World!');
expect(data.headers['Content-Type']).toEqual('text/plain');
});
tap.test('should send a stream using stream() method', async () => {
// Create a simple readable stream
const { Readable } = await import('stream');
tap.test('should send a web ReadableStream using stream() method', async () => {
const testData = 'Stream data test';
const stream = Readable.from([testData]);
// Use web-standard ReadableStream (works on all platforms)
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(testData));
controller.close();
},
});
const smartRequest = SmartRequest.create()
.url('https://httpbin.org/post')
.stream(stream, 'text/plain')
.method('POST');
const response = await smartRequest.post();
const data = await response.json();
expect(data).toHaveProperty('data');
expect(data.data).toEqual(testData);
});
tap.test('should handle raw streaming with custom function', async () => {
const testData = 'Custom raw stream data';
const smartRequest = SmartRequest.create()
.url('https://httpbin.org/post')
.raw((request) => {
// Custom streaming logic
request.write(testData);
request.end();
})
.method('POST');
const response = await smartRequest.post();
const data = await response.json();
expect(data).toHaveProperty('data');
expect(data.data).toEqual(testData);
});
tap.test('should send Uint8Array using buffer() method', async () => {
const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII
const smartRequest = SmartRequest.create()
.url('https://httpbin.org/post')
.buffer(testData, 'application/octet-stream')
.method('POST');
const response = await smartRequest.post();
const data = await response.json();
// Just verify that data was sent
expect(data).toHaveProperty('data');
expect(data.headers['Content-Type']).toEqual('application/octet-stream');

101
test/test.unixsocket.bun.ts Normal file
View File

@@ -0,0 +1,101 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SmartRequest } from '../ts/client/index.js';
import { CoreRequest } from '../ts/core_bun/index.js';
// Check if Docker socket exists (common unix socket for testing)
const dockerSocketPath = '/var/run/docker.sock';
let dockerAvailable = false;
try {
const file = Bun.file(dockerSocketPath);
dockerAvailable = await file.exists();
} catch (error) {
console.log(
'Docker socket not available - skipping unix socket tests. To enable, ensure Docker is running.',
);
}
tap.test('bun: should detect unix socket URLs correctly', async () => {
expect(CoreRequest.isUnixSocket('unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('http://unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('https://unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('http://example.com')).toBeFalse();
expect(CoreRequest.isUnixSocket('https://example.com')).toBeFalse();
});
tap.test('bun: should parse unix socket URLs correctly', async () => {
const result = CoreRequest.parseUnixSocketUrl('unix:/var/run/docker.sock:/v1.24/version');
expect(result.socketPath).toEqual('/var/run/docker.sock');
expect(result.path).toEqual('/v1.24/version');
});
if (dockerAvailable) {
tap.test('bun: should connect to Docker via unix socket (unix: protocol)', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
console.log(`Docker version: ${body.Version}`);
});
tap.test('bun: should connect to Docker via socketPath option', async () => {
const response = await CoreRequest.create('http://localhost/version', {
socketPath: '/var/run/docker.sock',
});
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
});
tap.test('bun: should connect to Docker via unix option', async () => {
const response = await CoreRequest.create('http://localhost/version', {
unix: '/var/run/docker.sock',
});
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
});
tap.test('bun: should handle unix socket with query parameters', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/containers/json')
.query({ all: 'true' })
.get();
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(Array.isArray(body)).toBeTrue();
});
tap.test('bun: should handle unix socket with POST requests', async () => {
// Test POST to Docker API (this specific endpoint may require permissions)
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/containers/json')
.query({ all: 'true', limit: '1' })
.get();
expect(response.status).toBeGreaterThanOrEqual(200);
expect(response.status).toBeLessThan(500);
await response.text(); // Consume body
});
} else {
tap.skip.test(
'bun: unix socket tests skipped - Docker socket not available',
);
}
export default tap.start();

View File

@@ -0,0 +1,154 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SmartRequest } from '../ts/client/index.js';
import { CoreRequest } from '../ts/core_deno/index.js';
// Check if Docker socket exists (common unix socket for testing)
const dockerSocketPath = '/var/run/docker.sock';
let dockerAvailable = false;
try {
const fileInfo = await Deno.stat(dockerSocketPath);
dockerAvailable = fileInfo.isFile || fileInfo.isSymlink;
} catch (error) {
console.log(
'Docker socket not available - skipping unix socket tests. To enable, ensure Docker is running.',
);
}
tap.test('deno: should detect unix socket URLs correctly', async () => {
expect(CoreRequest.isUnixSocket('unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('http://unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('https://unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('http://example.com')).toBeFalse();
expect(CoreRequest.isUnixSocket('https://example.com')).toBeFalse();
});
tap.test('deno: should parse unix socket URLs correctly', async () => {
const result = CoreRequest.parseUnixSocketUrl('unix:/var/run/docker.sock:/v1.24/version');
expect(result.socketPath).toEqual('/var/run/docker.sock');
expect(result.path).toEqual('/v1.24/version');
});
if (dockerAvailable) {
tap.test('deno: should connect to Docker via unix socket (unix: protocol)', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
console.log(`Docker version: ${body.Version}`);
});
tap.test('deno: should connect to Docker via socketPath option', async () => {
const response = await CoreRequest.create('http://localhost/version', {
socketPath: '/var/run/docker.sock',
});
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
});
tap.test('deno: should connect to Docker via HttpClient', async () => {
const client = Deno.createHttpClient({
proxy: {
url: 'unix:///var/run/docker.sock',
},
});
const response = await CoreRequest.create('http://localhost/version', {
client,
});
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
// Clean up client
client.close();
});
tap.test('deno: should handle unix socket with query parameters', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/containers/json')
.query({ all: 'true' })
.get();
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(Array.isArray(body)).toBeTrue();
});
tap.test('deno: should handle unix socket with POST requests', async () => {
// Test POST to Docker API (this specific endpoint may require permissions)
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/containers/json')
.query({ all: 'true', limit: '1' })
.get();
expect(response.status).toBeGreaterThanOrEqual(200);
expect(response.status).toBeLessThan(500);
await response.text(); // Consume body
});
tap.test('deno: should cache HttpClient for reuse', async () => {
// First request creates a client
const response1 = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response1.ok).toBeTrue();
await response1.text();
// Second request should reuse the cached client
const response2 = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response2.ok).toBeTrue();
await response2.text();
// Clean up cache
CoreRequest.clearClientCache();
});
tap.test('deno: should clear HttpClient cache', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response.ok).toBeTrue();
await response.text();
// Clear cache - should not throw
CoreRequest.clearClientCache();
// Subsequent request should create new client
const response2 = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response2.ok).toBeTrue();
await response2.text();
// Clean up
CoreRequest.clearClientCache();
});
} else {
tap.skip.test(
'deno: unix socket tests skipped - Docker socket not available',
);
}
export default tap.start();

View File

@@ -0,0 +1,90 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/core_node/plugins.js';
import { SmartRequest } from '../ts/client/index.js';
import { CoreRequest } from '../ts/core_node/index.js';
// Check if Docker socket exists (common unix socket for testing)
const dockerSocketPath = '/var/run/docker.sock';
let dockerAvailable = false;
try {
await plugins.fs.promises.access(dockerSocketPath, plugins.fs.constants.R_OK);
dockerAvailable = true;
} catch (error) {
console.log(
'Docker socket not available - skipping unix socket tests. To enable, ensure Docker is running.',
);
}
tap.test('node: should detect unix socket URLs correctly', async () => {
expect(CoreRequest.isUnixSocket('unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('http://unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('https://unix:/var/run/docker.sock:/version')).toBeTrue();
expect(CoreRequest.isUnixSocket('http://example.com')).toBeFalse();
expect(CoreRequest.isUnixSocket('https://example.com')).toBeFalse();
});
tap.test('node: should parse unix socket URLs correctly', async () => {
const result = CoreRequest.parseUnixSocketUrl('unix:/var/run/docker.sock:/v1.24/version');
expect(result.socketPath).toEqual('/var/run/docker.sock');
expect(result.path).toEqual('/v1.24/version');
});
if (dockerAvailable) {
tap.test('node: should connect to Docker via unix socket (unix: protocol)', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/version')
.get();
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
console.log(`Docker version: ${body.Version}`);
});
tap.test('node: should connect to Docker via socketPath option', async () => {
const response = await CoreRequest.create('http://localhost/version', {
socketPath: '/var/run/docker.sock',
});
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toHaveProperty('Version');
});
tap.test('node: should handle unix socket with query parameters', async () => {
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/containers/json')
.query({ all: 'true' })
.get();
expect(response.ok).toBeTrue();
expect(response.status).toEqual(200);
const body = await response.json();
expect(Array.isArray(body)).toBeTrue();
});
tap.test('node: should handle unix socket with POST requests', async () => {
// Test POST to Docker API (this specific endpoint may require permissions)
const response = await SmartRequest.create()
.url('http://unix:/var/run/docker.sock:/containers/json')
.query({ all: 'true', limit: '1' })
.get();
expect(response.status).toBeGreaterThanOrEqual(200);
expect(response.status).toBeLessThan(500);
await response.text(); // Consume body
});
} else {
tap.skip.test(
'node: unix socket tests skipped - Docker socket not available',
);
}
export default tap.start();

View File

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

View File

@@ -447,15 +447,18 @@ export class SmartRequest<T = any> {
requestDataFunc = (req: any) => {
nodeStream.pipe(req);
};
// Remove the temporary stream reference
delete (this._options as any).__nodeStream;
// Don't delete __nodeStream yet - let CoreRequest implementations handle it
// Node.js will use requestDataFunc, Bun/Deno will convert the stream
} else if ((this._options as any).__rawStreamFunc) {
requestDataFunc = (this._options as any).__rawStreamFunc;
// Remove the temporary function reference
delete (this._options as any).__rawStreamFunc;
// Don't delete __rawStreamFunc yet - let CoreRequest implementations handle it
}
const request = new CoreRequest(this._url, this._options as any, requestDataFunc);
// Clean up temporary properties after CoreRequest has been created
delete (this._options as any).__nodeStream;
delete (this._options as any).__rawStreamFunc;
const response = (await request.fire()) as ICoreResponse<R>;
// Check for 429 status if rate limit handling is enabled

View File

@@ -5,12 +5,22 @@ export * from '../core_base/types.js';
const smartenvInstance = new plugins.smartenv.Smartenv();
// Dynamically load the appropriate implementation
// Dynamically load the appropriate implementation based on runtime
let CoreRequest: any;
let CoreResponse: any;
if (smartenvInstance.isNode) {
// In Node.js, load the node implementation
if (smartenvInstance.isDeno) {
// In Deno, load the Deno implementation with HttpClient-based unix socket support
const impl = await import('../core_deno/index.js');
CoreRequest = impl.CoreRequest;
CoreResponse = impl.CoreResponse;
} else if (smartenvInstance.isBun) {
// In Bun, load the Bun implementation with native fetch unix socket support
const impl = await import('../core_bun/index.js');
CoreRequest = impl.CoreRequest;
CoreResponse = impl.CoreResponse;
} else if (smartenvInstance.isNode) {
// In Node.js, load the Node.js implementation with native http/https unix socket support
const modulePath = plugins.smartpath.join(
plugins.smartpath.dirname(import.meta.url),
'../core_node/index.js',
@@ -19,7 +29,7 @@ if (smartenvInstance.isNode) {
CoreRequest = impl.CoreRequest;
CoreResponse = impl.CoreResponse;
} else {
// In browser, load the fetch implementation
// In browser, load the fetch implementation (no unix socket support)
const impl = await import('../core_fetch/index.js');
CoreRequest = impl.CoreRequest;
CoreResponse = impl.CoreResponse;

View File

@@ -17,10 +17,28 @@ export abstract class CoreRequest<
/**
* Parses socket path and route from unix socket URL
* Handles both full URLs (http://unix:/path/to/socket:/route) and pre-stripped paths (unix:/path/to/socket:/route)
* Returns clean file system path for socketPath (e.g., /var/run/docker.sock)
*/
static parseUnixSocketUrl(url: string): { socketPath: string; path: string } {
// Strip http:// or https:// prefix if present
// This makes the method work with both full URLs and pre-stripped paths
let cleanUrl = url;
if (cleanUrl.startsWith('http://')) {
cleanUrl = cleanUrl.substring('http://'.length);
} else if (cleanUrl.startsWith('https://')) {
cleanUrl = cleanUrl.substring('https://'.length);
}
// Strip unix: prefix if present to get clean file system path
if (cleanUrl.startsWith('unix:')) {
cleanUrl = cleanUrl.substring('unix:'.length);
}
// Parse the socket path and HTTP path
// Format: /path/to/socket:/route/path
const parseRegex = /(.*):(.*)/;
const result = parseRegex.exec(url);
const result = parseRegex.exec(cleanUrl);
return {
socketPath: result[1],
path: result[2],

3
ts/core_bun/index.ts Normal file
View File

@@ -0,0 +1,3 @@
// Core Bun exports - Bun's native fetch implementation with unix socket support
export * from './response.js';
export { CoreRequest } from './request.js';

249
ts/core_bun/request.ts Normal file
View File

@@ -0,0 +1,249 @@
import * as types from './types.js';
import { CoreResponse } from './response.js';
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
/**
* Bun implementation of Core Request class using native fetch with unix socket support
*/
export class CoreRequest extends AbstractCoreRequest<
types.IBunRequestOptions,
CoreResponse
> {
private timeoutId: ReturnType<typeof setTimeout> | null = null;
private abortController: AbortController | null = null;
private requestDataFunc: ((req: any) => void) | null;
constructor(
url: string,
options: types.IBunRequestOptions = {},
requestDataFunc: ((req: any) => void) | null = null,
) {
super(url, options);
this.requestDataFunc = requestDataFunc;
// Check for unsupported Node.js-specific options
if (options.agent) {
throw new Error(
'Node.js specific option (agent) is not supported in Bun implementation',
);
}
// Handle Node.js stream conversion if requestDataFunc is provided
if (requestDataFunc && (options as any).__nodeStream) {
// Convert Node.js stream to web ReadableStream for Bun
const nodeStream = (options as any).__nodeStream;
// Bun can handle Node.js streams via Readable.toWeb if available
// Or we can create a web stream that reads from the Node stream
if (typeof (nodeStream as any).toWeb === 'function') {
this.options.requestBody = (nodeStream as any).toWeb();
} else {
// Create web ReadableStream from Node.js stream
this.options.requestBody = new ReadableStream({
async start(controller) {
nodeStream.on('data', (chunk: any) => {
controller.enqueue(new Uint8Array(chunk));
});
nodeStream.on('end', () => {
controller.close();
});
nodeStream.on('error', (err: any) => {
controller.error(err);
});
},
});
}
}
// Warn if raw streaming function is provided (not supported in Bun)
if (requestDataFunc && (options as any).__rawStreamFunc) {
throw new Error(
'Raw streaming with .raw() is not supported in Bun. Use .stream() with web ReadableStream instead.',
);
}
}
/**
* Build the full URL with query parameters
*/
private buildUrl(): string {
// For unix sockets, we need to extract the HTTP path part
if (CoreRequest.isUnixSocket(this.url)) {
const { path } = CoreRequest.parseUnixSocketUrl(this.url);
// Build URL for the HTTP request (the hostname doesn't matter for unix sockets)
if (
!this.options.queryParams ||
Object.keys(this.options.queryParams).length === 0
) {
return `http://localhost${path}`;
}
const url = new URL(`http://localhost${path}`);
Object.entries(this.options.queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
// Regular HTTP/HTTPS URL
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 with Bun-specific extensions
*/
private buildFetchOptions(): RequestInit & { unix?: string } {
const fetchOptions: RequestInit & { unix?: string } = {
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 unix socket
if (CoreRequest.isUnixSocket(this.url)) {
const { socketPath } = CoreRequest.parseUnixSocketUrl(this.url);
fetchOptions.unix = socketPath;
} else if (this.options.unix) {
// Direct unix option was provided
fetchOptions.unix = this.options.unix;
} else if (this.options.socketPath) {
// Legacy Node.js socketPath option - convert to Bun's unix option
fetchOptions.unix = this.options.socketPath;
}
// Handle request body
if (this.options.requestBody !== undefined) {
if (
typeof this.options.requestBody === 'string' ||
this.options.requestBody instanceof ArrayBuffer ||
this.options.requestBody instanceof Uint8Array ||
this.options.requestBody instanceof FormData ||
this.options.requestBody instanceof URLSearchParams ||
this.options.requestBody instanceof ReadableStream ||
// Check for Buffer (Bun supports Node.js Buffer)
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
) {
fetchOptions.body = this.options.requestBody as BodyInit;
// If streaming, we need to set duplex mode
if (this.options.requestBody instanceof ReadableStream) {
(fetchOptions as any).duplex = 'half';
}
} 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;
this.abortController = new AbortController();
this.timeoutId = setTimeout(() => {
if (this.abortController) {
this.abortController.abort();
}
}, timeout);
fetchOptions.signal = this.abortController.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);
// Clear timeout on successful response
this.clearTimeout();
return response;
} catch (error) {
// Clear timeout on error
this.clearTimeout();
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
/**
* Clear the timeout and abort controller
*/
private clearTimeout(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
if (this.abortController) {
this.abortController = null;
}
}
/**
* Static factory method to create and fire a request
*/
static async create(
url: string,
options: types.IBunRequestOptions = {},
): 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;

95
ts/core_bun/response.ts Normal file
View File

@@ -0,0 +1,95 @@
import * as types from './types.js';
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
/**
* Bun implementation of Core Response class that wraps native fetch Response
*/
export class CoreResponse<T = any>
extends AbstractCoreResponse<T>
implements types.IBunResponse<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;
}
/**
* Get response as a Node.js-style stream
* Bun supports Node.js streams, so we can provide this functionality
*
* Note: In Bun, you may also be able to use the web stream directly with stream() method
*/
streamNode(): never {
// Bun primarily uses web streams and has excellent compatibility
// For most use cases, use stream() which returns a standard ReadableStream
throw new Error(
'streamNode() is not available in Bun environment. Use stream() for web-style ReadableStream, which Bun fully supports.',
);
}
/**
* Get the raw Response object
*/
raw(): Response {
return this.responseClone;
}
}

23
ts/core_bun/types.ts Normal file
View File

@@ -0,0 +1,23 @@
import * as baseTypes from '../core_base/types.js';
// Re-export base types
export * from '../core_base/types.js';
/**
* Bun-specific request options
*/
export interface IBunRequestOptions extends baseTypes.ICoreRequestOptions {
/**
* Unix domain socket path for Bun's fetch
* When provided, the request will be sent over the unix socket instead of TCP
*/
unix?: string;
}
/**
* Bun-specific response extensions
*/
export interface IBunResponse<T = any> extends baseTypes.ICoreResponse<T> {
// Access to raw Response object
raw(): Response;
}

View File

@@ -0,0 +1,23 @@
/**
* Minimal Deno type definitions for compilation in Node.js environment
* These types are only used during build-time type checking
* At runtime, actual Deno APIs will be available in Deno environment
*/
declare global {
namespace Deno {
interface HttpClient {
close(): void;
}
interface CreateHttpClientOptions {
proxy?: {
url: string;
};
}
function createHttpClient(options: CreateHttpClientOptions): HttpClient;
}
}
export {};

3
ts/core_deno/index.ts Normal file
View File

@@ -0,0 +1,3 @@
// Core Deno exports - Deno's native fetch implementation with unix socket support via HttpClient
export * from './response.js';
export { CoreRequest } from './request.js';

295
ts/core_deno/request.ts Normal file
View File

@@ -0,0 +1,295 @@
/// <reference path="./deno.types.ts" />
import * as types from './types.js';
import { CoreResponse } from './response.js';
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
/**
* Cache for HttpClient instances keyed by socket path
* This prevents creating multiple clients for the same socket
*/
const httpClientCache = new Map<string, Deno.HttpClient>();
/**
* Deno implementation of Core Request class using native fetch with unix socket support via HttpClient
*/
export class CoreRequest extends AbstractCoreRequest<
types.IDenoRequestOptions,
CoreResponse
> {
private timeoutId: ReturnType<typeof setTimeout> | null = null;
private abortController: AbortController | null = null;
private createdClient: Deno.HttpClient | null = null;
private requestDataFunc: ((req: any) => void) | null;
constructor(
url: string,
options: types.IDenoRequestOptions = {},
requestDataFunc: ((req: any) => void) | null = null,
) {
super(url, options);
this.requestDataFunc = requestDataFunc;
// Check for unsupported Node.js-specific options
if (options.agent) {
throw new Error(
'Node.js specific option (agent) is not supported in Deno implementation',
);
}
// Handle Node.js stream conversion if requestDataFunc is provided
if (requestDataFunc && (options as any).__nodeStream) {
// Convert Node.js stream to web ReadableStream for Deno
const nodeStream = (options as any).__nodeStream;
// Create web ReadableStream from Node.js stream
this.options.requestBody = new ReadableStream({
async start(controller) {
nodeStream.on('data', (chunk: any) => {
controller.enqueue(new Uint8Array(chunk));
});
nodeStream.on('end', () => {
controller.close();
});
nodeStream.on('error', (err: any) => {
controller.error(err);
});
},
});
}
// Throw error if raw streaming function is provided (not supported in Deno)
if (requestDataFunc && (options as any).__rawStreamFunc) {
throw new Error(
'Raw streaming with .raw() is not supported in Deno. Use .stream() with web ReadableStream instead.',
);
}
}
/**
* Get or create an HttpClient for unix socket communication
*/
private getHttpClient(): Deno.HttpClient | undefined {
// If client was explicitly provided, use it
if (this.options.client) {
return this.options.client;
}
// Check if we need a unix socket client
const socketPath = this.options.socketPath ||
(CoreRequest.isUnixSocket(this.url)
? CoreRequest.parseUnixSocketUrl(this.url).socketPath
: null);
if (!socketPath) {
return undefined; // Use default client
}
// Check cache first
if (httpClientCache.has(socketPath)) {
return httpClientCache.get(socketPath);
}
// Create new HttpClient for this socket
const client = Deno.createHttpClient({
proxy: {
url: `unix://${socketPath}`,
},
});
// Cache it
httpClientCache.set(socketPath, client);
this.createdClient = client;
return client;
}
/**
* Build the full URL with query parameters
*/
private buildUrl(): string {
// For unix sockets, we need to extract the HTTP path part
if (CoreRequest.isUnixSocket(this.url)) {
const { path } = CoreRequest.parseUnixSocketUrl(this.url);
// Build URL for the HTTP request (the hostname doesn't matter for unix sockets)
if (
!this.options.queryParams ||
Object.keys(this.options.queryParams).length === 0
) {
return `http://localhost${path}`;
}
const url = new URL(`http://localhost${path}`);
Object.entries(this.options.queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
// Regular HTTP/HTTPS URL
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 & { client?: Deno.HttpClient } {
const fetchOptions: RequestInit & { client?: Deno.HttpClient } = {
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,
};
// Set the HttpClient (for unix sockets or custom configurations)
const client = this.getHttpClient();
if (client) {
fetchOptions.client = client;
}
// Handle request body
if (this.options.requestBody !== undefined) {
if (
typeof this.options.requestBody === 'string' ||
this.options.requestBody instanceof ArrayBuffer ||
this.options.requestBody instanceof Uint8Array ||
this.options.requestBody instanceof FormData ||
this.options.requestBody instanceof URLSearchParams ||
this.options.requestBody instanceof ReadableStream ||
// Check for Buffer (Deno provides Buffer via Node.js compatibility)
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
) {
fetchOptions.body = this.options.requestBody as BodyInit;
// If streaming, we need to set duplex mode
if (this.options.requestBody instanceof ReadableStream) {
(fetchOptions as any).duplex = 'half';
}
} 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;
this.abortController = new AbortController();
this.timeoutId = setTimeout(() => {
if (this.abortController) {
this.abortController.abort();
}
}, timeout);
fetchOptions.signal = this.abortController.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);
// Clear timeout on successful response
this.clearTimeout();
return response;
} catch (error) {
// Clear timeout on error
this.clearTimeout();
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
/**
* Clear the timeout and abort controller
* Note: We don't close the HttpClient here as it's cached for reuse
*/
private clearTimeout(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
if (this.abortController) {
this.abortController = null;
}
}
/**
* Static factory method to create and fire a request
*/
static async create(
url: string,
options: types.IDenoRequestOptions = {},
): Promise<CoreResponse> {
const request = new CoreRequest(url, options);
return request.fire();
}
/**
* Static method to clear the HttpClient cache
* Call this when you want to force new clients to be created
*/
static clearClientCache(): void {
httpClientCache.forEach((client) => {
client.close();
});
httpClientCache.clear();
}
}
/**
* Convenience exports for backward compatibility
*/
export const isUnixSocket = CoreRequest.isUnixSocket;
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;

91
ts/core_deno/response.ts Normal file
View File

@@ -0,0 +1,91 @@
import * as types from './types.js';
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
/**
* Deno implementation of Core Response class that wraps native fetch Response
*/
export class CoreResponse<T = any>
extends AbstractCoreResponse<T>
implements types.IDenoResponse<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 Deno's standard mode
* Throws an error as Deno uses web-standard ReadableStream
*/
streamNode(): never {
throw new Error(
'streamNode() is not available in Deno environment. Use stream() for web-style ReadableStream.',
);
}
/**
* Get the raw Response object
*/
raw(): Response {
return this.responseClone;
}
}

24
ts/core_deno/types.ts Normal file
View File

@@ -0,0 +1,24 @@
/// <reference path="./deno.types.ts" />
import * as baseTypes from '../core_base/types.js';
// Re-export base types
export * from '../core_base/types.js';
/**
* Deno-specific request options
*/
export interface IDenoRequestOptions extends baseTypes.ICoreRequestOptions {
/**
* Deno HttpClient instance for custom configurations including unix sockets
* If not provided and socketPath is specified, a client will be created automatically
*/
client?: Deno.HttpClient;
}
/**
* Deno-specific response extensions
*/
export interface IDenoResponse<T = any> extends baseTypes.ICoreResponse<T> {
// Access to raw Response object
raw(): Response;
}

View File

@@ -71,7 +71,7 @@ export class CoreRequest extends AbstractCoreRequest<
// Check for Buffer (Node.js polyfills in browser may provide this)
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
) {
fetchOptions.body = this.options.requestBody;
fetchOptions.body = this.options.requestBody as BodyInit;
// If streaming, we need to set duplex mode
if (this.options.requestBody instanceof ReadableStream) {

View File

@@ -3,8 +3,9 @@ import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import * as path from 'path';
import * as stream from 'stream';
export { http, https, fs, path };
export { http, https, fs, path, stream };
// pushrocks scope
import * as smartpromise from '@push.rocks/smartpromise';

View File

@@ -86,9 +86,7 @@ export class CoreRequest extends AbstractCoreRequest<
// Handle unix socket URLs
if (CoreRequest.isUnixSocket(this.url)) {
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(
this.options.path,
);
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(this.url);
this.options.socketPath = socketPath;
this.options.path = path;
}
@@ -147,6 +145,12 @@ export class CoreRequest extends AbstractCoreRequest<
this.options.requestBody.pipe(request).on('finish', () => {
request.end();
});
} else if (this.options.requestBody instanceof ReadableStream) {
// Convert web ReadableStream to Node.js Readable stream
const nodeStream = plugins.stream.Readable.fromWeb(this.options.requestBody as any);
nodeStream.pipe(request).on('finish', () => {
request.end();
});
} else {
// Write body as-is - caller is responsible for serialization
const bodyData =

View File

@@ -115,10 +115,12 @@ export class CoreResponse<T = any>
*/
async arrayBuffer(): Promise<ArrayBuffer> {
const buffer = await this.collectBody();
return buffer.buffer.slice(
const sliced = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
);
// Ensure we return ArrayBuffer, not SharedArrayBuffer
return sliced instanceof ArrayBuffer ? sliced : new ArrayBuffer(0);
}
/**