Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8622ee78d1 | |||
| c08e8b9132 | |||
| 980675ea05 | |||
| 15819d8a23 | |||
| bc71d2e5a8 | |||
| 0cf48b3688 | |||
| 1305b92ebe | |||
| 8b52ca1021 | |||
| e14800f077 | |||
| 9f3503704b | |||
| f3ba77050a | |||
| 6211acd60b | |||
| 32332309dc | |||
| 9d29bd92da | |||
| 6d148bb59e | |||
| e0f586693c |
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"@git.zone/npmts": {
|
||||||
|
"coverageTreshold": 50
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {
|
||||||
|
"npmGlobalTools": [],
|
||||||
|
"npmRegistryUrl": "registry.npmjs.org"
|
||||||
|
},
|
||||||
|
"@git.zone/cli": {
|
||||||
|
"projectType": "npm",
|
||||||
|
"module": {
|
||||||
|
"githost": "code.foss.global",
|
||||||
|
"gitscope": "push.rocks",
|
||||||
|
"gitrepo": "smartrequest",
|
||||||
|
"shortDescription": "modern HTTP request utilities",
|
||||||
|
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||||
|
"npmPackagename": "@push.rocks/smartrequest",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"HTTP",
|
||||||
|
"HTTPS",
|
||||||
|
"request library",
|
||||||
|
"form data",
|
||||||
|
"file uploads",
|
||||||
|
"JSON",
|
||||||
|
"binary data",
|
||||||
|
"streams",
|
||||||
|
"keepAlive",
|
||||||
|
"TypeScript",
|
||||||
|
"modern web requests",
|
||||||
|
"drop-in replacement",
|
||||||
|
"Bun",
|
||||||
|
"Deno",
|
||||||
|
"Node.js",
|
||||||
|
"unix sockets"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@git.zone/tsdoc": {
|
||||||
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,73 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-05-01 - 5.0.2 - fix(core-node)
|
||||||
|
update agentkeepalive import usage and align package metadata with smartconfig
|
||||||
|
|
||||||
|
- Switches the Node plugin integration to use the default agentkeepalive export and map HttpAgent/HttpsAgent from it.
|
||||||
|
- Adds .smartconfig.json and includes it together with the license file in the published package.
|
||||||
|
- Updates npmextra.json to the new scoped configuration keys and expands project release metadata and keywords.
|
||||||
|
- Adjusts package metadata to include the author email address.
|
||||||
|
|
||||||
|
## 2025-11-17 - 5.0.1 - fix(test)
|
||||||
|
Enable --logfile in test script and bump @git.zone/tstest to ^2.8.2
|
||||||
|
|
||||||
|
- Update npm script: add --logfile flag to the test command to produce test logs
|
||||||
|
- Bump devDependency @git.zone/tstest from ^2.8.1 to ^2.8.2
|
||||||
|
|
||||||
|
## 2025-11-17 - 5.0.0 - BREAKING CHANGE(client/streaming)
|
||||||
|
Unify streaming APIs: remove raw()/streamNode() and standardize on web ReadableStream across runtimes
|
||||||
|
|
||||||
|
- Removed SmartRequest.raw() and RawStreamFunction type. The raw streaming function API is gone — use .stream() with a web ReadableStream for request body streaming.
|
||||||
|
- Removed response.streamNode() from all runtimes. Responses now expose only response.stream() (ReadableStream<Uint8Array>). Node.js consumers must convert using Readable.fromWeb() if a Node.js stream is required.
|
||||||
|
- Node implementation now uses Readable.toWeb() to convert native Node streams into web ReadableStream for a single cross-platform streaming API.
|
||||||
|
- Client request.stream() still accepts Node.js streams but they are converted internally to web streams; temporary internal properties for raw streaming were removed.
|
||||||
|
- Updated tests and documentation (readme) with migration guidance and examples for converting between web and Node.js streams.
|
||||||
|
- Bumped devDependencies (@git.zone/tsbuild, tsrun, tstest) and upgraded form-data to a newer patch release.
|
||||||
|
|
||||||
|
## 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)
|
## 2025-10-26 - 4.3.6 - fix(ci)
|
||||||
Use .npmrc for registry authentication in Gitea workflow and add conditional npmjs publish
|
Use .npmrc for registry authentication in Gitea workflow and add conditional npmjs publish
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2026 Task Venture Capital GmbH <hello@task.vc>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+18
-6
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"npmts": {
|
"@git.zone/npmts": {
|
||||||
"coverageTreshold": 50
|
"coverageTreshold": 50
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"npmAccessLevel": "public"
|
"npmRegistryUrl": "registry.npmjs.org"
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "push.rocks",
|
"gitscope": "push.rocks",
|
||||||
"gitrepo": "smartrequest",
|
"gitrepo": "smartrequest",
|
||||||
|
"shortDescription": "modern HTTP request utilities",
|
||||||
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||||
"npmPackagename": "@push.rocks/smartrequest",
|
"npmPackagename": "@push.rocks/smartrequest",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -27,11 +28,22 @@
|
|||||||
"keepAlive",
|
"keepAlive",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"modern web requests",
|
"modern web requests",
|
||||||
"drop-in replacement"
|
"drop-in replacement",
|
||||||
|
"Bun",
|
||||||
|
"Deno",
|
||||||
|
"Node.js",
|
||||||
|
"unix sockets"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tsdoc": {
|
"@git.zone/tsdoc": {
|
||||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-10
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartrequest",
|
"name": "@push.rocks/smartrequest",
|
||||||
"version": "4.3.6",
|
"version": "5.0.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist_ts/index.js",
|
".": "./dist_ts/index.js",
|
||||||
"./core_node": "./dist_ts/core_node/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",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --verbose --timeout 120)",
|
"test": "(tstest test/ --verbose --timeout 120 --logfile)",
|
||||||
"build": "(tsbuild --web)",
|
"build": "(tsbuild --web)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
@@ -30,26 +32,30 @@
|
|||||||
"keepAlive",
|
"keepAlive",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"modern web requests",
|
"modern web requests",
|
||||||
"drop-in replacement"
|
"drop-in replacement",
|
||||||
|
"Bun",
|
||||||
|
"Deno",
|
||||||
|
"Node.js",
|
||||||
|
"unix sockets"
|
||||||
],
|
],
|
||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH <hello@task.vc>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://code.foss.global/push.rocks/smartrequest/issues"
|
"url": "https://code.foss.global/push.rocks/smartrequest/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://code.foss.global/push.rocks/smartrequest#readme",
|
"homepage": "https://code.foss.global/push.rocks/smartrequest#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartenv": "^5.0.13",
|
"@push.rocks/smartenv": "^6.0.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.4",
|
"@push.rocks/smartpromise": "^4.0.4",
|
||||||
"@push.rocks/smarturl": "^3.1.0",
|
"@push.rocks/smarturl": "^3.1.0",
|
||||||
"agentkeepalive": "^4.5.0",
|
"agentkeepalive": "^4.5.0",
|
||||||
"form-data": "^4.0.4"
|
"form-data": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.4",
|
"@git.zone/tsbuild": "^3.1.0",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^2.0.0",
|
||||||
"@git.zone/tstest": "^2.3.4",
|
"@git.zone/tstest": "^2.8.2",
|
||||||
"@types/node": "^22.9.0"
|
"@types/node": "^22.9.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -61,6 +67,8 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
|
".smartconfig.json",
|
||||||
|
"license",
|
||||||
"npmextra.json",
|
"npmextra.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
|
|||||||
Generated
+2246
-3547
File diff suppressed because it is too large
Load Diff
+52
-14
@@ -4,26 +4,28 @@
|
|||||||
|
|
||||||
- supports http
|
- supports http
|
||||||
- supports https
|
- supports https
|
||||||
- supports unix socks
|
- supports unix sockets on Node.js, Bun, and Deno
|
||||||
- supports formData
|
- supports formData
|
||||||
- supports file uploads
|
- supports file uploads
|
||||||
- supports best practice keepAlive
|
- supports best practice keepAlive
|
||||||
- dedicated functions for working with JSON request/response cycles
|
- dedicated functions for working with JSON request/response cycles
|
||||||
- written in TypeScript
|
- written in TypeScript
|
||||||
- continuously updated
|
- continuously updated
|
||||||
- uses node native http and https modules
|
- supports Node.js, Bun, Deno, and browser environments with automatic runtime detection
|
||||||
- supports both Node.js and browser environments
|
- 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
|
- 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
|
- Base layer (ts/core_base/) contains abstract classes and unified types
|
||||||
- Node.js implementation (ts/core_node/) uses native http/https modules
|
- Node.js implementation (ts/core_node/) uses native http/https modules with unix socket support
|
||||||
- Fetch implementation (ts/core_fetch/) uses Fetch API for browser compatibility
|
- Bun implementation (ts/core_bun/) uses Bun's native fetch with unix socket support via `unix` option
|
||||||
- Core module (ts/core/) dynamically selects the appropriate implementation based on environment
|
- Deno implementation (ts/core_deno/) uses Deno's fetch with unix socket support via HttpClient proxy
|
||||||
- Client API (ts/client/) provides a fluent, chainable interface
|
- Browser implementation (ts/core_fetch/) uses standard Fetch API for browser compatibility
|
||||||
- Legacy API has been completely removed in v3.0.0
|
- 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
|
## Key Components
|
||||||
|
|
||||||
@@ -56,6 +58,31 @@
|
|||||||
- `stream()` returns native web ReadableStream from response.body
|
- `stream()` returns native web ReadableStream from response.body
|
||||||
- `streamNode()` throws error explaining it's not available in browser
|
- `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/)
|
### Core Module (ts/core/)
|
||||||
|
|
||||||
- Dynamically loads appropriate implementation based on environment
|
- Dynamically loads appropriate implementation based on environment
|
||||||
@@ -70,10 +97,15 @@
|
|||||||
|
|
||||||
### Stream Handling
|
### 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
|
- In Node.js, converts native streams to web streams
|
||||||
- `streamNode()` available only in Node.js environment for native streams
|
- `streamNode()` availability by runtime:
|
||||||
- Consistent API across platforms while preserving platform-specific capabilities
|
- **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
|
### Binary Request Handling
|
||||||
|
|
||||||
@@ -85,5 +117,11 @@
|
|||||||
|
|
||||||
- Use `pnpm test` to run all tests
|
- Use `pnpm test` to run all tests
|
||||||
- Tests use @git.zone/tstest/tapbundle for assertions
|
- 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
|
- Browser tests run in headless Chromium via puppeteer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @push.rocks/smartrequest
|
# @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
|
## Install
|
||||||
|
|
||||||
@@ -18,26 +18,30 @@ yarn add @push.rocks/smartrequest
|
|||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- 🚀 **Modern Fetch-like API** - Familiar response methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`)
|
- 🚀 **Modern Fetch-like API** - Familiar response methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`)
|
||||||
- 🌐 **Cross-Platform** - Works in both Node.js and browsers with a unified API
|
- 🌐 **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 only)
|
- 🔌 **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
|
- 📦 **Form Data & File Uploads** - Built-in support for multipart/form-data
|
||||||
- 🔁 **Pagination Support** - Multiple strategies (offset, cursor, Link headers)
|
- 🔁 **Pagination Support** - Multiple strategies (offset, cursor, Link headers)
|
||||||
- ⚡ **Keep-Alive Connections** - Efficient connection pooling in Node.js
|
- ⚡ **Keep-Alive Connections** - Efficient connection pooling in Node.js
|
||||||
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
|
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
|
||||||
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
|
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
|
||||||
- 📡 **Streaming Support** - Stream buffers, files, and custom data without loading into memory
|
- 📡 **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
|
## 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 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 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
|
- **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
|
## Usage
|
||||||
|
|
||||||
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for modern applications.
|
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for modern applications.
|
||||||
@@ -178,11 +182,11 @@ async function downloadImage(url: string) {
|
|||||||
return Buffer.from(buffer); // Convert ArrayBuffer to Buffer if needed
|
return Buffer.from(buffer); // Convert ArrayBuffer to Buffer if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streaming response (Web Streams API)
|
// Streaming response (Web Streams API - cross-platform)
|
||||||
async function streamLargeFile(url: string) {
|
async function streamLargeFile(url: string) {
|
||||||
const response = await SmartRequest.create().url(url).get();
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
|
||||||
// Get a web-style ReadableStream (works in both Node.js and browsers)
|
// Get a web-style ReadableStream (works everywhere)
|
||||||
const stream = response.stream();
|
const stream = response.stream();
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
@@ -200,12 +204,14 @@ async function streamLargeFile(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node.js specific stream (only in Node.js environment)
|
// Convert to Node.js stream if needed (Node.js only)
|
||||||
async function streamWithNodeApi(url: string) {
|
async function streamWithNodeApi(url: string) {
|
||||||
const response = await SmartRequest.create().url(url).get();
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
|
||||||
// Only available in Node.js, throws error in browser
|
// Convert web stream to Node.js stream
|
||||||
const nodeStream = response.streamNode();
|
import { Readable } from 'stream';
|
||||||
|
const webStream = response.stream();
|
||||||
|
const nodeStream = Readable.fromWeb(webStream);
|
||||||
|
|
||||||
nodeStream.on('data', (chunk) => {
|
nodeStream.on('data', (chunk) => {
|
||||||
console.log(`Received ${chunk.length} bytes of data`);
|
console.log(`Received ${chunk.length} bytes of data`);
|
||||||
@@ -226,8 +232,7 @@ The response object provides these methods:
|
|||||||
- `text(): Promise<string>` - Get response as text
|
- `text(): Promise<string>` - Get response as text
|
||||||
- `arrayBuffer(): Promise<ArrayBuffer>` - Get response as ArrayBuffer
|
- `arrayBuffer(): Promise<ArrayBuffer>` - Get response as ArrayBuffer
|
||||||
- `stream(): ReadableStream<Uint8Array> | null` - Get web-style ReadableStream (cross-platform)
|
- `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 object
|
||||||
- `raw(): Response | http.IncomingMessage` - Get the underlying platform response
|
|
||||||
|
|
||||||
Each body method can only be called once per response, similar to the fetch API.
|
Each body method can only be called once per response, similar to the fetch API.
|
||||||
|
|
||||||
@@ -312,66 +317,67 @@ import { SmartRequest } from '@push.rocks/smartrequest';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
// Stream a Buffer directly
|
// Stream a Buffer directly (works everywhere)
|
||||||
async function uploadBuffer() {
|
async function uploadBuffer() {
|
||||||
const buffer = Buffer.from('Hello, World!');
|
const buffer = Buffer.from('Hello, World!');
|
||||||
|
|
||||||
const response = await SmartRequest.create()
|
const response = await SmartRequest.create()
|
||||||
.url('https://api.example.com/upload')
|
.url('https://api.example.com/upload')
|
||||||
.buffer(buffer, 'text/plain')
|
.buffer(buffer, 'text/plain')
|
||||||
.post();
|
.post();
|
||||||
|
|
||||||
return await response.json();
|
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) {
|
async function uploadLargeFile(filePath: string) {
|
||||||
const fileStream = fs.createReadStream(filePath);
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
const response = await SmartRequest.create()
|
const response = await SmartRequest.create()
|
||||||
.url('https://api.example.com/upload')
|
.url('https://api.example.com/upload')
|
||||||
.stream(fileStream, 'application/octet-stream')
|
.stream(fileStream, 'application/octet-stream')
|
||||||
.post();
|
.post();
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream data from any readable source
|
// Stream data from any readable source (Node.js only)
|
||||||
async function streamData(dataSource: Readable) {
|
async function streamData(dataSource: Readable) {
|
||||||
const response = await SmartRequest.create()
|
const response = await SmartRequest.create()
|
||||||
.url('https://api.example.com/stream')
|
.url('https://api.example.com/stream')
|
||||||
.stream(dataSource)
|
.stream(dataSource)
|
||||||
.post();
|
.post();
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced: Full control over request streaming (Node.js only)
|
// Send Uint8Array (works everywhere)
|
||||||
async function customStreaming() {
|
|
||||||
const response = await SmartRequest.create()
|
|
||||||
.url('https://api.example.com/stream')
|
|
||||||
.raw((request) => {
|
|
||||||
// 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() {
|
async function uploadBinaryData() {
|
||||||
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
|
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
|
||||||
|
|
||||||
const response = await SmartRequest.create()
|
const response = await SmartRequest.create()
|
||||||
.url('https://api.example.com/binary')
|
.url('https://api.example.com/binary')
|
||||||
.buffer(data, 'application/octet-stream')
|
.buffer(data, 'application/octet-stream')
|
||||||
.post();
|
.post();
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -379,20 +385,15 @@ async function uploadBinaryData() {
|
|||||||
#### Streaming Methods
|
#### Streaming Methods
|
||||||
|
|
||||||
- **`.buffer(data, contentType?)`** - Stream a Buffer or Uint8Array directly
|
- **`.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')
|
- `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(stream, contentType?)`** - Stream from ReadableStream or Node.js stream
|
||||||
- `stream`: Web ReadableStream (both platforms) or Node.js stream (Node.js only)
|
- `stream`: Web ReadableStream (cross-platform) or Node.js stream (Node.js only)
|
||||||
- `contentType`: Optional content type
|
- `contentType`: Optional content type
|
||||||
- ✅ Web ReadableStream works in both Node.js and browsers
|
- ✅ Web ReadableStream works everywhere (Node.js, Bun, Deno, browsers)
|
||||||
- ⚠️ Node.js streams only work in Node.js environment
|
- ⚠️ 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
|
|
||||||
- Use for advanced scenarios like chunked transfer encoding
|
|
||||||
|
|
||||||
These methods are particularly useful for:
|
These methods are particularly useful for:
|
||||||
- Uploading large files without loading them into memory
|
- Uploading large files without loading them into memory
|
||||||
@@ -400,12 +401,14 @@ These methods are particularly useful for:
|
|||||||
- Proxying data between services
|
- Proxying data between services
|
||||||
- Implementing chunked transfer encoding
|
- 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
|
```typescript
|
||||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
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() {
|
async function queryViaUnixSocket() {
|
||||||
const response = await SmartRequest.create()
|
const response = await SmartRequest.create()
|
||||||
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
|
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
|
||||||
@@ -413,6 +416,57 @@ async function queryViaUnixSocket() {
|
|||||||
|
|
||||||
return await response.json();
|
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
|
### Pagination Support
|
||||||
@@ -599,12 +653,61 @@ const response = await SmartRequest.create()
|
|||||||
.get();
|
.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 natively
|
||||||
|
const streamResponse = await SmartRequest.create()
|
||||||
|
.url('https://api.example.com/data')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const webStream = streamResponse.stream(); // ✅ Use web streams in Bun
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 natively
|
||||||
|
const streamResponse = await SmartRequest.create()
|
||||||
|
.url('https://api.example.com/data')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const webStream = streamResponse.stream(); // ✅ Use web streams in Deno
|
||||||
|
```
|
||||||
|
|
||||||
## Complete Example: Building a REST API Client
|
## Complete Example: Building a REST API Client
|
||||||
|
|
||||||
Here's a complete example of building a typed API client:
|
Here's a complete example of building a typed API client:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartRequest, type CoreResponse } from '@push.rocks/smartrequest';
|
import { SmartRequest, type ICoreResponse } from '@push.rocks/smartrequest';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -644,6 +747,9 @@ class BlogApiClient {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to delete post: ${response.statusText}`);
|
throw new Error(`Failed to delete post: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consume the body
|
||||||
|
await response.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllPosts(userId?: number): Promise<Post[]> {
|
async getAllPosts(userId?: number): Promise<Post[]> {
|
||||||
@@ -707,9 +813,158 @@ 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 v4.x to v5.x
|
||||||
|
|
||||||
|
Version 5.0 completes the transition to modern web standards by removing Node.js-specific streaming APIs:
|
||||||
|
|
||||||
|
#### **Breaking Changes**
|
||||||
|
|
||||||
|
1. **`.streamNode()` Method Removed**
|
||||||
|
- The `.streamNode()` method has been removed from all response objects
|
||||||
|
- Use the cross-platform `.stream()` method instead, which returns a web `ReadableStream<Uint8Array>`
|
||||||
|
- For Node.js users who need Node.js streams, convert using `Readable.fromWeb()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Before (v4.x) - Node.js only
|
||||||
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
const nodeStream = response.streamNode();
|
||||||
|
|
||||||
|
// ✅ After (v5.x) - Cross-platform
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
const webStream = response.stream();
|
||||||
|
const nodeStream = Readable.fromWeb(webStream); // Convert to Node.js stream
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Request `.raw()` Method Removed**
|
||||||
|
- The `.raw(streamFunc)` method has been removed from the SmartRequest client
|
||||||
|
- Use `.stream()` with a web `ReadableStream` instead for request body streaming
|
||||||
|
- Node.js users can create web streams from Node.js streams using `Readable.toWeb()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Before (v4.x) - Node.js only
|
||||||
|
const response = await SmartRequest.create()
|
||||||
|
.url(url)
|
||||||
|
.raw((request) => {
|
||||||
|
request.write('chunk1');
|
||||||
|
request.write('chunk2');
|
||||||
|
request.end();
|
||||||
|
})
|
||||||
|
.post();
|
||||||
|
|
||||||
|
// ✅ After (v5.x) - Cross-platform
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(new TextEncoder().encode('chunk1'));
|
||||||
|
controller.enqueue(new TextEncoder().encode('chunk2'));
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await SmartRequest.create()
|
||||||
|
.url(url)
|
||||||
|
.stream(stream)
|
||||||
|
.post();
|
||||||
|
|
||||||
|
// Or convert from Node.js stream (Node.js only)
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const nodeStream = fs.createReadStream('file.txt');
|
||||||
|
const webStream = Readable.toWeb(nodeStream);
|
||||||
|
|
||||||
|
const response = await SmartRequest.create()
|
||||||
|
.url(url)
|
||||||
|
.stream(webStream)
|
||||||
|
.post();
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Response `.raw()` Method Preserved**
|
||||||
|
- The `response.raw()` method is still available for accessing platform-specific response objects
|
||||||
|
- Returns `http.IncomingMessage` in Node.js or `Response` in other runtimes
|
||||||
|
- Use for advanced scenarios requiring access to raw platform objects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Still works in v5.x
|
||||||
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
const rawResponse = response.raw(); // http.IncomingMessage or Response
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Migration Guide**
|
||||||
|
|
||||||
|
**For Response Streaming:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before (v4.x)
|
||||||
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
const nodeStream = response.streamNode();
|
||||||
|
|
||||||
|
nodeStream.on('data', (chunk) => {
|
||||||
|
console.log(`Received ${chunk.length} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// After (v5.x) - Option 1: Use web streams directly
|
||||||
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
const webStream = response.stream();
|
||||||
|
|
||||||
|
if (webStream) {
|
||||||
|
const reader = webStream.getReader();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
console.log(`Received ${value.length} bytes`);
|
||||||
|
}
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// After (v5.x) - Option 2: Convert to Node.js stream (Node.js only)
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
const response = await SmartRequest.create().url(url).get();
|
||||||
|
const webStream = response.stream();
|
||||||
|
const nodeStream = Readable.fromWeb(webStream);
|
||||||
|
|
||||||
|
nodeStream.on('data', (chunk) => {
|
||||||
|
console.log(`Received ${chunk.length} bytes`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Request Streaming:**
|
||||||
|
|
||||||
|
Node.js streams are still accepted by the `.stream()` method and automatically converted internally. No changes required for most use cases:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Still works in v5.x
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const fileStream = fs.createReadStream('large-file.bin');
|
||||||
|
const response = await SmartRequest.create()
|
||||||
|
.url('https://api.example.com/upload')
|
||||||
|
.stream(fileStream, 'application/octet-stream')
|
||||||
|
.post();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ True cross-platform compatibility
|
||||||
|
- ✅ Modern web standards
|
||||||
|
- ✅ Cleaner API surface
|
||||||
|
- ✅ Single streaming approach works everywhere
|
||||||
|
|
||||||
|
### 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.
|
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.
|
2. **Unified Response API**: All responses now use the same fetch-like interface regardless of platform.
|
||||||
|
|||||||
@@ -1,71 +1,58 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import * as fs from 'fs';
|
|
||||||
import { SmartRequest } from '../ts/index.js';
|
import { SmartRequest } from '../ts/index.js';
|
||||||
|
|
||||||
|
// Cross-platform tests using web-standard APIs only
|
||||||
|
|
||||||
tap.test('should send a buffer using buffer() method', async () => {
|
tap.test('should send a buffer using buffer() method', async () => {
|
||||||
const testBuffer = Buffer.from('Hello, World!');
|
const testBuffer = Buffer.from('Hello, World!');
|
||||||
|
|
||||||
const smartRequest = SmartRequest.create()
|
const smartRequest = SmartRequest.create()
|
||||||
.url('https://httpbin.org/post')
|
.url('https://httpbin.org/post')
|
||||||
.buffer(testBuffer, 'text/plain')
|
.buffer(testBuffer, 'text/plain')
|
||||||
.method('POST');
|
.method('POST');
|
||||||
|
|
||||||
const response = await smartRequest.post();
|
const response = await smartRequest.post();
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
expect(data).toHaveProperty('data');
|
expect(data).toHaveProperty('data');
|
||||||
expect(data.data).toEqual('Hello, World!');
|
expect(data.data).toEqual('Hello, World!');
|
||||||
expect(data.headers['Content-Type']).toEqual('text/plain');
|
expect(data.headers['Content-Type']).toEqual('text/plain');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should send a stream using stream() method', async () => {
|
tap.test('should send a web ReadableStream using stream() method', async () => {
|
||||||
// Create a simple readable stream
|
|
||||||
const { Readable } = await import('stream');
|
|
||||||
const testData = 'Stream data test';
|
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()
|
const smartRequest = SmartRequest.create()
|
||||||
.url('https://httpbin.org/post')
|
.url('https://httpbin.org/post')
|
||||||
.stream(stream, 'text/plain')
|
.stream(stream, 'text/plain')
|
||||||
.method('POST');
|
.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 response = await smartRequest.post();
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
expect(data).toHaveProperty('data');
|
expect(data).toHaveProperty('data');
|
||||||
expect(data.data).toEqual(testData);
|
expect(data.data).toEqual(testData);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should send Uint8Array using buffer() method', async () => {
|
tap.test('should send Uint8Array using buffer() method', async () => {
|
||||||
const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII
|
const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII
|
||||||
|
|
||||||
const smartRequest = SmartRequest.create()
|
const smartRequest = SmartRequest.create()
|
||||||
.url('https://httpbin.org/post')
|
.url('https://httpbin.org/post')
|
||||||
.buffer(testData, 'application/octet-stream')
|
.buffer(testData, 'application/octet-stream')
|
||||||
.method('POST');
|
.method('POST');
|
||||||
|
|
||||||
const response = await smartRequest.post();
|
const response = await smartRequest.post();
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Just verify that data was sent
|
// Just verify that data was sent
|
||||||
expect(data).toHaveProperty('data');
|
expect(data).toHaveProperty('data');
|
||||||
expect(data.headers['Content-Type']).toEqual('application/octet-stream');
|
expect(data.headers['Content-Type']).toEqual('application/octet-stream');
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { SmartRequest } from '../ts/index.js';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
tap.test('should have stream() method that returns web ReadableStream', async () => {
|
||||||
|
const response = await SmartRequest.create()
|
||||||
|
.url('https://httpbin.org/get')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Verify stream() method exists
|
||||||
|
expect(response.stream).toBeDefined();
|
||||||
|
expect(typeof response.stream).toEqual('function');
|
||||||
|
|
||||||
|
// Get web stream
|
||||||
|
const webStream = response.stream();
|
||||||
|
expect(webStream).toBeDefined();
|
||||||
|
|
||||||
|
// Verify it's a web ReadableStream
|
||||||
|
expect(typeof webStream.getReader).toEqual('function');
|
||||||
|
expect(typeof webStream.cancel).toEqual('function');
|
||||||
|
|
||||||
|
// Convert to Node.js stream using Readable.fromWeb()
|
||||||
|
// Known TypeScript limitation: @types/node ReadableStream differs from web ReadableStream
|
||||||
|
const nodeStream = Readable.fromWeb(webStream as any);
|
||||||
|
expect(nodeStream).toBeDefined();
|
||||||
|
|
||||||
|
// Verify it's a Node.js readable stream
|
||||||
|
expect(typeof nodeStream.pipe).toEqual('function');
|
||||||
|
expect(typeof nodeStream.on).toEqual('function');
|
||||||
|
|
||||||
|
// Consume the stream to avoid hanging
|
||||||
|
nodeStream.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should convert web stream to Node.js stream correctly', async () => {
|
||||||
|
const response = await SmartRequest.create()
|
||||||
|
.url('https://httpbin.org/get')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const webStream = response.stream();
|
||||||
|
const nodeStream = Readable.fromWeb(webStream as any);
|
||||||
|
|
||||||
|
// Collect data from stream
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
nodeStream.on('data', (chunk) => {
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeStream.on('end', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeStream.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify we received data
|
||||||
|
const data = Buffer.concat(chunks);
|
||||||
|
expect(data.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify it's valid JSON
|
||||||
|
const json = JSON.parse(data.toString('utf-8'));
|
||||||
|
expect(json).toBeDefined();
|
||||||
|
expect(json.url).toEqual('https://httpbin.org/get');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { SmartRequest } from '../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('should have streamNode() method available', async () => {
|
|
||||||
const response = await SmartRequest.create()
|
|
||||||
.url('https://httpbin.org/get')
|
|
||||||
.get();
|
|
||||||
|
|
||||||
// Verify streamNode() method exists
|
|
||||||
expect(response.streamNode).toBeDefined();
|
|
||||||
expect(typeof response.streamNode).toEqual('function');
|
|
||||||
|
|
||||||
// In Node.js, it should return a stream
|
|
||||||
const nodeStream = response.streamNode();
|
|
||||||
expect(nodeStream).toBeDefined();
|
|
||||||
|
|
||||||
// Verify it's a Node.js readable stream
|
|
||||||
expect(typeof nodeStream.pipe).toEqual('function');
|
|
||||||
expect(typeof nodeStream.on).toEqual('function');
|
|
||||||
|
|
||||||
// Consume the stream to avoid hanging
|
|
||||||
nodeStream.resume();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartrequest',
|
name: '@push.rocks/smartrequest',
|
||||||
version: '4.3.6',
|
version: '5.0.2',
|
||||||
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
|
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type {
|
|||||||
ResponseType,
|
ResponseType,
|
||||||
FormField,
|
FormField,
|
||||||
RateLimitConfig,
|
RateLimitConfig,
|
||||||
RawStreamFunction,
|
|
||||||
} from './types/common.js';
|
} from './types/common.js';
|
||||||
import {
|
import {
|
||||||
type TPaginationConfig,
|
type TPaginationConfig,
|
||||||
@@ -142,12 +141,12 @@ export class SmartRequest<T = any> {
|
|||||||
if (!this._options.headers) {
|
if (!this._options.headers) {
|
||||||
this._options.headers = {};
|
this._options.headers = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set content type if provided
|
// Set content type if provided
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
this._options.headers['Content-Type'] = contentType;
|
this._options.headers['Content-Type'] = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a Node.js stream (has pipe method)
|
// Check if it's a Node.js stream (has pipe method)
|
||||||
if ('pipe' in stream && typeof (stream as any).pipe === 'function') {
|
if ('pipe' in stream && typeof (stream as any).pipe === 'function') {
|
||||||
// For Node.js streams, we need to use a custom approach
|
// For Node.js streams, we need to use a custom approach
|
||||||
@@ -157,18 +156,7 @@ export class SmartRequest<T = any> {
|
|||||||
// For web ReadableStream, pass directly
|
// For web ReadableStream, pass directly
|
||||||
this._options.requestBody = stream;
|
this._options.requestBody = stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide a custom function to handle raw request streaming
|
|
||||||
* This gives full control over the request body streaming
|
|
||||||
* Note: Only works in Node.js environment, not supported in browsers
|
|
||||||
*/
|
|
||||||
raw(streamFunc: RawStreamFunction): this {
|
|
||||||
// Store the raw streaming function to be used later
|
|
||||||
(this._options as any).__rawStreamFunc = streamFunc;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,22 +428,21 @@ export class SmartRequest<T = any> {
|
|||||||
// Main retry loop
|
// Main retry loop
|
||||||
for (let attempt = 0; attempt <= this._retries; attempt++) {
|
for (let attempt = 0; attempt <= this._retries; attempt++) {
|
||||||
try {
|
try {
|
||||||
// Check if we have a Node.js stream or raw function that needs special handling
|
// Check if we have a Node.js stream that needs special handling
|
||||||
let requestDataFunc = null;
|
let requestDataFunc = null;
|
||||||
if ((this._options as any).__nodeStream) {
|
if ((this._options as any).__nodeStream) {
|
||||||
const nodeStream = (this._options as any).__nodeStream;
|
const nodeStream = (this._options as any).__nodeStream;
|
||||||
requestDataFunc = (req: any) => {
|
requestDataFunc = (req: any) => {
|
||||||
nodeStream.pipe(req);
|
nodeStream.pipe(req);
|
||||||
};
|
};
|
||||||
// Remove the temporary stream reference
|
// Don't delete __nodeStream yet - let CoreRequest implementations handle it
|
||||||
delete (this._options as any).__nodeStream;
|
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new CoreRequest(this._url, this._options as any, requestDataFunc);
|
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;
|
||||||
const response = (await request.fire()) as ICoreResponse<R>;
|
const response = (await request.fire()) as ICoreResponse<R>;
|
||||||
|
|
||||||
// Check for 429 status if rate limit handling is enabled
|
// Check for 429 status if rate limit handling is enabled
|
||||||
|
|||||||
@@ -66,9 +66,3 @@ export interface RateLimitConfig {
|
|||||||
backoffFactor?: number; // Exponential backoff factor (default: 2)
|
backoffFactor?: number; // Exponential backoff factor (default: 2)
|
||||||
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
|
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw streaming function for advanced request body control
|
|
||||||
* Note: The request parameter type depends on the environment (Node.js ClientRequest or fetch Request)
|
|
||||||
*/
|
|
||||||
export type RawStreamFunction = (request: any) => void;
|
|
||||||
|
|||||||
+14
-4
@@ -5,12 +5,22 @@ export * from '../core_base/types.js';
|
|||||||
|
|
||||||
const smartenvInstance = new plugins.smartenv.Smartenv();
|
const smartenvInstance = new plugins.smartenv.Smartenv();
|
||||||
|
|
||||||
// Dynamically load the appropriate implementation
|
// Dynamically load the appropriate implementation based on runtime
|
||||||
let CoreRequest: any;
|
let CoreRequest: any;
|
||||||
let CoreResponse: any;
|
let CoreResponse: any;
|
||||||
|
|
||||||
if (smartenvInstance.isNode) {
|
if (smartenvInstance.isDeno) {
|
||||||
// In Node.js, load the node implementation
|
// 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(
|
const modulePath = plugins.smartpath.join(
|
||||||
plugins.smartpath.dirname(import.meta.url),
|
plugins.smartpath.dirname(import.meta.url),
|
||||||
'../core_node/index.js',
|
'../core_node/index.js',
|
||||||
@@ -19,7 +29,7 @@ if (smartenvInstance.isNode) {
|
|||||||
CoreRequest = impl.CoreRequest;
|
CoreRequest = impl.CoreRequest;
|
||||||
CoreResponse = impl.CoreResponse;
|
CoreResponse = impl.CoreResponse;
|
||||||
} else {
|
} 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');
|
const impl = await import('../core_fetch/index.js');
|
||||||
CoreRequest = impl.CoreRequest;
|
CoreRequest = impl.CoreRequest;
|
||||||
CoreResponse = impl.CoreResponse;
|
CoreResponse = impl.CoreResponse;
|
||||||
|
|||||||
+19
-1
@@ -17,10 +17,28 @@ export abstract class CoreRequest<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses socket path and route from unix socket URL
|
* 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 } {
|
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 parseRegex = /(.*):(.*)/;
|
||||||
const result = parseRegex.exec(url);
|
const result = parseRegex.exec(cleanUrl);
|
||||||
return {
|
return {
|
||||||
socketPath: result[1],
|
socketPath: result[1],
|
||||||
path: result[2],
|
path: result[2],
|
||||||
|
|||||||
@@ -42,9 +42,4 @@ export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> {
|
|||||||
* Get response as a web-style ReadableStream
|
* Get response as a web-style ReadableStream
|
||||||
*/
|
*/
|
||||||
abstract stream(): ReadableStream<Uint8Array> | null;
|
abstract stream(): ReadableStream<Uint8Array> | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get response as a Node.js stream (throws in browser)
|
|
||||||
*/
|
|
||||||
abstract streamNode(): NodeJS.ReadableStream | never;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,5 +86,4 @@ export interface ICoreResponse<T = any> {
|
|||||||
text(): Promise<string>;
|
text(): Promise<string>;
|
||||||
arrayBuffer(): Promise<ArrayBuffer>;
|
arrayBuffer(): Promise<ArrayBuffer>;
|
||||||
stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream
|
stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream
|
||||||
streamNode(): NodeJS.ReadableStream | never; // Returns Node.js stream or throws in browser
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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 the raw Response object
|
||||||
|
*/
|
||||||
|
raw(): Response {
|
||||||
|
return this.responseClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 {};
|
||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw Response object
|
||||||
|
*/
|
||||||
|
raw(): Response {
|
||||||
|
return this.responseClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
// Check for Buffer (Node.js polyfills in browser may provide this)
|
// Check for Buffer (Node.js polyfills in browser may provide this)
|
||||||
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
|
(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 streaming, we need to set duplex mode
|
||||||
if (this.options.requestBody instanceof ReadableStream) {
|
if (this.options.requestBody instanceof ReadableStream) {
|
||||||
|
|||||||
@@ -72,15 +72,6 @@ export class CoreResponse<T = any>
|
|||||||
return this.response.body;
|
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
|
* Get the raw Response object
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import * as fs from 'fs';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as stream from 'stream';
|
||||||
|
|
||||||
export { http, https, fs, path };
|
export { http, https, fs, path, stream };
|
||||||
|
|
||||||
// pushrocks scope
|
// pushrocks scope
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
@@ -13,8 +14,11 @@ import * as smarturl from '@push.rocks/smarturl';
|
|||||||
export { smartpromise, smarturl };
|
export { smartpromise, smarturl };
|
||||||
|
|
||||||
// third party scope
|
// third party scope
|
||||||
import { HttpAgent, HttpsAgent } from 'agentkeepalive';
|
import AgentKeepAlive from 'agentkeepalive';
|
||||||
const agentkeepalive = { HttpAgent, HttpsAgent };
|
const agentkeepalive = {
|
||||||
|
HttpAgent: AgentKeepAlive.HttpAgent,
|
||||||
|
HttpsAgent: AgentKeepAlive.HttpsAgent,
|
||||||
|
};
|
||||||
import formData from 'form-data';
|
import formData from 'form-data';
|
||||||
|
|
||||||
export { agentkeepalive, formData };
|
export { agentkeepalive, formData };
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
|
|
||||||
// Handle unix socket URLs
|
// Handle unix socket URLs
|
||||||
if (CoreRequest.isUnixSocket(this.url)) {
|
if (CoreRequest.isUnixSocket(this.url)) {
|
||||||
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(
|
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(this.url);
|
||||||
this.options.path,
|
|
||||||
);
|
|
||||||
this.options.socketPath = socketPath;
|
this.options.socketPath = socketPath;
|
||||||
this.options.path = path;
|
this.options.path = path;
|
||||||
}
|
}
|
||||||
@@ -147,6 +145,12 @@ export class CoreRequest extends AbstractCoreRequest<
|
|||||||
this.options.requestBody.pipe(request).on('finish', () => {
|
this.options.requestBody.pipe(request).on('finish', () => {
|
||||||
request.end();
|
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 {
|
} else {
|
||||||
// Write body as-is - caller is responsible for serialization
|
// Write body as-is - caller is responsible for serialization
|
||||||
const bodyData =
|
const bodyData =
|
||||||
|
|||||||
@@ -115,10 +115,12 @@ export class CoreResponse<T = any>
|
|||||||
*/
|
*/
|
||||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||||
const buffer = await this.collectBody();
|
const buffer = await this.collectBody();
|
||||||
return buffer.buffer.slice(
|
const sliced = buffer.buffer.slice(
|
||||||
buffer.byteOffset,
|
buffer.byteOffset,
|
||||||
buffer.byteOffset + buffer.byteLength,
|
buffer.byteOffset + buffer.byteLength,
|
||||||
);
|
);
|
||||||
|
// Ensure we return ArrayBuffer, not SharedArrayBuffer
|
||||||
|
return sliced instanceof ArrayBuffer ? sliced : new ArrayBuffer(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,41 +129,15 @@ export class CoreResponse<T = any>
|
|||||||
stream(): ReadableStream<Uint8Array> | null {
|
stream(): ReadableStream<Uint8Array> | null {
|
||||||
this.ensureNotConsumed();
|
this.ensureNotConsumed();
|
||||||
|
|
||||||
// Convert Node.js stream to web stream
|
// Convert Node.js stream to web stream using Readable.toWeb()
|
||||||
// In Node.js 16.5+ we can use Readable.toWeb()
|
// This creates a proper Node.js-compatible web stream that works with Readable.fromWeb()
|
||||||
if (this.incomingMessage.readableEnded || this.incomingMessage.destroyed) {
|
if (this.incomingMessage.readableEnded || this.incomingMessage.destroyed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a web ReadableStream from the Node.js stream
|
// Use Readable.toWeb() to convert Node.js stream to web stream (Node.js 16.5+)
|
||||||
const nodeStream = this.incomingMessage;
|
// The returned type is automatically Node.js ReadableStream which is compatible with Readable.fromWeb()
|
||||||
return new ReadableStream<Uint8Array>({
|
return plugins.stream.Readable.toWeb(this.incomingMessage) as any;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user