Compare commits
220 Commits
Author | SHA1 | Date | |
---|---|---|---|
b4769e7feb | |||
4cbca08f43 | |||
cf24bf94b9 | |||
3e24f1c5a8 | |||
2dc82bd730 | |||
8e75047d1f | |||
eb2ccd8d9f | |||
bc99aa3569 | |||
94bf23ad55 | |||
ea54a8aeda | |||
18d8ab0278 | |||
b8d707b363 | |||
7dcc5f3fe2 | |||
8f5c88b47e | |||
28a56b87bc | |||
d627bc870e | |||
2cded974a8 | |||
31c25c8333 | |||
01bbfa4a06 | |||
0ebd47d1b2 | |||
bbb57004d9 | |||
f7d2c6de4f | |||
b8f545cdd5 | |||
96820090d4 | |||
6e2c63fe1b | |||
39d3bb4d24 | |||
62db3a9bc5 | |||
a82d9eafe2 | |||
f55ab55365 | |||
9cf8c0b0f3 | |||
49796f89dd | |||
aaeaa23b79 | |||
85199b8839 | |||
33f7df28e2 | |||
29ee46b3a2 | |||
18dd110d4e | |||
fa93f13306 | |||
81694cf58c | |||
fdd1c7cdb3 | |||
40f330791f | |||
16b4d168db | |||
cc017a9bbf | |||
2adf7e9ee2 | |||
2303b6da7e | |||
4487579bfd | |||
8d2bbcae2a | |||
deb25a3068 | |||
0a83d8b476 | |||
8e7c730d86 | |||
fe50adb1ff | |||
cd75f7acd8 | |||
bb0dd6426a | |||
d471376681 | |||
b882922f2b | |||
9a0d35c325 | |||
7b49bba0d2 | |||
6600a23a00 | |||
e2845c9992 | |||
5e6f2c6fbf | |||
d3d0649b73 | |||
fba43df3c4 | |||
c6fa540543 | |||
1891b54389 | |||
fee8443af1 | |||
c48f956ae3 | |||
4a4b64a2c4 | |||
43d4b47782 | |||
6d970cb925 | |||
43710c930e | |||
306dd7c366 | |||
3d69d97891 | |||
a6d52702fd | |||
de31ee6093 | |||
cd2d7b2525 | |||
2d4a75c9cd | |||
557fec0386 | |||
e803f9e48d | |||
76c714a931 | |||
e8669f0420 | |||
d9e6214a7e | |||
7c4227bfc6 | |||
e55a521395 | |||
06fc279caf | |||
e89e317bbc | |||
d182832e47 | |||
92059a50de | |||
db80f2df7e | |||
145505b891 | |||
f4f50c6a94 | |||
d204059313 | |||
00210566d5 | |||
14245b2521 | |||
f0fa91e2db | |||
19a1fe1524 | |||
27770a8ad1 | |||
ab48f11e83 | |||
a0a9e3f824 | |||
c829b06169 | |||
80fa40baf4 | |||
3659b80e1e | |||
770e7d46ea | |||
2a46f2a306 | |||
eae4d09664 | |||
23a2f597fc | |||
c278249c32 | |||
a32c372374 | |||
f98972d9fe | |||
acebe6a381 | |||
7031504852 | |||
3010a1da9a | |||
cdead33be4 | |||
5e23649702 | |||
cc6bd5726a | |||
f487584e80 | |||
443bccd4c9 | |||
f359856b1d | |||
bda04f124b | |||
466187fd52 | |||
d22504317e | |||
6e31d84798 | |||
36472b7306 | |||
e86f14b8d8 | |||
2b9e7f6dd2 | |||
5e4afccf9c | |||
3de7a1a210 | |||
bd2a5eedff | |||
aa18357d75 | |||
9960aff219 | |||
03d884ed59 | |||
9a0ac6fc62 | |||
ad35ea4eb8 | |||
ffb0195f04 | |||
78737c24df | |||
6e276eda00 | |||
021d26a23a | |||
c9c8a1234c | |||
dffabd905f | |||
36f2707141 | |||
b00d674b6f | |||
b09598d465 | |||
acc7b2d46f | |||
16a97a420c | |||
a73c78e54b | |||
1f408b5123 | |||
284f4967f4 | |||
55c80c1403 | |||
7a3e565dbb | |||
6f5d10ccd3 | |||
f1ddab72f6 | |||
376225888c | |||
63e8660f6c | |||
2358b1d48f | |||
9db29bacc2 | |||
5f27b6834c | |||
6717ecf80c | |||
7784f99878 | |||
54a0521f9e | |||
ef25315d59 | |||
74b6bf230f | |||
fe693e6383 | |||
6014e94ee0 | |||
88927fa6f8 | |||
e8133247f7 | |||
4e51ed315e | |||
0ffc44b3ef | |||
a8291febec | |||
c0704eb2d8 | |||
9eeb9c16b6 | |||
52bf520eb9 | |||
c9f6198114 | |||
0a17591eae | |||
3417f09cdb | |||
20dc3c9230 | |||
6f865a356f | |||
71a6ffef96 | |||
189916e62b | |||
33e36b5d44 | |||
dbe999eea7 | |||
31b326cf51 | |||
0c03763281 | |||
21e85062f7 | |||
33670bb4d5 | |||
6893e1f460 | |||
3a1943417b | |||
8e6834da02 | |||
7d78890e14 | |||
5c413947a4 | |||
5de63a1ef3 | |||
ee7fa87b25 | |||
4b65674fb2 | |||
14f48c99d0 | |||
e9f1d5697f | |||
b4a9d1aa0c | |||
d77d2b13da | |||
aaa2394b36 | |||
99efccd827 | |||
b1cfca5f35 | |||
ca4ef6d5c0 | |||
a7516c86e6 | |||
abece86511 | |||
8eca91145b | |||
d7a9d173b8 | |||
fcef5fcb6c | |||
93595a222b | |||
5f8b5f7690 | |||
980ea344e7 | |||
9c69fb6c1c | |||
fe33cfdeaa | |||
f0c74b1568 | |||
a6920b18d4 | |||
85a647cd10 | |||
df48a06d3e | |||
52fa217fda | |||
97bbb8b38b | |||
8c941fe1b7 | |||
b821ca60cc | |||
06c945ad0f | |||
48d839b9b9 | |||
00199df3e2 | |||
3c799e950e |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,4 +1,20 @@
|
||||
node_modules/
|
||||
.nogit/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# caches
|
||||
.yarn/
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# custom
|
@@ -1,59 +0,0 @@
|
||||
image: hosttoday/ht-docker-node:npmts
|
||||
|
||||
stages:
|
||||
- test
|
||||
- release
|
||||
- trigger
|
||||
- pages
|
||||
|
||||
testLEGACY:
|
||||
stage: test
|
||||
script:
|
||||
- npmci test legacy
|
||||
tags:
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
testLTS:
|
||||
stage: test
|
||||
script:
|
||||
- npmci test lts
|
||||
tags:
|
||||
- docker
|
||||
|
||||
testSTABLE:
|
||||
stage: test
|
||||
script:
|
||||
- npmci test stable
|
||||
tags:
|
||||
- docker
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- docker
|
||||
|
||||
trigger:
|
||||
stage: trigger
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- docker
|
||||
|
||||
pages:
|
||||
image: hosttoday/ht-docker-node:npmpage
|
||||
stage: pages
|
||||
script:
|
||||
- npmci command npmpage --publish gitlab
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"npmci": {
|
||||
"type": "object",
|
||||
"description": "settings for npmci"
|
||||
},
|
||||
"gitzone": {
|
||||
"type": "object",
|
||||
"description": "settings for gitzone",
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
66
README.md
66
README.md
@@ -1,66 +0,0 @@
|
||||
# smartrequest
|
||||
dropin replacement for request
|
||||
|
||||
## Availabililty
|
||||
[](https://www.npmjs.com/package/smartrequest)
|
||||
[](https://GitLab.com/pushrocks/smartrequest)
|
||||
[](https://github.com/pushrocks/smartrequest)
|
||||
[](https://pushrocks.gitlab.io/smartrequest/)
|
||||
|
||||
## Status for master
|
||||
[](https://GitLab.com/pushrocks/smartrequest/commits/master)
|
||||
[](https://GitLab.com/pushrocks/smartrequest/commits/master)
|
||||
[](https://www.npmjs.com/package/smartrequest)
|
||||
[](https://david-dm.org/pushrocks/smartrequest)
|
||||
[](https://www.bithound.io/github/pushrocks/smartrequest/master/dependencies/npm)
|
||||
[](https://www.bithound.io/github/pushrocks/smartrequest)
|
||||
[](https://nodejs.org/dist/latest-v6.x/docs/api/)
|
||||
[](https://nodejs.org/dist/latest-v6.x/docs/api/)
|
||||
[](http://standardjs.com/)
|
||||
|
||||
## Usage
|
||||
Use TypeScript for best in class instellisense.
|
||||
|
||||
> note: smartrequest uses the **native** node request module under the hood (not the one from npm)
|
||||
|
||||
```javascript
|
||||
import * as smartrequest from 'smartrequest'
|
||||
|
||||
// simple post
|
||||
let options: smartrequest.ISmartRequestOptions = { // typed options
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Authorization": "Bearer token"
|
||||
},
|
||||
requestBody: {
|
||||
key1: 'value1',
|
||||
key2: 3
|
||||
}
|
||||
}
|
||||
|
||||
smartrequest.post('https://example.com', options).then(res => {
|
||||
console.log(res.status)
|
||||
console.log(res.body) // if json, body will be parsed automatically
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
// also available
|
||||
smartrequest.get(...)
|
||||
smartrequest.put(...)
|
||||
smartrequest.del(...)
|
||||
|
||||
// streaming
|
||||
smartrequest.get('https://example.com/bigfile.mp4', optionsArg, true).then(res => { // third arg = true signals streaming
|
||||
console.log(res.status)
|
||||
res.on('data', data => {
|
||||
// do something with the data chunk here
|
||||
}
|
||||
res.on('end', () => {
|
||||
// do something when things have ended
|
||||
})
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
[](https://push.rocks)
|
159
changelog.md
Normal file
159
changelog.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-07-29 - 4.2.0 - feat(client)
|
||||
Add handle429Backoff method for intelligent rate limit handling
|
||||
|
||||
**Features:**
|
||||
- Added `handle429Backoff()` method to SmartRequest class for automatic HTTP 429 handling
|
||||
- Respects `Retry-After` headers with support for both seconds and HTTP date formats
|
||||
- Configurable exponential backoff when no Retry-After header is present
|
||||
- Added `RateLimitConfig` interface with customizable retry behavior
|
||||
- Optional callback for monitoring rate limit events
|
||||
- Maximum wait time capping to prevent excessive delays
|
||||
|
||||
**Improvements:**
|
||||
- Updated test endpoints to use more reliable services (jsonplaceholder, echo.zuplo.io)
|
||||
- Added timeout parameter to test script for better CI/CD compatibility
|
||||
|
||||
**Documentation:**
|
||||
- Added comprehensive rate limiting section to README with examples
|
||||
- Documented all configuration options for handle429Backoff
|
||||
|
||||
## 2025-07-29 - 4.1.0 - feat(client)
|
||||
Add missing options() method to SmartRequest client
|
||||
|
||||
**Features:**
|
||||
- Added `options()` method to SmartRequest class for setting arbitrary request options
|
||||
- Enables setting keepAlive and other platform-specific options via fluent API
|
||||
- Added test coverage for keepAlive functionality
|
||||
|
||||
**Documentation:**
|
||||
- Updated README with examples of using the `options()` method
|
||||
- Added specific examples for enabling keepAlive connections
|
||||
- Corrected all documentation to use `options()` instead of `option()`
|
||||
|
||||
## 2025-07-28 - 4.0.0 - BREAKING CHANGE(core)
|
||||
Complete architectural overhaul with cross-platform support
|
||||
|
||||
**Breaking Changes:**
|
||||
- Renamed `SmartRequestClient` to `SmartRequest` for simpler, cleaner API
|
||||
- Removed legacy API entirely (no more `/legacy` import path)
|
||||
- Major architectural refactoring:
|
||||
- Added abstraction layer with `core_base` containing abstract classes
|
||||
- Split implementations into `core_node` (Node.js) and `core_fetch` (browser)
|
||||
- Dynamic implementation selection based on environment
|
||||
- Response streaming API changes:
|
||||
- `stream()` now always returns web-style `ReadableStream<Uint8Array>`
|
||||
- Added `streamNode()` for Node.js streams (throws error in browser)
|
||||
- Unified type system with single `ICoreRequestOptions` interface
|
||||
- Removed all "Abstract" prefixes from type names
|
||||
|
||||
**Features:**
|
||||
- Full cross-platform support (Node.js and browsers)
|
||||
- Automatic platform detection using @push.rocks/smartenv
|
||||
- Consistent API across platforms with platform-specific capabilities
|
||||
- Web Streams API support in both environments
|
||||
- Better error messages for unsupported platform features
|
||||
|
||||
**Documentation:**
|
||||
- Completely rewritten README with platform-specific examples
|
||||
- Added architecture overview section
|
||||
- Added migration guide from v2.x and v3.x
|
||||
- Updated all examples to use the new `SmartRequest` class name
|
||||
|
||||
## 2025-07-27 - 3.0.0 - BREAKING CHANGE(core)
|
||||
Major architectural refactoring with fetch-like API
|
||||
|
||||
**Breaking Changes:**
|
||||
- Legacy API functions are now imported from `@push.rocks/smartrequest/legacy` instead of the main export
|
||||
- Modern API response objects now use fetch-like methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`) instead of direct `.body` access
|
||||
- Renamed `responseType()` method to `accept()` in modern API
|
||||
- Removed automatic defaults:
|
||||
- No default keepAlive (must be explicitly set)
|
||||
- No default timeouts
|
||||
- No automatic JSON parsing in core
|
||||
- Complete internal architecture refactoring:
|
||||
- Core module now always returns raw streams
|
||||
- Response parsing happens in SmartResponse methods
|
||||
- Legacy API is now just an adapter over the core module
|
||||
|
||||
**Features:**
|
||||
- New fetch-like response API with single-use body consumption
|
||||
- Better TypeScript support and type safety
|
||||
- Cleaner separation of concerns between request and response
|
||||
- More predictable behavior aligned with fetch API standards
|
||||
|
||||
**Documentation:**
|
||||
- Updated all examples to show correct import paths
|
||||
- Added comprehensive examples for the new response API
|
||||
- Enhanced migration guide
|
||||
|
||||
## 2025-04-03 - 2.1.0 - feat(docs)
|
||||
Enhance documentation and tests with modern API usage examples and migration guide
|
||||
|
||||
- Updated README to include detailed examples for the modern fluent API, covering GET, POST, headers, query, timeout, retries, and pagination
|
||||
- Added a migration guide comparing the legacy API and modern API usage
|
||||
- Improved installation instructions with npm, pnpm, and yarn examples
|
||||
- Added and updated test files for both legacy and modern API functionalities
|
||||
- Minor formatting improvements in the code and documentation examples
|
||||
|
||||
## 2024-11-06 - 2.0.23 - fix(core)
|
||||
Enhance type safety for response in binary requests
|
||||
|
||||
- Updated the dependency versions in package.json to their latest versions.
|
||||
- Improved type inference for the response body in getBinary method of smartrequest.binaryrest.ts.
|
||||
- Introduced generic typing to IExtendedIncomingMessage interface for better type safety.
|
||||
|
||||
## 2024-05-29 - 2.0.22 - Documentation
|
||||
update description
|
||||
|
||||
## 2024-04-01 - 2.0.21 - Configuration
|
||||
Updated configuration files
|
||||
|
||||
- Updated `tsconfig`
|
||||
- Updated `npmextra.json`: githost
|
||||
|
||||
## 2023-07-10 - 2.0.15 - Structure
|
||||
Refactored the organization structure
|
||||
|
||||
- Switched to a new organization scheme
|
||||
|
||||
## 2022-07-29 - 1.1.57 to 2.0.0 - Major Update
|
||||
Significant changes and improvements leading to a major version update
|
||||
|
||||
- **BREAKING CHANGE**: Switched the core to use ECMAScript modules (ESM)
|
||||
|
||||
## 2018-08-14 - 1.1.12 to 1.1.13 - Functional Enhancements
|
||||
Enhanced request capabilities and removed unnecessary dependencies
|
||||
|
||||
- Fixed request module to allow sending strings
|
||||
- Removed CI dependencies
|
||||
|
||||
## 2018-07-19 - 1.1.1 to 1.1.11 - Various Fixes and Improvements
|
||||
Improvements and fixes across various components
|
||||
|
||||
- Added formData capability
|
||||
- Corrected path resolution to use current working directory (CWD)
|
||||
- Improved formData handling
|
||||
- Included correct headers
|
||||
- Updated request ending method
|
||||
|
||||
## 2018-06-19 - 1.0.14 - Structural Fix
|
||||
Resolved conflicts with file extensions
|
||||
|
||||
- Changed `.json.ts` to `.jsonrest.ts` to avoid conflicts
|
||||
|
||||
## 2018-06-13 - 1.0.8 to 1.0.10 - Core Updates
|
||||
Ensured binary handling compliance
|
||||
|
||||
- Enhanced core to uphold latest standards
|
||||
- Correct binary file handling response
|
||||
- Fix for handling and returning binary responses
|
||||
|
||||
## 2017-06-09 - 1.0.4 to 1.0.6 - Infrastructure and Type Improvements
|
||||
Types and infrastructure updates
|
||||
|
||||
- Improved types
|
||||
- Removed need for content type on post requests
|
||||
- Updated for new infrastructure
|
||||
|
7
dist/index.d.ts
vendored
7
dist/index.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
import * as interfaces from './smartrequest.interfaces';
|
||||
export { request } from './smartrequest.request';
|
||||
export { ISmartRequestOptions } from './smartrequest.interfaces';
|
||||
export declare let get: (domainArg: string, optionsArg?: interfaces.ISmartRequestOptions) => Promise<Response>;
|
||||
export declare let post: (domainArg: string, optionsArg?: interfaces.ISmartRequestOptions) => Promise<Response>;
|
||||
export declare let put: (domainArg: string, optionsArg?: interfaces.ISmartRequestOptions) => Promise<Response>;
|
||||
export declare let del: (domainArg: string, optionsArg?: interfaces.ISmartRequestOptions) => Promise<Response>;
|
34
dist/index.js
vendored
34
dist/index.js
vendored
@@ -1,34 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const smartrequest_request_1 = require("./smartrequest.request");
|
||||
var smartrequest_request_2 = require("./smartrequest.request");
|
||||
exports.request = smartrequest_request_2.request;
|
||||
exports.get = (domainArg, optionsArg = {}) => __awaiter(this, void 0, void 0, function* () {
|
||||
optionsArg.method = 'GET';
|
||||
let response = yield smartrequest_request_1.request(domainArg, optionsArg);
|
||||
return response;
|
||||
});
|
||||
exports.post = (domainArg, optionsArg = {}) => __awaiter(this, void 0, void 0, function* () {
|
||||
optionsArg.method = 'POST';
|
||||
let response = yield smartrequest_request_1.request(domainArg, optionsArg);
|
||||
return response;
|
||||
});
|
||||
exports.put = (domainArg, optionsArg = {}) => __awaiter(this, void 0, void 0, function* () {
|
||||
optionsArg.method = 'PUT';
|
||||
let response = yield smartrequest_request_1.request(domainArg, optionsArg);
|
||||
return response;
|
||||
});
|
||||
exports.del = (domainArg, optionsArg = {}) => __awaiter(this, void 0, void 0, function* () {
|
||||
optionsArg.method = 'DELETE';
|
||||
let response = yield smartrequest_request_1.request(domainArg, optionsArg);
|
||||
return response;
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7O0FBS0EsaUVBQWdEO0FBRWhELCtEQUFnRDtBQUF2Qyx5Q0FBQSxPQUFPLENBQUE7QUFHTCxRQUFBLEdBQUcsR0FBRyxDQUFPLFNBQWlCLEVBQUUsYUFBOEMsRUFBRTtJQUN6RixVQUFVLENBQUMsTUFBTSxHQUFHLEtBQUssQ0FBQTtJQUN6QixJQUFJLFFBQVEsR0FBRyxNQUFNLDhCQUFPLENBQUMsU0FBUyxFQUFFLFVBQVUsQ0FBQyxDQUFBO0lBQ25ELE1BQU0sQ0FBQyxRQUFRLENBQUE7QUFDakIsQ0FBQyxDQUFBLENBQUE7QUFFVSxRQUFBLElBQUksR0FBRyxDQUFPLFNBQWlCLEVBQUUsYUFBOEMsRUFBRTtJQUMxRixVQUFVLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQTtJQUMxQixJQUFJLFFBQVEsR0FBRyxNQUFNLDhCQUFPLENBQUMsU0FBUyxFQUFFLFVBQVUsQ0FBQyxDQUFBO0lBQ25ELE1BQU0sQ0FBQyxRQUFRLENBQUE7QUFDakIsQ0FBQyxDQUFBLENBQUE7QUFFVSxRQUFBLEdBQUcsR0FBRyxDQUFPLFNBQWlCLEVBQUUsYUFBOEMsRUFBRTtJQUN6RixVQUFVLENBQUMsTUFBTSxHQUFHLEtBQUssQ0FBQTtJQUN6QixJQUFJLFFBQVEsR0FBRyxNQUFNLDhCQUFPLENBQUMsU0FBUyxFQUFFLFVBQVUsQ0FBQyxDQUFBO0lBQ25ELE1BQU0sQ0FBQyxRQUFRLENBQUE7QUFDakIsQ0FBQyxDQUFBLENBQUE7QUFFVSxRQUFBLEdBQUcsR0FBRyxDQUFPLFNBQWlCLEVBQUUsYUFBOEMsRUFBRTtJQUN6RixVQUFVLENBQUMsTUFBTSxHQUFHLFFBQVEsQ0FBQTtJQUM1QixJQUFJLFFBQVEsR0FBRyxNQUFNLDhCQUFPLENBQUMsU0FBUyxFQUFFLFVBQVUsQ0FBQyxDQUFBO0lBQ25ELE1BQU0sQ0FBQyxRQUFRLENBQUE7QUFDakIsQ0FBQyxDQUFBLENBQUEifQ==
|
4
dist/smartrequest.interfaces.d.ts
vendored
4
dist/smartrequest.interfaces.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
import * as https from 'https';
|
||||
export interface ISmartRequestOptions extends https.RequestOptions {
|
||||
requestBody?: any;
|
||||
}
|
3
dist/smartrequest.interfaces.js
vendored
3
dist/smartrequest.interfaces.js
vendored
@@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRyZXF1ZXN0LmludGVyZmFjZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHJlcXVlc3QuaW50ZXJmYWNlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
|
6
dist/smartrequest.plugins.d.ts
vendored
6
dist/smartrequest.plugins.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
import 'typings-global';
|
||||
import * as url from 'url';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as q from 'smartq';
|
||||
export { url, http, https, q };
|
12
dist/smartrequest.plugins.js
vendored
12
dist/smartrequest.plugins.js
vendored
@@ -1,12 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
require("typings-global");
|
||||
const url = require("url");
|
||||
exports.url = url;
|
||||
const http = require("http");
|
||||
exports.http = http;
|
||||
const https = require("https");
|
||||
exports.https = https;
|
||||
const q = require("smartq");
|
||||
exports.q = q;
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRyZXF1ZXN0LnBsdWdpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHJlcXVlc3QucGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLDBCQUF1QjtBQUN2QiwyQkFBMEI7QUFPeEIsa0JBQUc7QUFOTCw2QkFBNEI7QUFPMUIsb0JBQUk7QUFOTiwrQkFBOEI7QUFPNUIsc0JBQUs7QUFMUCw0QkFBMkI7QUFNekIsY0FBQyJ9
|
2
dist/smartrequest.request.d.ts
vendored
2
dist/smartrequest.request.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
import * as interfaces from './smartrequest.interfaces';
|
||||
export declare let request: (domainArg: string, optionsArg?: interfaces.ISmartRequestOptions, streamArg?: boolean) => Promise<Response>;
|
86
dist/smartrequest.request.js
vendored
86
dist/smartrequest.request.js
vendored
@@ -1,86 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const plugins = require("./smartrequest.plugins");
|
||||
let buildResponse = (responseArg) => {
|
||||
let done = plugins.q.defer();
|
||||
// Continuously update stream with data
|
||||
let body = '';
|
||||
responseArg.on('data', function (chunkArg) {
|
||||
body += chunkArg;
|
||||
});
|
||||
responseArg.on('end', function () {
|
||||
try {
|
||||
responseArg.body = JSON.parse(body);
|
||||
}
|
||||
catch (err) {
|
||||
responseArg.body = body;
|
||||
}
|
||||
done.resolve(responseArg);
|
||||
});
|
||||
return done.promise;
|
||||
};
|
||||
exports.request = (domainArg, optionsArg = {}, streamArg = false) => __awaiter(this, void 0, void 0, function* () {
|
||||
let done = plugins.q.defer();
|
||||
let parsedUrl;
|
||||
if (domainArg) {
|
||||
parsedUrl = plugins.url.parse(domainArg);
|
||||
optionsArg.hostname = parsedUrl.hostname;
|
||||
if (parsedUrl.port) {
|
||||
optionsArg.port = parseInt(parsedUrl.port);
|
||||
}
|
||||
optionsArg.path = parsedUrl.path;
|
||||
}
|
||||
if (!parsedUrl || parsedUrl.protocol === 'https:') {
|
||||
let request = plugins.https.request(optionsArg, response => {
|
||||
if (streamArg) {
|
||||
done.resolve(response);
|
||||
}
|
||||
else {
|
||||
buildResponse(response).then(done.resolve);
|
||||
}
|
||||
});
|
||||
if (optionsArg.requestBody) {
|
||||
if (typeof optionsArg.requestBody !== 'string') {
|
||||
optionsArg.requestBody = JSON.stringify(optionsArg.requestBody);
|
||||
}
|
||||
request.write(optionsArg.requestBody);
|
||||
}
|
||||
request.on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
request.end();
|
||||
}
|
||||
else if (parsedUrl.protocol === 'http:') {
|
||||
let request = plugins.http.request(optionsArg, response => {
|
||||
if (streamArg) {
|
||||
done.resolve(response);
|
||||
}
|
||||
else {
|
||||
buildResponse(response).then(done.resolve);
|
||||
}
|
||||
});
|
||||
if (optionsArg.requestBody) {
|
||||
if (typeof optionsArg.requestBody !== 'string') {
|
||||
optionsArg.requestBody = JSON.stringify(optionsArg.requestBody);
|
||||
}
|
||||
request.write(optionsArg.requestBody);
|
||||
}
|
||||
request.on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
request.end();
|
||||
}
|
||||
else {
|
||||
throw new Error(`unsupported protocol: ${parsedUrl.protocol}`);
|
||||
}
|
||||
return done.promise;
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRyZXF1ZXN0LnJlcXVlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHJlcXVlc3QucmVxdWVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7O0FBQ0Esa0RBQWlEO0FBR2pELElBQUksYUFBYSxHQUFHLENBQUMsV0FBVztJQUM5QixJQUFJLElBQUksR0FBRyxPQUFPLENBQUMsQ0FBQyxDQUFDLEtBQUssRUFBRSxDQUFBO0lBQzVCLHVDQUF1QztJQUN2QyxJQUFJLElBQUksR0FBRyxFQUFFLENBQUE7SUFDYixXQUFXLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxVQUFVLFFBQVE7UUFDdkMsSUFBSSxJQUFJLFFBQVEsQ0FBQTtJQUNsQixDQUFDLENBQUMsQ0FBQTtJQUNGLFdBQVcsQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFO1FBQ3BCLElBQUksQ0FBQztZQUNILFdBQVcsQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQTtRQUNyQyxDQUFDO1FBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztZQUNiLFdBQVcsQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFBO1FBQ3pCLENBQUM7UUFDRCxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxDQUFBO0lBQzNCLENBQUMsQ0FBQyxDQUFBO0lBQ0YsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUE7QUFDckIsQ0FBQyxDQUFBO0FBRVUsUUFBQSxPQUFPLEdBQUcsQ0FBTyxTQUFpQixFQUFFLGFBQThDLEVBQUUsRUFBRSxZQUFxQixLQUFLO0lBQ3pILElBQUksSUFBSSxHQUFHLE9BQU8sQ0FBQyxDQUFDLENBQUMsS0FBSyxFQUFPLENBQUE7SUFDakMsSUFBSSxTQUEwQixDQUFBO0lBQzlCLEVBQUUsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUM7UUFDZCxTQUFTLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUE7UUFDeEMsVUFBVSxDQUFDLFFBQVEsR0FBRyxTQUFTLENBQUMsUUFBUSxDQUFBO1FBQ3hDLEVBQUUsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1lBQUMsVUFBVSxDQUFDLElBQUksR0FBRyxRQUFRLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFBO1FBQUMsQ0FBQztRQUNsRSxVQUFVLENBQUMsSUFBSSxHQUFHLFNBQVMsQ0FBQyxJQUFJLENBQUE7SUFDbEMsQ0FBQztJQUNELEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxJQUFJLFNBQVMsQ0FBQyxRQUFRLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQztRQUNsRCxJQUFJLE9BQU8sR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsUUFBUTtZQUN0RCxFQUFFLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO2dCQUNkLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLENBQUE7WUFDeEIsQ0FBQztZQUFDLElBQUksQ0FBQyxDQUFDO2dCQUNOLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFBO1lBQzVDLENBQUM7UUFDSCxDQUFDLENBQUMsQ0FBQTtRQUNGLEVBQUUsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDO1lBQzNCLEVBQUUsQ0FBQyxDQUFDLE9BQU8sVUFBVSxDQUFDLFdBQVcsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDO2dCQUMvQyxVQUFVLENBQUMsV0FBVyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFBO1lBQ2pFLENBQUM7WUFDRCxPQUFPLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQTtRQUN2QyxDQUFDO1FBQ0QsT0FBTyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQ3BCLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUE7UUFDbEIsQ0FBQyxDQUFDLENBQUE7UUFDRixPQUFPLENBQUMsR0FBRyxFQUFFLENBQUE7SUFDZixDQUFDO0lBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxRQUFRLEtBQUssT0FBTyxDQUFDLENBQUMsQ0FBQztRQUMxQyxJQUFJLE9BQU8sR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsUUFBUTtZQUNyRCxFQUFFLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO2dCQUNkLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLENBQUE7WUFDeEIsQ0FBQztZQUFDLElBQUksQ0FBQyxDQUFDO2dCQUNOLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFBO1lBQzVDLENBQUM7UUFDSCxDQUFDLENBQUMsQ0FBQTtRQUNGLEVBQUUsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDO1lBQzNCLEVBQUUsQ0FBQyxDQUFDLE9BQU8sVUFBVSxDQUFDLFdBQVcsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDO2dCQUMvQyxVQUFVLENBQUMsV0FBVyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFBO1lBQ2pFLENBQUM7WUFDRCxPQUFPLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQTtRQUN2QyxDQUFDO1FBQ0QsT0FBTyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQ3BCLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUE7UUFDbEIsQ0FBQyxDQUFDLENBQUE7UUFDRixPQUFPLENBQUMsR0FBRyxFQUFFLENBQUE7SUFDZixDQUFDO0lBQUMsSUFBSSxDQUFDLENBQUM7UUFDTixNQUFNLElBQUksS0FBSyxDQUFDLHlCQUF5QixTQUFTLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQTtJQUNoRSxDQUFDO0lBQ0QsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUE7QUFDckIsQ0FBQyxDQUFBLENBQUEifQ==
|
37
npmextra.json
Normal file
37
npmextra.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"npmts": {
|
||||
"coverageTreshold": 50
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartrequest",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
72
package.json
72
package.json
@@ -1,31 +1,71 @@
|
||||
{
|
||||
"name": "smartrequest",
|
||||
"version": "1.0.5",
|
||||
"description": "dropin replacement for request",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"name": "@push.rocks/smartrequest",
|
||||
"version": "4.2.0",
|
||||
"private": false,
|
||||
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js",
|
||||
"./core_node": "./dist_ts/core_node/index.js",
|
||||
"./core_fetch": "./dist_ts/core_fetch/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(npmts)"
|
||||
"test": "(tstest test/ --verbose --timeout 60)",
|
||||
"build": "(tsbuild --web)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@gitlab.com/pushrocks/smartrequest.git"
|
||||
"url": "https://code.foss.global/push.rocks/smartrequest.git"
|
||||
},
|
||||
"keywords": [
|
||||
"request"
|
||||
"HTTP",
|
||||
"HTTPS",
|
||||
"request library",
|
||||
"form data",
|
||||
"file uploads",
|
||||
"JSON",
|
||||
"binary data",
|
||||
"streams",
|
||||
"keepAlive",
|
||||
"TypeScript",
|
||||
"modern web requests",
|
||||
"drop-in replacement"
|
||||
],
|
||||
"author": "Lossless GmbH",
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/pushrocks/smartrequest/issues"
|
||||
"url": "https://gitlab.com/push.rocks/smartrequest/issues"
|
||||
},
|
||||
"homepage": "https://gitlab.com/pushrocks/smartrequest#README",
|
||||
"homepage": "https://code.foss.global/push.rocks/smartrequest",
|
||||
"dependencies": {
|
||||
"smartq": "^1.1.1",
|
||||
"typings-global": "^1.0.17"
|
||||
"@push.rocks/smartenv": "^5.0.13",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.0.4",
|
||||
"@push.rocks/smarturl": "^3.1.0",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"form-data": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tapbundle": "^1.0.14",
|
||||
"typings-test": "^1.0.3"
|
||||
}
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.2",
|
||||
"@types/node": "^22.9.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
9316
pnpm-lock.yaml
generated
Normal file
9316
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
readme.hints.md
Normal file
79
readme.hints.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# SmartRequest Architecture Hints
|
||||
|
||||
## Core Features
|
||||
- supports http
|
||||
- supports https
|
||||
- supports unix socks
|
||||
- supports formData
|
||||
- supports file uploads
|
||||
- supports best practice keepAlive
|
||||
- dedicated functions for working with JSON request/response cycles
|
||||
- written in TypeScript
|
||||
- continuously updated
|
||||
- uses node native http and https modules
|
||||
- supports both Node.js and browser environments
|
||||
- used in modules like @push.rocks/smartproxy and @api.global/typedrequest
|
||||
|
||||
## Architecture Overview (as of v3.0.0 major refactoring)
|
||||
- The project now has a multi-layer architecture with platform abstraction
|
||||
- Base layer (ts/core_base/) contains abstract classes and unified types
|
||||
- Node.js implementation (ts/core_node/) uses native http/https modules
|
||||
- Fetch implementation (ts/core_fetch/) uses Fetch API for browser compatibility
|
||||
- Core module (ts/core/) dynamically selects the appropriate implementation based on environment
|
||||
- Client API (ts/client/) provides a fluent, chainable interface
|
||||
- Legacy API has been completely removed in v3.0.0
|
||||
|
||||
## Key Components
|
||||
|
||||
### Core Base Module (ts/core_base/)
|
||||
- `request.ts`: Abstract CoreRequest class defining the request interface
|
||||
- `response.ts`: Abstract CoreResponse class with fetch-like API
|
||||
- Defines `stream()` method that always returns web-style ReadableStream
|
||||
- Body can only be consumed once (throws error on second attempt)
|
||||
- `types.ts`: Unified TypeScript interfaces and types
|
||||
- Single `ICoreRequestOptions` interface for all implementations
|
||||
- Implementations handle unsupported options by throwing errors
|
||||
|
||||
### Core Node Module (ts/core_node/)
|
||||
- `request.ts`: Node.js implementation using http/https modules
|
||||
- Supports unix socket connections and keep-alive agents
|
||||
- Converts Node.js specific options from unified interface
|
||||
- `response.ts`: Node.js CoreResponse implementation
|
||||
- `stream()` method converts Node.js stream to web ReadableStream
|
||||
- `streamNode()` method returns native Node.js stream
|
||||
- Methods like `json()`, `text()`, `arrayBuffer()` handle parsing
|
||||
|
||||
### Core Fetch Module (ts/core_fetch/)
|
||||
- `request.ts`: Fetch API implementation for browsers
|
||||
- Throws errors for Node.js specific options (agent, socketPath)
|
||||
- Native support for CORS, credentials, and other browser features
|
||||
- `response.ts`: Fetch-based CoreResponse implementation
|
||||
- `stream()` returns native web ReadableStream from response.body
|
||||
- `streamNode()` throws error explaining it's not available in browser
|
||||
|
||||
### Core Module (ts/core/)
|
||||
- Dynamically loads appropriate implementation based on environment
|
||||
- Uses @push.rocks/smartenv for environment detection
|
||||
- Exports unified types from core_base
|
||||
|
||||
### Client API (ts/client/)
|
||||
- SmartRequest: Fluent API with method chaining
|
||||
- Returns CoreResponse objects with fetch-like methods
|
||||
- Supports pagination, retries, timeouts, and various response types
|
||||
|
||||
### Stream Handling
|
||||
- `stream()` method always returns web-style ReadableStream<Uint8Array>
|
||||
- In Node.js, converts native streams to web streams
|
||||
- `streamNode()` available only in Node.js environment for native streams
|
||||
- Consistent API across platforms while preserving platform-specific capabilities
|
||||
|
||||
### Binary Request Handling
|
||||
- Binary requests handled through ArrayBuffer API
|
||||
- Response body kept as Buffer/ArrayBuffer without string conversion
|
||||
- No automatic transformations applied to binary data
|
||||
|
||||
## Testing
|
||||
- Use `pnpm test` to run all tests
|
||||
- Tests use @git.zone/tstest/tapbundle for assertions
|
||||
- Separate test files for Node.js (test.node.ts) and browser (test.browser.ts)
|
||||
- Browser tests run in headless Chromium via puppeteer
|
597
readme.md
Normal file
597
readme.md
Normal file
@@ -0,0 +1,597 @@
|
||||
# @push.rocks/smartrequest
|
||||
A modern, cross-platform HTTP/HTTPS request library for Node.js and browsers with a unified API, supporting form data, file uploads, JSON, binary data, streams, and unix sockets.
|
||||
|
||||
## Install
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @push.rocks/smartrequest --save
|
||||
|
||||
# Using pnpm
|
||||
pnpm add @push.rocks/smartrequest
|
||||
|
||||
# Using yarn
|
||||
yarn add @push.rocks/smartrequest
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🚀 **Modern Fetch-like API** - Familiar response methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`)
|
||||
- 🌐 **Cross-Platform** - Works in both Node.js and browsers with a unified API
|
||||
- 🔌 **Unix Socket Support** - Connect to local services like Docker (Node.js only)
|
||||
- 📦 **Form Data & File Uploads** - Built-in support for multipart/form-data
|
||||
- 🔁 **Pagination Support** - Multiple strategies (offset, cursor, Link headers)
|
||||
- ⚡ **Keep-Alive Connections** - Efficient connection pooling in Node.js
|
||||
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
|
||||
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
|
||||
- 📡 **Streaming Support** - Handle large files and real-time data
|
||||
- 🔧 **Highly Configurable** - Timeouts, retries, headers, and more
|
||||
|
||||
## Architecture
|
||||
|
||||
SmartRequest v3.0 features a multi-layer architecture that provides consistent behavior across platforms:
|
||||
|
||||
- **Core Base** - Abstract classes and unified types shared across implementations
|
||||
- **Core Node** - Node.js implementation using native http/https modules
|
||||
- **Core Fetch** - Browser implementation using the Fetch API
|
||||
- **Core** - Dynamic implementation selection based on environment
|
||||
- **Client** - High-level fluent API for everyday use
|
||||
|
||||
## Usage
|
||||
|
||||
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for modern applications.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Simple GET request
|
||||
async function fetchUserData(userId: number) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(`https://jsonplaceholder.typicode.com/users/${userId}`)
|
||||
.get();
|
||||
|
||||
// Use the fetch-like response API
|
||||
const userData = await response.json();
|
||||
console.log(userData); // The parsed JSON response
|
||||
}
|
||||
|
||||
// POST request with JSON body
|
||||
async function createPost(title: string, body: string, userId: number) {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts')
|
||||
.json({ title, body, userId })
|
||||
.post();
|
||||
|
||||
const createdPost = await response.json();
|
||||
console.log(createdPost); // The created post
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Core API Usage
|
||||
|
||||
For advanced use cases, you can use the Core API directly:
|
||||
|
||||
```typescript
|
||||
import { CoreRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function directCoreRequest() {
|
||||
const request = new CoreRequest('https://api.example.com/data', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const response = await request.fire();
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Headers and Query Parameters
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function searchRepositories(query: string, perPage: number = 10) {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.github.com/search/repositories')
|
||||
.header('Accept', 'application/vnd.github.v3+json')
|
||||
.query({
|
||||
q: query,
|
||||
per_page: perPage.toString()
|
||||
})
|
||||
.get();
|
||||
|
||||
const data = await response.json();
|
||||
return data.items;
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Timeouts and Retries
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function fetchWithRetry(url: string) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(5000) // 5 seconds timeout
|
||||
.retry(3) // Retry up to 3 times on failure
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Request Options
|
||||
|
||||
Use the `options()` method to set any request options supported by the underlying implementation:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Set various options
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.options({
|
||||
keepAlive: true, // Enable connection reuse (Node.js)
|
||||
timeout: 10000, // 10 second timeout
|
||||
hardDataCuttingTimeout: 15000, // 15 second hard timeout
|
||||
// Platform-specific options are also supported
|
||||
})
|
||||
.get();
|
||||
```
|
||||
|
||||
### Working with Different Response Types
|
||||
|
||||
The API provides a fetch-like interface for handling different response types:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// JSON response (default)
|
||||
async function fetchJson(url: string) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
return await response.json(); // Parses JSON automatically
|
||||
}
|
||||
|
||||
// Text response
|
||||
async function fetchText(url: string) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
return await response.text(); // Returns response as string
|
||||
}
|
||||
|
||||
// Binary data
|
||||
async function downloadImage(url: string) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.accept('binary') // Optional: hints to server we want binary
|
||||
.get();
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
return Buffer.from(buffer); // Convert ArrayBuffer to Buffer if needed
|
||||
}
|
||||
|
||||
// Streaming response (Web Streams API)
|
||||
async function streamLargeFile(url: string) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
// Get a web-style ReadableStream (works in both Node.js and browsers)
|
||||
const stream = response.stream();
|
||||
|
||||
if (stream) {
|
||||
const reader = stream.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
console.log(`Received ${value.length} bytes of data`);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Node.js specific stream (only in Node.js environment)
|
||||
async function streamWithNodeApi(url: string) {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.get();
|
||||
|
||||
// Only available in Node.js, throws error in browser
|
||||
const nodeStream = response.streamNode();
|
||||
|
||||
nodeStream.on('data', (chunk) => {
|
||||
console.log(`Received ${chunk.length} bytes of data`);
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
nodeStream.on('end', resolve);
|
||||
nodeStream.on('error', reject);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Response Object Methods
|
||||
|
||||
The response object provides these methods:
|
||||
|
||||
- `json<T>(): Promise<T>` - Parse response as JSON
|
||||
- `text(): Promise<string>` - Get response as text
|
||||
- `arrayBuffer(): Promise<ArrayBuffer>` - Get response as ArrayBuffer
|
||||
- `stream(): ReadableStream<Uint8Array> | null` - Get web-style ReadableStream (cross-platform)
|
||||
- `streamNode(): NodeJS.ReadableStream` - Get Node.js stream (Node.js only, throws in browser)
|
||||
- `raw(): Response | http.IncomingMessage` - Get the underlying platform response
|
||||
|
||||
Each body method can only be called once per response, similar to the fetch API.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Form Data with File Uploads
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
import * as fs from 'fs';
|
||||
|
||||
async function uploadMultipleFiles(files: Array<{name: string, path: string}>) {
|
||||
const formFields = files.map(file => ({
|
||||
name: 'files',
|
||||
value: fs.readFileSync(file.path),
|
||||
filename: file.name,
|
||||
contentType: 'application/octet-stream'
|
||||
}));
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/upload')
|
||||
.formData(formFields)
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Unix Socket Support (Node.js only)
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Connect to a service via Unix socket
|
||||
async function queryViaUnixSocket() {
|
||||
const response = await SmartRequest.create()
|
||||
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination Support
|
||||
|
||||
The library includes built-in support for various pagination strategies:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Offset-based pagination (page & limit)
|
||||
async function fetchAllUsers() {
|
||||
const client = SmartRequest.create()
|
||||
.url('https://api.example.com/users')
|
||||
.withOffsetPagination({
|
||||
pageParam: 'page',
|
||||
limitParam: 'limit',
|
||||
startPage: 1,
|
||||
pageSize: 20,
|
||||
totalPath: 'meta.total'
|
||||
});
|
||||
|
||||
// Get first page with pagination info
|
||||
const firstPage = await client.getPaginated();
|
||||
console.log(`Found ${firstPage.items.length} users on first page`);
|
||||
console.log(`Has more pages: ${firstPage.hasNextPage}`);
|
||||
|
||||
if (firstPage.hasNextPage) {
|
||||
// Get next page
|
||||
const secondPage = await firstPage.getNextPage();
|
||||
console.log(`Found ${secondPage.items.length} more users`);
|
||||
}
|
||||
|
||||
// Or get all pages at once (use with caution for large datasets)
|
||||
const allUsers = await client.getAllPages();
|
||||
console.log(`Retrieved ${allUsers.length} users in total`);
|
||||
}
|
||||
|
||||
// Cursor-based pagination
|
||||
async function fetchAllPosts() {
|
||||
const allPosts = await SmartRequest.create()
|
||||
.url('https://api.example.com/posts')
|
||||
.withCursorPagination({
|
||||
cursorParam: 'cursor',
|
||||
cursorPath: 'meta.nextCursor',
|
||||
hasMorePath: 'meta.hasMore'
|
||||
})
|
||||
.getAllPages();
|
||||
|
||||
console.log(`Retrieved ${allPosts.length} posts in total`);
|
||||
}
|
||||
|
||||
// Link header-based pagination (GitHub API style)
|
||||
async function fetchAllIssues(repo: string) {
|
||||
const paginatedResponse = await SmartRequest.create()
|
||||
.url(`https://api.github.com/repos/${repo}/issues`)
|
||||
.header('Accept', 'application/vnd.github.v3+json')
|
||||
.withLinkPagination()
|
||||
.getPaginated();
|
||||
|
||||
return paginatedResponse.getAllPages();
|
||||
}
|
||||
```
|
||||
|
||||
### Keep-Alive Connections (Node.js)
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Enable keep-alive for better performance with multiple requests
|
||||
async function performMultipleRequests() {
|
||||
// Note: keepAlive is NOT enabled by default
|
||||
const response1 = await SmartRequest.create()
|
||||
.url('https://api.example.com/endpoint1')
|
||||
.options({ keepAlive: true })
|
||||
.get();
|
||||
|
||||
const response2 = await SmartRequest.create()
|
||||
.url('https://api.example.com/endpoint2')
|
||||
.options({ keepAlive: true })
|
||||
.get();
|
||||
|
||||
// Connections are pooled and reused when keepAlive is enabled
|
||||
return [await response1.json(), await response2.json()];
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting (429 Too Many Requests) Handling
|
||||
|
||||
The library includes built-in support for handling HTTP 429 (Too Many Requests) responses with intelligent backoff:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
// Simple usage - handle 429 with defaults
|
||||
async function fetchWithRateLimitHandling() {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.handle429Backoff() // Automatically retry on 429
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Advanced usage with custom configuration
|
||||
async function fetchWithCustomRateLimiting() {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.handle429Backoff({
|
||||
maxRetries: 5, // Try up to 5 times (default: 3)
|
||||
respectRetryAfter: true, // Honor Retry-After header (default: true)
|
||||
maxWaitTime: 30000, // Max 30 seconds wait (default: 60000)
|
||||
fallbackDelay: 2000, // 2s initial delay if no Retry-After (default: 1000)
|
||||
backoffFactor: 2, // Exponential backoff multiplier (default: 2)
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
console.log(`Rate limited. Attempt ${attempt}, waiting ${waitTime}ms`);
|
||||
}
|
||||
})
|
||||
.get();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Example: API client with rate limit handling
|
||||
class RateLimitedApiClient {
|
||||
private async request(path: string) {
|
||||
return SmartRequest.create()
|
||||
.url(`https://api.example.com${path}`)
|
||||
.handle429Backoff({
|
||||
maxRetries: 3,
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
console.log(`API rate limit hit. Waiting ${waitTime}ms before retry ${attempt}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchData(id: string) {
|
||||
const response = await this.request(`/data/${id}`).get();
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The rate limiting feature:
|
||||
- Automatically detects 429 responses and retries with backoff
|
||||
- Respects the `Retry-After` header when present (supports both seconds and HTTP date formats)
|
||||
- Uses exponential backoff when no `Retry-After` header is provided
|
||||
- Allows custom callbacks for monitoring rate limit events
|
||||
- Caps maximum wait time to prevent excessive delays
|
||||
|
||||
## Platform-Specific Features
|
||||
|
||||
### Browser-Specific Options
|
||||
|
||||
When running in a browser, you can use browser-specific fetch options:
|
||||
|
||||
```typescript
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.options({
|
||||
credentials: 'include', // Include cookies
|
||||
mode: 'cors', // CORS mode
|
||||
cache: 'no-cache', // Cache mode
|
||||
referrerPolicy: 'no-referrer'
|
||||
})
|
||||
.get();
|
||||
```
|
||||
|
||||
### Node.js-Specific Options
|
||||
|
||||
When running in Node.js, you can use Node-specific options:
|
||||
|
||||
```typescript
|
||||
import { Agent } from 'https';
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/data')
|
||||
.options({
|
||||
agent: new Agent({ keepAlive: true }), // Custom agent
|
||||
socketPath: '/var/run/api.sock', // Unix socket
|
||||
})
|
||||
.get();
|
||||
```
|
||||
|
||||
## Complete Example: Building a REST API Client
|
||||
|
||||
Here's a complete example of building a typed API client:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest, type CoreResponse } from '@push.rocks/smartrequest';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
class BlogApiClient {
|
||||
private baseUrl = 'https://jsonplaceholder.typicode.com';
|
||||
|
||||
private async request(path: string) {
|
||||
return SmartRequest.create()
|
||||
.url(`${this.baseUrl}${path}`)
|
||||
.header('Accept', 'application/json');
|
||||
}
|
||||
|
||||
async getUser(id: number): Promise<User> {
|
||||
const response = await this.request(`/users/${id}`).get();
|
||||
return response.json<User>();
|
||||
}
|
||||
|
||||
async createPost(post: Omit<Post, 'id'>): Promise<Post> {
|
||||
const response = await this.request('/posts')
|
||||
.json(post)
|
||||
.post();
|
||||
return response.json<Post>();
|
||||
}
|
||||
|
||||
async deletePost(id: number): Promise<void> {
|
||||
const response = await this.request(`/posts/${id}`).delete();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete post: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllPosts(userId?: number): Promise<Post[]> {
|
||||
const client = this.request('/posts');
|
||||
|
||||
if (userId) {
|
||||
client.query({ userId: userId.toString() });
|
||||
}
|
||||
|
||||
const response = await client.get();
|
||||
return response.json<Post[]>();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const api = new BlogApiClient();
|
||||
const user = await api.getUser(1);
|
||||
const posts = await api.getAllPosts(user.id);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function fetchWithErrorHandling(url: string) {
|
||||
try {
|
||||
const response = await SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(5000)
|
||||
.retry(2)
|
||||
.get();
|
||||
|
||||
// Check if request was successful
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Handle different content types
|
||||
const contentType = response.headers['content-type'];
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
return await response.json();
|
||||
} else if (contentType?.includes('text/')) {
|
||||
return await response.text();
|
||||
} else {
|
||||
return await response.arrayBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
console.error('Connection refused - is the server running?');
|
||||
} else if (error.code === 'ETIMEDOUT') {
|
||||
console.error('Request timed out');
|
||||
} else if (error.name === 'AbortError') {
|
||||
console.error('Request was aborted');
|
||||
} else {
|
||||
console.error('Request failed:', error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migrating from v2.x to v3.x
|
||||
|
||||
Version 3.0 brings significant architectural improvements and a more consistent API:
|
||||
|
||||
1. **Legacy API Removed**: The function-based API (getJson, postJson, etc.) has been removed. Use SmartRequest instead.
|
||||
2. **Unified Response API**: All responses now use the same fetch-like interface regardless of platform.
|
||||
3. **Stream Changes**: The `stream()` method now returns a web-style ReadableStream on all platforms. Use `streamNode()` for Node.js streams.
|
||||
4. **Cross-Platform by Default**: The library now works in browsers out of the box with automatic platform detection.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**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.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This 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.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
105
test/test.browser.ts
Normal file
105
test/test.browser.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
// For browser tests, we need to import from a browser-safe path
|
||||
// that doesn't trigger Node.js module imports
|
||||
import { CoreRequest, CoreResponse } from '../ts/core/index.js';
|
||||
import type { ICoreRequestOptions } from '../ts/core_base/types.js';
|
||||
|
||||
tap.test('browser: should request a JSON document over https', async () => {
|
||||
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts/1');
|
||||
const response = await request.fire();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response).toHaveProperty('status');
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('id');
|
||||
expect(data.id).toEqual(1);
|
||||
expect(data).toHaveProperty('title');
|
||||
});
|
||||
|
||||
tap.test('browser: should handle CORS requests', async () => {
|
||||
const options: ICoreRequestOptions = {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
}
|
||||
};
|
||||
|
||||
const request = new CoreRequest('https://api.github.com/users/github', options);
|
||||
const response = await request.fire();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('login');
|
||||
expect(data.login).toEqual('github');
|
||||
});
|
||||
|
||||
tap.test('browser: should handle request timeouts', async () => {
|
||||
let timedOut = false;
|
||||
|
||||
const options: ICoreRequestOptions = {
|
||||
timeout: 100 // Very short timeout
|
||||
};
|
||||
|
||||
try {
|
||||
// Use a URL that will likely take longer than 100ms
|
||||
const request = new CoreRequest('https://jsonplaceholder.typicode.com/photos', options);
|
||||
await request.fire();
|
||||
} catch (error) {
|
||||
timedOut = true;
|
||||
// Different browsers might have different timeout error messages
|
||||
expect(error.message.toLowerCase()).toMatch(/timeout|timed out|aborted/i);
|
||||
}
|
||||
|
||||
expect(timedOut).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('browser: should handle POST requests with JSON', async () => {
|
||||
const testData = {
|
||||
title: 'foo',
|
||||
body: 'bar',
|
||||
userId: 1
|
||||
};
|
||||
|
||||
const options: ICoreRequestOptions = {
|
||||
method: 'POST',
|
||||
requestBody: testData
|
||||
};
|
||||
|
||||
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts', options);
|
||||
const response = await request.fire();
|
||||
|
||||
expect(response.status).toEqual(201);
|
||||
|
||||
const responseData = await response.json();
|
||||
expect(responseData).toHaveProperty('id');
|
||||
expect(responseData.title).toEqual(testData.title);
|
||||
expect(responseData.body).toEqual(testData.body);
|
||||
expect(responseData.userId).toEqual(testData.userId);
|
||||
});
|
||||
|
||||
tap.test('browser: should handle query parameters', async () => {
|
||||
const options: ICoreRequestOptions = {
|
||||
queryParams: {
|
||||
userId: '2'
|
||||
}
|
||||
};
|
||||
|
||||
const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts', options);
|
||||
const response = await request.fire();
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(Array.isArray(data)).toBeTrue();
|
||||
// Verify we got posts filtered by userId 2
|
||||
if (data.length > 0) {
|
||||
expect(data[0]).toHaveProperty('userId');
|
||||
expect(data[0].userId).toEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
1
test/test.d.ts
vendored
1
test/test.d.ts
vendored
@@ -1 +0,0 @@
|
||||
import 'typings-test';
|
18
test/test.js
18
test/test.js
@@ -1,18 +0,0 @@
|
||||
"use strict";
|
||||
require("typings-test");
|
||||
const smartchai_1 = require("smartchai");
|
||||
const smartrequest = require("../dist/index");
|
||||
describe('smartrequest', function () {
|
||||
it('should request a html document over https', function () {
|
||||
this.timeout(10000);
|
||||
return smartchai_1.expect(smartrequest.get('https://encrypted.google.com/')).to.eventually.property('body').be.a('string');
|
||||
});
|
||||
it('should request a JSON document over https', function () {
|
||||
return smartchai_1.expect(smartrequest.get('https://jsonplaceholder.typicode.com/posts/1')).to.eventually.property('body').property('id').equal(1);
|
||||
});
|
||||
it('should post a JSON document over http', function () {
|
||||
this.timeout(5000);
|
||||
return smartchai_1.expect(smartrequest.post('http://md5.jsontest.com/?text=example_text')).to.eventually.property('body').property('md5').equal('fa4c6baa0812e5b5c80ed8885e55a8a6');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLHdCQUFxQjtBQUVyQix5Q0FBa0M7QUFFbEMsOENBQTZDO0FBRTdDLFFBQVEsQ0FBQyxjQUFjLEVBQUU7SUFDckIsRUFBRSxDQUFDLDJDQUEyQyxFQUFFO1FBQzVDLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUE7UUFDbkIsTUFBTSxDQUFDLGtCQUFNLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQywrQkFBK0IsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUNsSCxDQUFDLENBQUMsQ0FBQTtJQUVGLEVBQUUsQ0FBQywyQ0FBMkMsRUFBRTtRQUM1QyxNQUFNLENBQUMsa0JBQU0sQ0FBQyxZQUFZLENBQUMsR0FBRyxDQUFDLDhDQUE4QyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFBO0lBQzFJLENBQUMsQ0FBQyxDQUFBO0lBRUYsRUFBRSxDQUFDLHVDQUF1QyxFQUFFO1FBQ3hDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7UUFDbEIsTUFBTSxDQUFDLGtCQUFNLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQyxrQ0FBa0MsQ0FBQyxDQUFBO0lBQzNLLENBQUMsQ0FBQyxDQUFBO0FBQ04sQ0FBQyxDQUFDLENBQUEifQ==
|
204
test/test.node.ts
Normal file
204
test/test.node.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import { SmartRequest } from '../ts/client/index.js';
|
||||
|
||||
tap.test('client: should request a html document over https', async () => {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://encrypted.google.com/')
|
||||
.get();
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response).toHaveProperty('status');
|
||||
expect(response.status).toBeGreaterThan(0);
|
||||
const text = await response.text();
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('client: should request a JSON document over https', async () => {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(body.id).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('client: should post a JSON document over http', async () => {
|
||||
const testData = { title: 'example_text', body: 'test body', userId: 1 };
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts')
|
||||
.json(testData)
|
||||
.post();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('title');
|
||||
expect(body.title).toEqual('example_text');
|
||||
expect(body).toHaveProperty('id'); // jsonplaceholder returns an id for created posts
|
||||
});
|
||||
|
||||
tap.test('client: should set headers correctly', async () => {
|
||||
const customHeader = 'X-Custom-Header';
|
||||
const headerValue = 'test-value';
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://echo.zuplo.io/')
|
||||
.header(customHeader, headerValue)
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('headers');
|
||||
|
||||
// Check if the header exists (headers might be lowercase)
|
||||
const headers = body.headers;
|
||||
const headerFound = headers[customHeader] || headers[customHeader.toLowerCase()] || headers['x-custom-header'];
|
||||
expect(headerFound).toEqual(headerValue);
|
||||
});
|
||||
|
||||
tap.test('client: should handle query parameters', async () => {
|
||||
const params = { userId: '1' };
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts')
|
||||
.query(params)
|
||||
.get();
|
||||
|
||||
const body = await response.json();
|
||||
expect(Array.isArray(body)).toBeTrue();
|
||||
// Check that we got posts for userId 1
|
||||
if (body.length > 0) {
|
||||
expect(body[0]).toHaveProperty('userId');
|
||||
expect(body[0].userId).toEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('client: should handle timeout configuration', async () => {
|
||||
// This test just verifies that the timeout method doesn't throw
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.timeout(5000);
|
||||
|
||||
const response = await client.get();
|
||||
expect(response).toHaveProperty('ok');
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('client: should handle retry configuration', async () => {
|
||||
// This test just verifies that the retry method doesn't throw
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.retry(1);
|
||||
|
||||
const response = await client.get();
|
||||
expect(response).toHaveProperty('ok');
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('client: should support keepAlive option for connection reuse', async () => {
|
||||
// Test basic keepAlive functionality
|
||||
const responses = [];
|
||||
|
||||
// Make multiple requests with keepAlive enabled
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.options({ keepAlive: true })
|
||||
.header('X-Request-Number', String(i))
|
||||
.get();
|
||||
|
||||
expect(response.ok).toBeTrue();
|
||||
responses.push(response);
|
||||
}
|
||||
|
||||
// Verify all requests succeeded
|
||||
expect(responses).toHaveLength(3);
|
||||
|
||||
// Also test that keepAlive: false works
|
||||
const responseNoKeepAlive = await SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/2')
|
||||
.options({ keepAlive: false })
|
||||
.get();
|
||||
|
||||
expect(responseNoKeepAlive.ok).toBeTrue();
|
||||
|
||||
// Verify we can parse the responses
|
||||
const data = await responses[0].json();
|
||||
expect(data).toHaveProperty('id');
|
||||
expect(data.id).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('client: should handle 429 rate limiting with default config', async () => {
|
||||
// Test that handle429Backoff can be configured without errors
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.handle429Backoff();
|
||||
|
||||
const response = await client.get();
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
tap.test('client: should handle 429 with custom config', async () => {
|
||||
let rateLimitCallbackCalled = false;
|
||||
let attemptCount = 0;
|
||||
let waitTimeReceived = 0;
|
||||
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.handle429Backoff({
|
||||
maxRetries: 2,
|
||||
fallbackDelay: 500,
|
||||
maxWaitTime: 5000,
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
rateLimitCallbackCalled = true;
|
||||
attemptCount = attempt;
|
||||
waitTimeReceived = waitTime;
|
||||
}
|
||||
});
|
||||
|
||||
const response = await client.get();
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
// The callback should not have been called for a 200 response
|
||||
expect(rateLimitCallbackCalled).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('client: should respect Retry-After header format (seconds)', async () => {
|
||||
// Test the configuration works - actual 429 testing would require a mock server
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.handle429Backoff({
|
||||
maxRetries: 1,
|
||||
respectRetryAfter: true
|
||||
});
|
||||
|
||||
const response = await client.get();
|
||||
expect(response.ok).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('client: should handle rate limiting with exponential backoff', async () => {
|
||||
// Test exponential backoff configuration
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/1')
|
||||
.handle429Backoff({
|
||||
maxRetries: 3,
|
||||
fallbackDelay: 100,
|
||||
backoffFactor: 2,
|
||||
maxWaitTime: 1000
|
||||
});
|
||||
|
||||
const response = await client.get();
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
tap.test('client: should not retry non-429 errors with rate limit handler', async () => {
|
||||
// Test that 404 errors are not retried by rate limit handler
|
||||
const client = SmartRequest.create()
|
||||
.url('https://jsonplaceholder.typicode.com/posts/999999')
|
||||
.handle429Backoff();
|
||||
|
||||
const response = await client.get();
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.ok).toBeFalse();
|
||||
});
|
||||
|
||||
tap.start();
|
25
test/test.ts
25
test/test.ts
@@ -1,25 +0,0 @@
|
||||
import 'typings-test'
|
||||
|
||||
import { tap, expect } from 'tapbundle'
|
||||
|
||||
import * as smartrequest from '../dist/index'
|
||||
|
||||
tap.test('should request a html document over https', async () => {
|
||||
await expect(
|
||||
smartrequest.get('https://encrypted.google.com/')
|
||||
).to.eventually.property('body').be.a('string')
|
||||
})
|
||||
|
||||
tap.test('should request a JSON document over https', async () => {
|
||||
await expect(
|
||||
smartrequest.get('https://jsonplaceholder.typicode.com/posts/1')
|
||||
).to.eventually.property('body').property('id').equal(1)
|
||||
})
|
||||
|
||||
tap.test('should post a JSON document over http', async () => {
|
||||
await expect(
|
||||
smartrequest.post('http://md5.jsontest.com/?text=example_text')
|
||||
).to.eventually.property('body').property('md5').equal('fa4c6baa0812e5b5c80ed8885e55a8a6')
|
||||
})
|
||||
|
||||
tap.start()
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartrequest',
|
||||
version: '2.1.0',
|
||||
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
|
||||
}
|
176
ts/client/features/pagination.ts
Normal file
176
ts/client/features/pagination.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { type CoreResponse } from '../../core/index.js';
|
||||
import type { ICoreResponse } from '../../core_base/types.js';
|
||||
import { type TPaginationConfig, PaginationStrategy, type TPaginatedResponse } from '../types/pagination.js';
|
||||
|
||||
/**
|
||||
* Creates a paginated response from a regular response
|
||||
*/
|
||||
export async function createPaginatedResponse<T>(
|
||||
response: ICoreResponse<any>,
|
||||
paginationConfig: TPaginationConfig,
|
||||
queryParams: Record<string, string>,
|
||||
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
|
||||
): Promise<TPaginatedResponse<T>> {
|
||||
// Parse response body first
|
||||
const body = await response.json() as any;
|
||||
|
||||
// Default to response.body for items if response is JSON
|
||||
let items: T[] = Array.isArray(body)
|
||||
? body
|
||||
: (body?.items || body?.data || body?.results || []);
|
||||
|
||||
let hasNextPage = false;
|
||||
let nextPageParams: Record<string, string> = {};
|
||||
|
||||
// Determine if there's a next page based on pagination strategy
|
||||
switch (paginationConfig.strategy) {
|
||||
case PaginationStrategy.OFFSET: {
|
||||
const config = paginationConfig;
|
||||
const currentPage = parseInt(queryParams[config.pageParam || 'page'] || String(config.startPage || 1));
|
||||
const limit = parseInt(queryParams[config.limitParam || 'limit'] || String(config.pageSize || 20));
|
||||
const total = getValueByPath(body, config.totalPath || 'total') || 0;
|
||||
|
||||
hasNextPage = currentPage * limit < total;
|
||||
|
||||
if (hasNextPage) {
|
||||
nextPageParams = {
|
||||
...queryParams,
|
||||
[config.pageParam || 'page']: String(currentPage + 1)
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PaginationStrategy.CURSOR: {
|
||||
const config = paginationConfig;
|
||||
const nextCursor = getValueByPath(body, config.cursorPath || 'nextCursor');
|
||||
const hasMore = getValueByPath(body, config.hasMorePath || 'hasMore');
|
||||
|
||||
hasNextPage = !!nextCursor || !!hasMore;
|
||||
|
||||
if (hasNextPage && nextCursor) {
|
||||
nextPageParams = {
|
||||
...queryParams,
|
||||
[config.cursorParam || 'cursor']: nextCursor
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PaginationStrategy.LINK_HEADER: {
|
||||
const linkHeader = response.headers['link'] || '';
|
||||
// Handle both string and string[] types for the link header
|
||||
const headerValue = Array.isArray(linkHeader) ? linkHeader[0] : linkHeader;
|
||||
const links = parseLinkHeader(headerValue);
|
||||
|
||||
hasNextPage = !!links.next;
|
||||
|
||||
if (hasNextPage && links.next) {
|
||||
// Extract query parameters from next link URL
|
||||
const url = new URL(links.next);
|
||||
nextPageParams = {};
|
||||
|
||||
url.searchParams.forEach((value, key) => {
|
||||
nextPageParams[key] = value;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PaginationStrategy.CUSTOM: {
|
||||
const config = paginationConfig;
|
||||
hasNextPage = config.hasNextPage(response);
|
||||
|
||||
if (hasNextPage) {
|
||||
nextPageParams = config.getNextPageParams(response, queryParams);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a function to fetch the next page
|
||||
const getNextPage = async (): Promise<TPaginatedResponse<T>> => {
|
||||
if (!hasNextPage) {
|
||||
throw new Error('No more pages available');
|
||||
}
|
||||
|
||||
return fetchNextPage(nextPageParams);
|
||||
};
|
||||
|
||||
// Create a function to fetch all remaining pages
|
||||
const getAllPages = async (): Promise<T[]> => {
|
||||
const allItems = [...items];
|
||||
let currentPage: TPaginatedResponse<T> = { items, hasNextPage, getNextPage, getAllPages, response };
|
||||
|
||||
while (currentPage.hasNextPage) {
|
||||
try {
|
||||
currentPage = await currentPage.getNextPage();
|
||||
allItems.push(...currentPage.items);
|
||||
} catch (error) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasNextPage,
|
||||
getNextPage,
|
||||
getAllPages,
|
||||
response
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Link header for pagination
|
||||
* Link: <https://api.example.com/users?page=2>; rel="next", <https://api.example.com/users?page=5>; rel="last"
|
||||
*/
|
||||
export function parseLinkHeader(header: string): Record<string, string> {
|
||||
const links: Record<string, string> = {};
|
||||
|
||||
if (!header) {
|
||||
return links;
|
||||
}
|
||||
|
||||
// Split parts by comma
|
||||
const parts = header.split(',');
|
||||
|
||||
// Parse each part into a name:value pair
|
||||
for (const part of parts) {
|
||||
const section = part.split(';');
|
||||
if (section.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = section[0].replace(/<(.*)>/, '$1').trim();
|
||||
const name = section[1].replace(/rel="(.*)"/, '$1').trim();
|
||||
|
||||
links[name] = url;
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a nested value from an object using dot notation path
|
||||
* e.g., getValueByPath(obj, "data.pagination.nextCursor")
|
||||
*/
|
||||
export function getValueByPath(obj: any, path?: string): any {
|
||||
if (!path || !obj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
48
ts/client/index.ts
Normal file
48
ts/client/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Export the main client
|
||||
export { SmartRequest } from './smartrequest.js';
|
||||
|
||||
// Export response type from core
|
||||
export { CoreResponse } from '../core/index.js';
|
||||
|
||||
// Export types
|
||||
export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig, RateLimitConfig } from './types/common.js';
|
||||
export {
|
||||
PaginationStrategy,
|
||||
type TPaginationConfig as PaginationConfig,
|
||||
type OffsetPaginationConfig,
|
||||
type CursorPaginationConfig,
|
||||
type LinkPaginationConfig,
|
||||
type CustomPaginationConfig,
|
||||
type TPaginatedResponse as PaginatedResponse
|
||||
} from './types/pagination.js';
|
||||
|
||||
// Convenience factory functions
|
||||
import { SmartRequest } from './smartrequest.js';
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for JSON requests
|
||||
*/
|
||||
export function createJsonClient<T = any>() {
|
||||
return SmartRequest.create<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for form data requests
|
||||
*/
|
||||
export function createFormClient<T = any>() {
|
||||
return SmartRequest.create<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for binary data
|
||||
*/
|
||||
export function createBinaryClient<T = any>() {
|
||||
return SmartRequest.create<T>().accept('binary');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client pre-configured for streaming
|
||||
*/
|
||||
export function createStreamClient() {
|
||||
return SmartRequest.create().accept('stream');
|
||||
}
|
6
ts/client/plugins.ts
Normal file
6
ts/client/plugins.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// plugins for client module
|
||||
import FormData from 'form-data';
|
||||
|
||||
export {
|
||||
FormData as formData
|
||||
};
|
426
ts/client/smartrequest.ts
Normal file
426
ts/client/smartrequest.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import { CoreRequest, CoreResponse } from '../core/index.js';
|
||||
import type { ICoreResponse } from '../core_base/types.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import type { ICoreRequestOptions } from '../core_base/types.js';
|
||||
|
||||
import type { HttpMethod, ResponseType, FormField, RateLimitConfig } from './types/common.js';
|
||||
import {
|
||||
type TPaginationConfig,
|
||||
PaginationStrategy,
|
||||
type OffsetPaginationConfig,
|
||||
type CursorPaginationConfig,
|
||||
type CustomPaginationConfig,
|
||||
type TPaginatedResponse
|
||||
} from './types/pagination.js';
|
||||
import { createPaginatedResponse } from './features/pagination.js';
|
||||
|
||||
/**
|
||||
* Parse Retry-After header value to milliseconds
|
||||
* @param retryAfter - The Retry-After header value (seconds or HTTP date)
|
||||
* @returns Delay in milliseconds
|
||||
*/
|
||||
function parseRetryAfter(retryAfter: string | string[]): number {
|
||||
// Handle array of values (take first)
|
||||
const value = Array.isArray(retryAfter) ? retryAfter[0] : retryAfter;
|
||||
|
||||
if (!value) return 0;
|
||||
|
||||
// Try to parse as seconds (number)
|
||||
const seconds = parseInt(value, 10);
|
||||
if (!isNaN(seconds)) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
||||
// Try to parse as HTTP date
|
||||
const retryDate = new Date(value);
|
||||
if (!isNaN(retryDate.getTime())) {
|
||||
return Math.max(0, retryDate.getTime() - Date.now());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern fluent client for making HTTP requests
|
||||
*/
|
||||
export class SmartRequest<T = any> {
|
||||
private _url: string;
|
||||
private _options: ICoreRequestOptions = {};
|
||||
private _retries: number = 0;
|
||||
private _queryParams: Record<string, string> = {};
|
||||
private _paginationConfig?: TPaginationConfig;
|
||||
private _rateLimitConfig?: RateLimitConfig;
|
||||
|
||||
/**
|
||||
* Create a new SmartRequest instance
|
||||
*/
|
||||
static create<T = any>(): SmartRequest<T> {
|
||||
return new SmartRequest<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL for the request
|
||||
*/
|
||||
url(url: string): this {
|
||||
this._url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP method
|
||||
*/
|
||||
method(method: HttpMethod): this {
|
||||
this._options.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set JSON body for the request
|
||||
*/
|
||||
json(data: any): this {
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
this._options.headers['Content-Type'] = 'application/json';
|
||||
this._options.requestBody = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set form data for the request
|
||||
*/
|
||||
formData(data: FormField[]): this {
|
||||
const form = new plugins.formData();
|
||||
|
||||
for (const item of data) {
|
||||
if (Buffer.isBuffer(item.value)) {
|
||||
form.append(item.name, item.value, {
|
||||
filename: item.filename || 'file',
|
||||
contentType: item.contentType || 'application/octet-stream'
|
||||
});
|
||||
} else {
|
||||
form.append(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
|
||||
this._options.headers = {
|
||||
...this._options.headers,
|
||||
...form.getHeaders()
|
||||
};
|
||||
|
||||
this._options.requestBody = form;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request timeout in milliseconds
|
||||
*/
|
||||
timeout(ms: number): this {
|
||||
this._options.timeout = ms;
|
||||
this._options.hardDataCuttingTimeout = ms;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set number of retry attempts
|
||||
*/
|
||||
retry(count: number): this {
|
||||
this._retries = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable automatic 429 (Too Many Requests) handling with configurable backoff
|
||||
*/
|
||||
handle429Backoff(config?: RateLimitConfig): this {
|
||||
this._rateLimitConfig = {
|
||||
maxRetries: config?.maxRetries ?? 3,
|
||||
respectRetryAfter: config?.respectRetryAfter ?? true,
|
||||
maxWaitTime: config?.maxWaitTime ?? 60000,
|
||||
fallbackDelay: config?.fallbackDelay ?? 1000,
|
||||
backoffFactor: config?.backoffFactor ?? 2,
|
||||
onRateLimit: config?.onRateLimit
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set HTTP headers
|
||||
*/
|
||||
headers(headers: Record<string, string>): this {
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
this._options.headers = {
|
||||
...this._options.headers,
|
||||
...headers
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single HTTP header
|
||||
*/
|
||||
header(name: string, value: string): this {
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
this._options.headers[name] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set query parameters
|
||||
*/
|
||||
query(params: Record<string, string>): this {
|
||||
this._queryParams = {
|
||||
...this._queryParams,
|
||||
...params
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional request options
|
||||
*/
|
||||
options(options: Partial<ICoreRequestOptions>): this {
|
||||
this._options = {
|
||||
...this._options,
|
||||
...options
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Accept header to indicate what content type is expected
|
||||
*/
|
||||
accept(type: ResponseType): this {
|
||||
// Map response types to Accept header values
|
||||
const acceptHeaders: Record<ResponseType, string> = {
|
||||
'json': 'application/json',
|
||||
'text': 'text/plain',
|
||||
'binary': 'application/octet-stream',
|
||||
'stream': '*/*'
|
||||
};
|
||||
|
||||
return this.header('Accept', acceptHeaders[type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure pagination for requests
|
||||
*/
|
||||
pagination(config: TPaginationConfig): this {
|
||||
this._paginationConfig = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure offset-based pagination (page & limit)
|
||||
*/
|
||||
withOffsetPagination(config: Omit<OffsetPaginationConfig, 'strategy'> = {}): this {
|
||||
this._paginationConfig = {
|
||||
strategy: PaginationStrategy.OFFSET,
|
||||
pageParam: config.pageParam || 'page',
|
||||
limitParam: config.limitParam || 'limit',
|
||||
startPage: config.startPage || 1,
|
||||
pageSize: config.pageSize || 20,
|
||||
totalPath: config.totalPath || 'total'
|
||||
};
|
||||
|
||||
// Add initial pagination parameters
|
||||
this.query({
|
||||
[this._paginationConfig.pageParam]: String(this._paginationConfig.startPage),
|
||||
[this._paginationConfig.limitParam]: String(this._paginationConfig.pageSize)
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure cursor-based pagination
|
||||
*/
|
||||
withCursorPagination(config: Omit<CursorPaginationConfig, 'strategy'> = {}): this {
|
||||
this._paginationConfig = {
|
||||
strategy: PaginationStrategy.CURSOR,
|
||||
cursorParam: config.cursorParam || 'cursor',
|
||||
cursorPath: config.cursorPath || 'nextCursor',
|
||||
hasMorePath: config.hasMorePath || 'hasMore'
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Link header-based pagination
|
||||
*/
|
||||
withLinkPagination(): this {
|
||||
this._paginationConfig = {
|
||||
strategy: PaginationStrategy.LINK_HEADER
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure custom pagination
|
||||
*/
|
||||
withCustomPagination(config: Omit<CustomPaginationConfig, 'strategy'>): this {
|
||||
this._paginationConfig = {
|
||||
strategy: PaginationStrategy.CUSTOM,
|
||||
hasNextPage: config.hasNextPage,
|
||||
getNextPageParams: config.getNextPageParams
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
*/
|
||||
async get<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
*/
|
||||
async post<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
*/
|
||||
async put<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('PUT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
*/
|
||||
async delete<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a PATCH request
|
||||
*/
|
||||
async patch<R = T>(): Promise<ICoreResponse<R>> {
|
||||
return this.execute<R>('PATCH');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated response
|
||||
*/
|
||||
async getPaginated<ItemType = T>(): Promise<TPaginatedResponse<ItemType>> {
|
||||
if (!this._paginationConfig) {
|
||||
throw new Error('Pagination not configured. Call one of the pagination methods first.');
|
||||
}
|
||||
|
||||
// Default to GET if no method specified
|
||||
if (!this._options.method) {
|
||||
this._options.method = 'GET';
|
||||
}
|
||||
|
||||
const response = await this.execute();
|
||||
|
||||
return await createPaginatedResponse<ItemType>(
|
||||
response,
|
||||
this._paginationConfig,
|
||||
this._queryParams,
|
||||
(nextPageParams) => {
|
||||
// Create a new client with the same configuration but updated query params
|
||||
const nextClient = new SmartRequest<ItemType>();
|
||||
Object.assign(nextClient, this);
|
||||
nextClient._queryParams = nextPageParams;
|
||||
|
||||
return nextClient.getPaginated<ItemType>();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages at once (use with caution for large datasets)
|
||||
*/
|
||||
async getAllPages<ItemType = T>(): Promise<ItemType[]> {
|
||||
const firstPage = await this.getPaginated<ItemType>();
|
||||
return firstPage.getAllPages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the HTTP request
|
||||
*/
|
||||
private async execute<R = T>(method?: HttpMethod): Promise<ICoreResponse<R>> {
|
||||
if (method) {
|
||||
this._options.method = method;
|
||||
}
|
||||
|
||||
this._options.queryParams = this._queryParams;
|
||||
|
||||
// Track rate limit attempts separately
|
||||
let rateLimitAttempt = 0;
|
||||
let lastError: Error;
|
||||
|
||||
// Main retry loop
|
||||
for (let attempt = 0; attempt <= this._retries; attempt++) {
|
||||
try {
|
||||
const request = new CoreRequest(this._url, this._options as any);
|
||||
const response = await request.fire() as ICoreResponse<R>;
|
||||
|
||||
// Check for 429 status if rate limit handling is enabled
|
||||
if (this._rateLimitConfig && response.status === 429) {
|
||||
if (rateLimitAttempt >= this._rateLimitConfig.maxRetries) {
|
||||
// Max rate limit retries reached, return the 429 response
|
||||
return response;
|
||||
}
|
||||
|
||||
let waitTime: number;
|
||||
|
||||
if (this._rateLimitConfig.respectRetryAfter && response.headers['retry-after']) {
|
||||
// Parse Retry-After header
|
||||
waitTime = parseRetryAfter(response.headers['retry-after']);
|
||||
|
||||
// Cap wait time to maxWaitTime
|
||||
waitTime = Math.min(waitTime, this._rateLimitConfig.maxWaitTime);
|
||||
} else {
|
||||
// Use exponential backoff
|
||||
waitTime = Math.min(
|
||||
this._rateLimitConfig.fallbackDelay * Math.pow(this._rateLimitConfig.backoffFactor, rateLimitAttempt),
|
||||
this._rateLimitConfig.maxWaitTime
|
||||
);
|
||||
}
|
||||
|
||||
// Call rate limit callback if provided
|
||||
if (this._rateLimitConfig.onRateLimit) {
|
||||
this._rateLimitConfig.onRateLimit(rateLimitAttempt + 1, waitTime);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
|
||||
rateLimitAttempt++;
|
||||
// Decrement attempt to retry this attempt
|
||||
attempt--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Success or non-429 error response
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// If this is the last attempt, throw the error
|
||||
if (attempt === this._retries) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// Otherwise, wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// This should never be reached due to the throw in the loop above
|
||||
throw lastError;
|
||||
}
|
||||
}
|
61
ts/client/types/common.ts
Normal file
61
ts/client/types/common.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* HTTP Methods supported by the client
|
||||
*/
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/**
|
||||
* Response types supported by the client
|
||||
*/
|
||||
export type ResponseType = 'json' | 'text' | 'binary' | 'stream';
|
||||
|
||||
/**
|
||||
* Form field data for multipart/form-data requests
|
||||
*/
|
||||
export interface FormField {
|
||||
name: string;
|
||||
value: string | Buffer;
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encoded form field
|
||||
*/
|
||||
export interface UrlEncodedField {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry configuration
|
||||
*/
|
||||
export interface RetryConfig {
|
||||
attempts: number; // Number of retry attempts
|
||||
initialDelay?: number; // Initial delay in ms
|
||||
maxDelay?: number; // Maximum delay in ms
|
||||
factor?: number; // Backoff factor
|
||||
statusCodes?: number[]; // Status codes to retry on
|
||||
shouldRetry?: (error: Error, attemptCount: number) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout configuration
|
||||
*/
|
||||
export interface TimeoutConfig {
|
||||
request?: number; // Overall request timeout in ms
|
||||
connection?: number; // Connection timeout in ms
|
||||
socket?: number; // Socket idle timeout in ms
|
||||
response?: number; // Response timeout in ms
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit configuration for handling 429 responses
|
||||
*/
|
||||
export interface RateLimitConfig {
|
||||
maxRetries?: number; // Maximum number of retries (default: 3)
|
||||
respectRetryAfter?: boolean; // Respect Retry-After header (default: true)
|
||||
maxWaitTime?: number; // Max wait time in ms (default: 60000)
|
||||
fallbackDelay?: number; // Delay when no Retry-After header (default: 1000)
|
||||
backoffFactor?: number; // Exponential backoff factor (default: 2)
|
||||
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
|
||||
}
|
67
ts/client/types/pagination.ts
Normal file
67
ts/client/types/pagination.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type CoreResponse } from '../../core/index.js';
|
||||
import type { ICoreResponse } from '../../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Pagination strategy options
|
||||
*/
|
||||
export enum PaginationStrategy {
|
||||
OFFSET = 'offset', // Uses page & limit parameters
|
||||
CURSOR = 'cursor', // Uses a cursor/token for next page
|
||||
LINK_HEADER = 'link', // Uses Link headers
|
||||
CUSTOM = 'custom' // Uses a custom pagination handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for offset-based pagination
|
||||
*/
|
||||
export interface OffsetPaginationConfig {
|
||||
strategy: PaginationStrategy.OFFSET;
|
||||
pageParam?: string; // Parameter name for page number (default: "page")
|
||||
limitParam?: string; // Parameter name for page size (default: "limit")
|
||||
startPage?: number; // Starting page number (default: 1)
|
||||
pageSize?: number; // Number of items per page (default: 20)
|
||||
totalPath?: string; // JSON path to total item count (default: "total")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for cursor-based pagination
|
||||
*/
|
||||
export interface CursorPaginationConfig {
|
||||
strategy: PaginationStrategy.CURSOR;
|
||||
cursorParam?: string; // Parameter name for cursor (default: "cursor")
|
||||
cursorPath?: string; // JSON path to next cursor (default: "nextCursor")
|
||||
hasMorePath?: string; // JSON path to check if more items exist (default: "hasMore")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Link header-based pagination
|
||||
*/
|
||||
export interface LinkPaginationConfig {
|
||||
strategy: PaginationStrategy.LINK_HEADER;
|
||||
// No additional config needed, uses standard Link header format
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for custom pagination
|
||||
*/
|
||||
export interface CustomPaginationConfig {
|
||||
strategy: PaginationStrategy.CUSTOM;
|
||||
hasNextPage: (response: ICoreResponse<any>) => boolean;
|
||||
getNextPageParams: (response: ICoreResponse<any>, currentParams: Record<string, string>) => Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type of all pagination configurations
|
||||
*/
|
||||
export type TPaginationConfig = OffsetPaginationConfig | CursorPaginationConfig | LinkPaginationConfig | CustomPaginationConfig;
|
||||
|
||||
/**
|
||||
* Interface for a paginated response
|
||||
*/
|
||||
export interface TPaginatedResponse<T> {
|
||||
items: T[]; // Current page items
|
||||
hasNextPage: boolean; // Whether there are more pages
|
||||
getNextPage: () => Promise<TPaginatedResponse<T>>; // Function to get the next page
|
||||
getAllPages: () => Promise<T[]>; // Function to get all remaining pages and combine
|
||||
response: ICoreResponse<any>; // Original response
|
||||
}
|
30
ts/core/index.ts
Normal file
30
ts/core/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// Export all base types - these are the public API
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
const smartenvInstance = new plugins.smartenv.Smartenv();
|
||||
|
||||
// Dynamically load the appropriate implementation
|
||||
let CoreRequest: any;
|
||||
let CoreResponse: any;
|
||||
|
||||
if (smartenvInstance.isNode) {
|
||||
// In Node.js, load the node implementation
|
||||
const modulePath = plugins.smartpath.join(
|
||||
plugins.smartpath.dirname(import.meta.url),
|
||||
'../core_node/index.js'
|
||||
)
|
||||
console.log(modulePath);
|
||||
const impl = await smartenvInstance.getSafeNodeModule(modulePath);
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
} else {
|
||||
// In browser, load the fetch implementation
|
||||
const impl = await import('../core_fetch/index.js');
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
}
|
||||
|
||||
// Export the loaded implementations
|
||||
export { CoreRequest, CoreResponse };
|
4
ts/core/plugins.ts
Normal file
4
ts/core/plugins.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as smartenv from '@push.rocks/smartenv';
|
||||
import * as smartpath from '@push.rocks/smartpath/iso';
|
||||
|
||||
export { smartenv, smartpath };
|
4
ts/core_base/index.ts
Normal file
4
ts/core_base/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Core base exports - abstract classes and platform-agnostic types
|
||||
export * from './types.js';
|
||||
export * from './request.js';
|
||||
export * from './response.js';
|
45
ts/core_base/request.ts
Normal file
45
ts/core_base/request.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as types from './types.js';
|
||||
|
||||
/**
|
||||
* Abstract Core Request class that defines the interface for all HTTP/HTTPS requests
|
||||
*/
|
||||
export abstract class CoreRequest<TOptions extends types.ICoreRequestOptions = types.ICoreRequestOptions, TResponse = any> {
|
||||
/**
|
||||
* Tests if a URL is a unix socket
|
||||
*/
|
||||
static isUnixSocket(url: string): boolean {
|
||||
const unixRegex = /^(http:\/\/|https:\/\/|)unix:/;
|
||||
return unixRegex.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses socket path and route from unix socket URL
|
||||
*/
|
||||
static parseUnixSocketUrl(url: string): { socketPath: string; path: string } {
|
||||
const parseRegex = /(.*):(.*)/;
|
||||
const result = parseRegex.exec(url);
|
||||
return {
|
||||
socketPath: result[1],
|
||||
path: result[2],
|
||||
};
|
||||
}
|
||||
|
||||
protected url: string;
|
||||
protected options: TOptions;
|
||||
|
||||
constructor(url: string, options?: TOptions) {
|
||||
this.url = url;
|
||||
this.options = options || ({} as TOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a response
|
||||
*/
|
||||
abstract fire(): Promise<TResponse>;
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw response (platform-specific)
|
||||
*/
|
||||
abstract fireCore(): Promise<any>;
|
||||
|
||||
}
|
45
ts/core_base/response.ts
Normal file
45
ts/core_base/response.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as types from './types.js';
|
||||
|
||||
/**
|
||||
* Abstract Core Response class that provides a fetch-like API
|
||||
*/
|
||||
export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> {
|
||||
protected consumed = false;
|
||||
|
||||
// Public properties
|
||||
public abstract readonly ok: boolean;
|
||||
public abstract readonly status: number;
|
||||
public abstract readonly statusText: string;
|
||||
public abstract readonly headers: types.Headers;
|
||||
public abstract readonly url: string;
|
||||
|
||||
/**
|
||||
* Ensures the body can only be consumed once
|
||||
*/
|
||||
protected ensureNotConsumed(): void {
|
||||
if (this.consumed) {
|
||||
throw new Error('Body has already been consumed');
|
||||
}
|
||||
this.consumed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
abstract json(): Promise<T>;
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
abstract text(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
abstract arrayBuffer(): Promise<ArrayBuffer>;
|
||||
|
||||
/**
|
||||
* Get response as a web-style ReadableStream
|
||||
*/
|
||||
abstract stream(): ReadableStream<Uint8Array> | null;
|
||||
}
|
81
ts/core_base/types.ts
Normal file
81
ts/core_base/types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* HTTP Methods supported
|
||||
*/
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
/**
|
||||
* Response types supported
|
||||
*/
|
||||
export type ResponseType = 'json' | 'text' | 'binary' | 'stream';
|
||||
|
||||
/**
|
||||
* Form field data for multipart/form-data requests
|
||||
*/
|
||||
export interface IFormField {
|
||||
name: string;
|
||||
value: string | Buffer;
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encoded form field
|
||||
*/
|
||||
export interface IUrlEncodedField {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core request options - unified interface for all implementations
|
||||
*/
|
||||
export interface ICoreRequestOptions {
|
||||
// Common options
|
||||
method?: THttpMethod | string; // Allow string for compatibility
|
||||
headers?: any; // Allow any for platform compatibility
|
||||
keepAlive?: boolean;
|
||||
requestBody?: any;
|
||||
queryParams?: { [key: string]: string };
|
||||
timeout?: number;
|
||||
hardDataCuttingTimeout?: number;
|
||||
|
||||
// Node.js specific options (ignored in fetch implementation)
|
||||
agent?: any;
|
||||
socketPath?: string;
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
path?: string;
|
||||
|
||||
// Fetch API specific options (ignored in Node.js implementation)
|
||||
credentials?: RequestCredentials;
|
||||
mode?: RequestMode;
|
||||
cache?: RequestCache;
|
||||
redirect?: RequestRedirect;
|
||||
referrer?: string;
|
||||
referrerPolicy?: ReferrerPolicy;
|
||||
integrity?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response headers - platform agnostic
|
||||
*/
|
||||
export type Headers = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Core response interface - platform agnostic
|
||||
*/
|
||||
export interface ICoreResponse<T = any> {
|
||||
// Properties
|
||||
ok: boolean;
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Headers;
|
||||
url: string;
|
||||
|
||||
// Methods
|
||||
json(): Promise<T>;
|
||||
text(): Promise<string>;
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream
|
||||
}
|
3
ts/core_fetch/index.ts
Normal file
3
ts/core_fetch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Core fetch exports - native fetch implementation
|
||||
export * from './response.js';
|
||||
export { CoreRequest } from './request.js';
|
131
ts/core_fetch/request.ts
Normal file
131
ts/core_fetch/request.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
|
||||
|
||||
/**
|
||||
* Fetch-based implementation of Core Request class
|
||||
*/
|
||||
export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions, CoreResponse> {
|
||||
constructor(url: string, options: types.ICoreRequestOptions = {}) {
|
||||
super(url, options);
|
||||
|
||||
// Check for unsupported Node.js-specific options
|
||||
if (options.agent || options.socketPath) {
|
||||
throw new Error('Node.js specific options (agent, socketPath) are not supported in browser/fetch implementation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full URL with query parameters
|
||||
*/
|
||||
private buildUrl(): string {
|
||||
if (!this.options.queryParams || Object.keys(this.options.queryParams).length === 0) {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
const url = new URL(this.url);
|
||||
Object.entries(this.options.queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our options to fetch RequestInit
|
||||
*/
|
||||
private buildFetchOptions(): RequestInit {
|
||||
const fetchOptions: RequestInit = {
|
||||
method: this.options.method,
|
||||
headers: this.options.headers,
|
||||
credentials: this.options.credentials,
|
||||
mode: this.options.mode,
|
||||
cache: this.options.cache,
|
||||
redirect: this.options.redirect,
|
||||
referrer: this.options.referrer,
|
||||
referrerPolicy: this.options.referrerPolicy,
|
||||
integrity: this.options.integrity,
|
||||
keepalive: this.options.keepAlive,
|
||||
signal: this.options.signal,
|
||||
};
|
||||
|
||||
// Handle request body
|
||||
if (this.options.requestBody !== undefined) {
|
||||
if (typeof this.options.requestBody === 'string' ||
|
||||
this.options.requestBody instanceof ArrayBuffer ||
|
||||
this.options.requestBody instanceof FormData ||
|
||||
this.options.requestBody instanceof URLSearchParams ||
|
||||
this.options.requestBody instanceof ReadableStream) {
|
||||
fetchOptions.body = this.options.requestBody;
|
||||
} else {
|
||||
// Convert objects to JSON
|
||||
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
||||
// Set content-type if not already set
|
||||
if (!fetchOptions.headers) {
|
||||
fetchOptions.headers = { 'Content-Type': 'application/json' };
|
||||
} else if (fetchOptions.headers instanceof Headers) {
|
||||
if (!fetchOptions.headers.has('Content-Type')) {
|
||||
fetchOptions.headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
} else if (typeof fetchOptions.headers === 'object' && !Array.isArray(fetchOptions.headers)) {
|
||||
const headersObj = fetchOptions.headers as Record<string, string>;
|
||||
if (!headersObj['Content-Type']) {
|
||||
headersObj['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
||||
const timeout = this.options.hardDataCuttingTimeout || this.options.timeout;
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
fetchOptions.signal = controller.signal;
|
||||
}
|
||||
|
||||
return fetchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const response = await this.fireCore();
|
||||
return new CoreResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw Response
|
||||
*/
|
||||
async fireCore(): Promise<Response> {
|
||||
const url = this.buildUrl();
|
||||
const options = this.buildFetchOptions();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.ICoreRequestOptions = {}
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience exports for backward compatibility
|
||||
*/
|
||||
export const isUnixSocket = CoreRequest.isUnixSocket;
|
||||
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;
|
85
ts/core_fetch/response.ts
Normal file
85
ts/core_fetch/response.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
|
||||
|
||||
/**
|
||||
* Fetch-based implementation of Core Response class
|
||||
*/
|
||||
export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements types.IFetchResponse<T> {
|
||||
private response: Response;
|
||||
private responseClone: Response;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly headers: types.Headers;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(response: Response) {
|
||||
super();
|
||||
// Clone the response so we can read the body multiple times if needed
|
||||
this.response = response;
|
||||
this.responseClone = response.clone();
|
||||
|
||||
this.ok = response.ok;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.url = response.url;
|
||||
|
||||
// Convert Headers to plain object
|
||||
this.headers = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
this.headers[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
async json(): Promise<T> {
|
||||
this.ensureNotConsumed();
|
||||
try {
|
||||
return await this.response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a readable stream (Web Streams API)
|
||||
*/
|
||||
stream(): ReadableStream<Uint8Array> | null {
|
||||
this.ensureNotConsumed();
|
||||
return this.response.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js stream method - not available in browser
|
||||
*/
|
||||
streamNode(): never {
|
||||
throw new Error('streamNode() is not available in browser/fetch environment. Use stream() for web-style ReadableStream.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw Response object
|
||||
*/
|
||||
raw(): Response {
|
||||
return this.responseClone;
|
||||
}
|
||||
}
|
15
ts/core_fetch/types.ts
Normal file
15
ts/core_fetch/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as baseTypes from '../core_base/types.js';
|
||||
|
||||
// Re-export base types
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Fetch-specific response extensions
|
||||
*/
|
||||
export interface IFetchResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Node.js stream method that throws in browser
|
||||
streamNode(): never;
|
||||
|
||||
// Access to raw Response object
|
||||
raw(): Response;
|
||||
}
|
3
ts/core_node/index.ts
Normal file
3
ts/core_node/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Core exports
|
||||
export * from './response.js';
|
||||
export { CoreRequest } from './request.js';
|
20
ts/core_node/plugins.ts
Normal file
20
ts/core_node/plugins.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// node native scope
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
|
||||
export { http, https, fs, path };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smarturl from '@push.rocks/smarturl';
|
||||
|
||||
export { smartpromise, smarturl };
|
||||
|
||||
// third party scope
|
||||
import { HttpAgent, HttpsAgent } from 'agentkeepalive';
|
||||
const agentkeepalive = { HttpAgent, HttpsAgent };
|
||||
import formData from 'form-data';
|
||||
|
||||
export { agentkeepalive, formData };
|
163
ts/core_node/request.ts
Normal file
163
ts/core_node/request.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
|
||||
|
||||
// Keep-alive agents for connection pooling
|
||||
const httpAgent = new plugins.agentkeepalive.HttpAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpAgentKeepAliveFalse = new plugins.agentkeepalive.HttpAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: true,
|
||||
maxFreeSockets: 10,
|
||||
maxSockets: 100,
|
||||
maxTotalSockets: 1000,
|
||||
});
|
||||
|
||||
const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
|
||||
keepAlive: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Node.js implementation of Core Request class that handles all HTTP/HTTPS requests
|
||||
*/
|
||||
export class CoreRequest extends AbstractCoreRequest<types.ICoreRequestOptions, CoreResponse> {
|
||||
private requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
options: types.ICoreRequestOptions = {},
|
||||
requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null
|
||||
) {
|
||||
super(url, options);
|
||||
this.requestDataFunc = requestDataFunc;
|
||||
|
||||
// Check for unsupported fetch-specific options
|
||||
if (options.credentials || options.mode || options.cache || options.redirect ||
|
||||
options.referrer || options.referrerPolicy || options.integrity) {
|
||||
throw new Error('Fetch API specific options (credentials, mode, cache, redirect, referrer, referrerPolicy, integrity) are not supported in Node.js implementation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const incomingMessage = await this.fireCore();
|
||||
return new CoreResponse(incomingMessage, this.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw IncomingMessage
|
||||
*/
|
||||
async fireCore(): Promise<plugins.http.IncomingMessage> {
|
||||
const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();
|
||||
|
||||
// Parse URL
|
||||
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.url, {
|
||||
searchParams: this.options.queryParams || {},
|
||||
});
|
||||
|
||||
this.options.hostname = parsedUrl.hostname;
|
||||
if (parsedUrl.port) {
|
||||
this.options.port = parseInt(parsedUrl.port, 10);
|
||||
}
|
||||
this.options.path = parsedUrl.path;
|
||||
|
||||
// Handle unix socket URLs
|
||||
if (CoreRequest.isUnixSocket(this.url)) {
|
||||
const { socketPath, path } = CoreRequest.parseUnixSocketUrl(this.options.path);
|
||||
this.options.socketPath = socketPath;
|
||||
this.options.path = path;
|
||||
}
|
||||
|
||||
// Determine agent based on protocol and keep-alive setting
|
||||
if (!this.options.agent) {
|
||||
// Only use keep-alive agents if explicitly requested
|
||||
if (this.options.keepAlive === true) {
|
||||
this.options.agent = parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent;
|
||||
} else if (this.options.keepAlive === false) {
|
||||
this.options.agent = parsedUrl.protocol === 'https:' ? httpsAgentKeepAliveFalse : httpAgentKeepAliveFalse;
|
||||
}
|
||||
// If keepAlive is undefined, don't set any agent (more fetch-like behavior)
|
||||
}
|
||||
|
||||
// Determine request module
|
||||
const requestModule = parsedUrl.protocol === 'https:' ? plugins.https : plugins.http;
|
||||
|
||||
if (!requestModule) {
|
||||
throw new Error(`The request to ${this.url} is missing a viable protocol. Must be http or https`);
|
||||
}
|
||||
|
||||
// Perform the request
|
||||
const request = requestModule.request(this.options, async (response) => {
|
||||
// Handle hard timeout
|
||||
if (this.options.hardDataCuttingTimeout) {
|
||||
setTimeout(() => {
|
||||
response.destroy();
|
||||
done.reject(new Error('Request timed out'));
|
||||
}, this.options.hardDataCuttingTimeout);
|
||||
}
|
||||
|
||||
// Always return the raw stream
|
||||
done.resolve(response);
|
||||
});
|
||||
|
||||
// Write request body
|
||||
if (this.options.requestBody) {
|
||||
if (this.options.requestBody instanceof plugins.formData) {
|
||||
this.options.requestBody.pipe(request).on('finish', () => {
|
||||
request.end();
|
||||
});
|
||||
} else {
|
||||
// Write body as-is - caller is responsible for serialization
|
||||
const bodyData = typeof this.options.requestBody === 'string'
|
||||
? this.options.requestBody
|
||||
: this.options.requestBody instanceof Buffer
|
||||
? this.options.requestBody
|
||||
: JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility
|
||||
request.write(bodyData);
|
||||
request.end();
|
||||
}
|
||||
} else if (this.requestDataFunc) {
|
||||
this.requestDataFunc(request);
|
||||
} else {
|
||||
request.end();
|
||||
}
|
||||
|
||||
// Handle request errors
|
||||
request.on('error', (e) => {
|
||||
console.error(e);
|
||||
request.destroy();
|
||||
done.reject(e);
|
||||
});
|
||||
|
||||
// Get response and handle response errors
|
||||
const response = await done.promise;
|
||||
response.on('error', (err) => {
|
||||
console.error(err);
|
||||
response.destroy();
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.ICoreRequestOptions = {}
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
}
|
136
ts/core_node/response.ts
Normal file
136
ts/core_node/response.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
|
||||
|
||||
/**
|
||||
* Node.js implementation of Core Response class that provides a fetch-like API
|
||||
*/
|
||||
export class CoreResponse<T = any> extends AbstractCoreResponse<T> implements types.INodeResponse<T> {
|
||||
private incomingMessage: plugins.http.IncomingMessage;
|
||||
private bodyBufferPromise: Promise<Buffer> | null = null;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly headers: plugins.http.IncomingHttpHeaders;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(incomingMessage: plugins.http.IncomingMessage, url: string) {
|
||||
super();
|
||||
this.incomingMessage = incomingMessage;
|
||||
this.url = url;
|
||||
this.status = incomingMessage.statusCode || 0;
|
||||
this.statusText = incomingMessage.statusMessage || '';
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
this.headers = incomingMessage.headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the body as a buffer
|
||||
*/
|
||||
private async collectBody(): Promise<Buffer> {
|
||||
this.ensureNotConsumed();
|
||||
|
||||
if (this.bodyBufferPromise) {
|
||||
return this.bodyBufferPromise;
|
||||
}
|
||||
|
||||
this.bodyBufferPromise = new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
this.incomingMessage.on('data', (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
this.incomingMessage.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
|
||||
this.incomingMessage.on('error', reject);
|
||||
});
|
||||
|
||||
return this.bodyBufferPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
async json(): Promise<T> {
|
||||
const buffer = await this.collectBody();
|
||||
const text = buffer.toString('utf-8');
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
const buffer = await this.collectBody();
|
||||
return buffer.toString('utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
const buffer = await this.collectBody();
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a web-style ReadableStream
|
||||
*/
|
||||
stream(): ReadableStream<Uint8Array> | null {
|
||||
this.ensureNotConsumed();
|
||||
|
||||
// Convert Node.js stream to web stream
|
||||
// In Node.js 16.5+ we can use Readable.toWeb()
|
||||
if (this.incomingMessage.readableEnded || this.incomingMessage.destroyed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a web ReadableStream from the Node.js stream
|
||||
const nodeStream = this.incomingMessage;
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
nodeStream.on('data', (chunk) => {
|
||||
controller.enqueue(new Uint8Array(chunk));
|
||||
});
|
||||
|
||||
nodeStream.on('end', () => {
|
||||
controller.close();
|
||||
});
|
||||
|
||||
nodeStream.on('error', (err) => {
|
||||
controller.error(err);
|
||||
});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
nodeStream.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a Node.js readable stream
|
||||
*/
|
||||
streamNode(): NodeJS.ReadableStream {
|
||||
this.ensureNotConsumed();
|
||||
return this.incomingMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw IncomingMessage (for legacy compatibility)
|
||||
*/
|
||||
raw(): plugins.http.IncomingMessage {
|
||||
return this.incomingMessage;
|
||||
}
|
||||
|
||||
}
|
23
ts/core_node/types.ts
Normal file
23
ts/core_node/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as baseTypes from '../core_base/types.js';
|
||||
|
||||
// Re-export base types
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Extended IncomingMessage with body property (legacy compatibility)
|
||||
*/
|
||||
export interface IExtendedIncomingMessage<T = any> extends plugins.http.IncomingMessage {
|
||||
body: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js specific response extensions
|
||||
*/
|
||||
export interface INodeResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Node.js specific methods
|
||||
streamNode(): NodeJS.ReadableStream; // Returns Node.js style stream
|
||||
|
||||
// Legacy compatibility
|
||||
raw(): plugins.http.IncomingMessage;
|
||||
}
|
39
ts/index.ts
39
ts/index.ts
@@ -1,33 +1,10 @@
|
||||
import * as https from 'https'
|
||||
// Client API exports
|
||||
export * from './client/index.js';
|
||||
|
||||
import * as plugins from './smartrequest.plugins'
|
||||
import * as interfaces from './smartrequest.interfaces'
|
||||
// Core exports for advanced usage
|
||||
export { CoreResponse } from './core/index.js';
|
||||
export type { ICoreRequestOptions, ICoreResponse } from './core_base/types.js';
|
||||
|
||||
import { request } from './smartrequest.request'
|
||||
|
||||
export { request } from './smartrequest.request'
|
||||
export { ISmartRequestOptions } from './smartrequest.interfaces'
|
||||
|
||||
export let get = async (domainArg: string, optionsArg: interfaces.ISmartRequestOptions = {}) => {
|
||||
optionsArg.method = 'GET'
|
||||
let response = await request(domainArg, optionsArg)
|
||||
return response
|
||||
}
|
||||
|
||||
export let post = async (domainArg: string, optionsArg: interfaces.ISmartRequestOptions = {}) => {
|
||||
optionsArg.method = 'POST'
|
||||
let response = await request(domainArg, optionsArg)
|
||||
return response
|
||||
}
|
||||
|
||||
export let put = async (domainArg: string, optionsArg: interfaces.ISmartRequestOptions = {}) => {
|
||||
optionsArg.method = 'PUT'
|
||||
let response = await request(domainArg, optionsArg)
|
||||
return response
|
||||
}
|
||||
|
||||
export let del = async (domainArg: string, optionsArg: interfaces.ISmartRequestOptions = {}) => {
|
||||
optionsArg.method = 'DELETE'
|
||||
let response = await request(domainArg, optionsArg)
|
||||
return response
|
||||
}
|
||||
// Default export for easier importing
|
||||
import { SmartRequest } from './client/smartrequest.js';
|
||||
export default SmartRequest;
|
@@ -1,6 +0,0 @@
|
||||
import * as plugins from './smartrequest.plugins'
|
||||
import * as https from 'https'
|
||||
|
||||
export interface ISmartRequestOptions extends https.RequestOptions {
|
||||
requestBody?: any
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import 'typings-global'
|
||||
import * as url from 'url'
|
||||
import * as http from 'http'
|
||||
import * as https from 'https'
|
||||
|
||||
import * as q from 'smartq'
|
||||
|
||||
export {
|
||||
url,
|
||||
http,
|
||||
https,
|
||||
q
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
import * as https from 'https'
|
||||
import * as plugins from './smartrequest.plugins'
|
||||
import * as interfaces from './smartrequest.interfaces'
|
||||
|
||||
let buildResponse = (responseArg): Promise<any> => {
|
||||
let done = plugins.q.defer()
|
||||
// Continuously update stream with data
|
||||
let body = ''
|
||||
responseArg.on('data', function (chunkArg) {
|
||||
body += chunkArg
|
||||
})
|
||||
responseArg.on('end', function () {
|
||||
try {
|
||||
responseArg.body = JSON.parse(body)
|
||||
} catch (err) {
|
||||
responseArg.body = body
|
||||
}
|
||||
done.resolve(responseArg)
|
||||
})
|
||||
return done.promise
|
||||
}
|
||||
|
||||
export let request = async (domainArg: string, optionsArg: interfaces.ISmartRequestOptions = {}, streamArg: boolean = false): Promise<Response> => {
|
||||
let done = plugins.q.defer<any>()
|
||||
let parsedUrl: plugins.url.Url
|
||||
if (domainArg) {
|
||||
parsedUrl = plugins.url.parse(domainArg)
|
||||
optionsArg.hostname = parsedUrl.hostname
|
||||
if (parsedUrl.port) { optionsArg.port = parseInt(parsedUrl.port) }
|
||||
optionsArg.path = parsedUrl.path
|
||||
}
|
||||
if (!parsedUrl || parsedUrl.protocol === 'https:') {
|
||||
let request = plugins.https.request(optionsArg, response => {
|
||||
if (streamArg) {
|
||||
done.resolve(response)
|
||||
} else {
|
||||
buildResponse(response).then(done.resolve)
|
||||
}
|
||||
})
|
||||
if (optionsArg.requestBody) {
|
||||
if (typeof optionsArg.requestBody !== 'string') {
|
||||
optionsArg.requestBody = JSON.stringify(optionsArg.requestBody)
|
||||
}
|
||||
request.write(optionsArg.requestBody)
|
||||
}
|
||||
request.on('error', (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
request.end()
|
||||
} else if (parsedUrl.protocol === 'http:') {
|
||||
let request = plugins.http.request(optionsArg, response => {
|
||||
if (streamArg) {
|
||||
done.resolve(response)
|
||||
} else {
|
||||
buildResponse(response).then(done.resolve)
|
||||
}
|
||||
})
|
||||
if (optionsArg.requestBody) {
|
||||
if (typeof optionsArg.requestBody !== 'string') {
|
||||
optionsArg.requestBody = JSON.stringify(optionsArg.requestBody)
|
||||
}
|
||||
request.write(optionsArg.requestBody)
|
||||
}
|
||||
request.on('error', (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
request.end()
|
||||
} else {
|
||||
throw new Error(`unsupported protocol: ${parsedUrl.protocol}`)
|
||||
}
|
||||
return done.promise
|
||||
}
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "tslint-config-standard"
|
||||
}
|
310
yarn.lock
310
yarn.lock
@@ -1,310 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/chai-as-promised@0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-0.0.29.tgz#43d52892aa998e185a3de3e2477edb8573be1d77"
|
||||
dependencies:
|
||||
"@types/chai" "*"
|
||||
"@types/promises-a-plus" "*"
|
||||
|
||||
"@types/chai-string@^1.1.30":
|
||||
version "1.1.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai-string/-/chai-string-1.1.30.tgz#4d8744b31a5a2295fc01c981ed1e2d4c8a070f0a"
|
||||
dependencies:
|
||||
"@types/chai" "*"
|
||||
|
||||
"@types/chai@*", "@types/chai@^3.4.35":
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-3.5.2.tgz#c11cd2817d3a401b7ba0f5a420f35c56139b1c1e"
|
||||
|
||||
"@types/mocha@^2.2.31":
|
||||
version "2.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.41.tgz#e27cf0817153eb9f2713b2d3f6c68f1e1c3ca608"
|
||||
|
||||
"@types/node@*", "@types/node@^7.0.29":
|
||||
version "7.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.29.tgz#ccfcec5b7135c7caf6c4ffb8c7f33102340d99df"
|
||||
|
||||
"@types/promises-a-plus@*":
|
||||
version "0.0.27"
|
||||
resolved "https://registry.yarnpkg.com/@types/promises-a-plus/-/promises-a-plus-0.0.27.tgz#c64651134614c84b8f5d7114ce8901d36a609780"
|
||||
|
||||
"@types/shelljs@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.6.0.tgz#090b705c102ce7fc5c0c5ea9b524418ff15840df"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/which@^1.0.28":
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6"
|
||||
|
||||
ansi-256-colors@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-256-colors/-/ansi-256-colors-1.1.0.tgz#910de50efcc7c09e3d82f2f87abd6b700c18818a"
|
||||
|
||||
assertion-error@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
|
||||
|
||||
balanced-match@^0.4.1:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
|
||||
|
||||
beautycolor@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/beautycolor/-/beautycolor-1.0.7.tgz#a4715738ac4c8221371e9cbeb5a6cc6d11ecbf7c"
|
||||
dependencies:
|
||||
ansi-256-colors "^1.1.0"
|
||||
typings-global "^1.0.14"
|
||||
|
||||
bindings@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59"
|
||||
dependencies:
|
||||
balanced-match "^0.4.1"
|
||||
concat-map "0.0.1"
|
||||
|
||||
chai-as-promised@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-6.0.0.tgz#1a02a433a6f24dafac63b9c96fa1684db1aa8da6"
|
||||
dependencies:
|
||||
check-error "^1.0.2"
|
||||
|
||||
chai-string@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chai-string/-/chai-string-1.3.0.tgz#df6139f294391b1035be5606f60a843b3a5041e7"
|
||||
|
||||
chai@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
|
||||
dependencies:
|
||||
assertion-error "^1.0.1"
|
||||
deep-eql "^0.1.3"
|
||||
type-detect "^1.0.0"
|
||||
|
||||
check-error@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
deep-eql@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
|
||||
dependencies:
|
||||
type-detect "0.1.1"
|
||||
|
||||
early@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/early/-/early-2.1.1.tgz#841e23254ea5dc54d8afaeee82f5ab65c00ee23c"
|
||||
dependencies:
|
||||
beautycolor "^1.0.7"
|
||||
smartq "^1.1.1"
|
||||
typings-global "^1.0.16"
|
||||
|
||||
es6-error@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.0.2.tgz#eec5c726eacef51b7f6b73c20db6e1b13b069c98"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
||||
glob@^7.0.0:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
interpret@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
|
||||
leakage@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/leakage/-/leakage-0.3.0.tgz#15d698abdc76bbc6439601f4f3020e77e2d50c39"
|
||||
dependencies:
|
||||
es6-error "^4.0.2"
|
||||
left-pad "^1.1.3"
|
||||
memwatch-next "^0.3.0"
|
||||
minimist "^1.2.0"
|
||||
pretty-bytes "^4.0.2"
|
||||
|
||||
left-pad@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.1.3.tgz#612f61c033f3a9e08e939f1caebeea41b6f3199a"
|
||||
|
||||
memwatch-next@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/memwatch-next/-/memwatch-next-0.3.0.tgz#2111050f9a906e0aa2d72a4ec0f0089c78726f8f"
|
||||
dependencies:
|
||||
bindings "^1.2.1"
|
||||
nan "^2.3.2"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
nan@^2.3.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
|
||||
path-parse@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
|
||||
|
||||
pretty-bytes@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
|
||||
|
||||
rechoir@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
||||
dependencies:
|
||||
resolve "^1.1.6"
|
||||
|
||||
resolve@^1.1.6:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"
|
||||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
semver@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
||||
shelljs@^0.7.6:
|
||||
version "0.7.8"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
|
||||
dependencies:
|
||||
glob "^7.0.0"
|
||||
interpret "^1.0.0"
|
||||
rechoir "^0.6.2"
|
||||
|
||||
smartchai@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/smartchai/-/smartchai-1.0.3.tgz#de6d010bb8b5aef24cb70b31a5f5334e8c41b72f"
|
||||
dependencies:
|
||||
"@types/chai" "^3.4.35"
|
||||
"@types/chai-as-promised" "0.0.29"
|
||||
"@types/chai-string" "^1.1.30"
|
||||
chai "^3.5.0"
|
||||
chai-as-promised "^6.0.0"
|
||||
chai-string "^1.3.0"
|
||||
|
||||
smartdelay@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/smartdelay/-/smartdelay-1.0.3.tgz#5fd44dad77262d110702f0293efa80c072cfb579"
|
||||
dependencies:
|
||||
smartq "^1.1.1"
|
||||
typings-global "^1.0.16"
|
||||
|
||||
smartq@^1.1.0, smartq@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/smartq/-/smartq-1.1.1.tgz#efb358705260d41ae18aef7ffd815f7b6fe17dd3"
|
||||
dependencies:
|
||||
typed-promisify "^0.3.0"
|
||||
typings-global "^1.0.14"
|
||||
|
||||
smartshell@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/smartshell/-/smartshell-1.0.6.tgz#27b1c79029784abe72ac7e91fe698b7ebecc6629"
|
||||
dependencies:
|
||||
"@types/shelljs" "^0.6.0"
|
||||
"@types/which" "^1.0.28"
|
||||
shelljs "^0.7.6"
|
||||
smartq "^1.1.0"
|
||||
which "^1.2.12"
|
||||
|
||||
tapbundle@^1.0.14:
|
||||
version "1.0.14"
|
||||
resolved "https://registry.yarnpkg.com/tapbundle/-/tapbundle-1.0.14.tgz#75827e335fcb02216f0267a26a26d702ddc02e3c"
|
||||
dependencies:
|
||||
early "^2.1.1"
|
||||
leakage "^0.3.0"
|
||||
smartchai "^1.0.3"
|
||||
smartdelay "^1.0.3"
|
||||
smartq "^1.1.1"
|
||||
typings-global "^1.0.16"
|
||||
|
||||
type-detect@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822"
|
||||
|
||||
type-detect@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2"
|
||||
|
||||
typed-promisify@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/typed-promisify/-/typed-promisify-0.3.0.tgz#1ba0af5e444c87d8047406f18ce49092a1191853"
|
||||
|
||||
typings-global@*, typings-global@^1.0.14, typings-global@^1.0.16, typings-global@^1.0.17:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/typings-global/-/typings-global-1.0.17.tgz#41edc331ccec3168289adc8849e1e255efbe7152"
|
||||
dependencies:
|
||||
"@types/node" "^7.0.29"
|
||||
semver "^5.3.0"
|
||||
smartshell "^1.0.6"
|
||||
|
||||
typings-test@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/typings-test/-/typings-test-1.0.3.tgz#fbab895eb3f0c44842e73db059f65946b971e369"
|
||||
dependencies:
|
||||
"@types/mocha" "^2.2.31"
|
||||
typings-global "*"
|
||||
|
||||
which@^1.2.12:
|
||||
version "1.2.14"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
Reference in New Issue
Block a user