├── .editorconfig
├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── browser.yml
│ ├── deno.yml
│ └── node.yml
├── .gitignore
├── .nojekyll
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
├── browserstack-logo.png
└── wretch.svg
├── browserstack.json
├── docs
├── api
│ ├── .nojekyll
│ ├── assets
│ │ ├── highlight.css
│ │ ├── icons.js
│ │ ├── icons.svg
│ │ ├── main.js
│ │ ├── navigation.js
│ │ ├── search.js
│ │ └── style.css
│ ├── functions
│ │ ├── addons_abort.default.html
│ │ ├── addons_perfs.default.html
│ │ ├── addons_progress.default.html
│ │ ├── index.default.html
│ │ ├── middlewares_dedupe.dedupe.html
│ │ ├── middlewares_delay.delay.html
│ │ ├── middlewares_retry.retry.html
│ │ └── middlewares_throttlingCache.throttlingCache.html
│ ├── index.html
│ ├── interfaces
│ │ ├── addons_abort.AbortResolver.html
│ │ ├── addons_abort.AbortWretch.html
│ │ ├── addons_basicAuth.BasicAuthAddon.html
│ │ ├── addons_formData.FormDataAddon.html
│ │ ├── addons_formUrl.FormUrlAddon.html
│ │ ├── addons_perfs.PerfsAddon.html
│ │ ├── addons_progress.ProgressResolver.html
│ │ ├── addons_queryString.QueryStringAddon.html
│ │ ├── index.Wretch.html
│ │ ├── index.WretchError.html
│ │ └── index.WretchResponseChain.html
│ ├── modules
│ │ ├── addons.html
│ │ ├── addons_abort.html
│ │ ├── addons_basicAuth.html
│ │ ├── addons_formData.html
│ │ ├── addons_formUrl.html
│ │ ├── addons_perfs.html
│ │ ├── addons_progress.html
│ │ ├── addons_queryString.html
│ │ ├── index.html
│ │ ├── middlewares.html
│ │ ├── middlewares_dedupe.html
│ │ ├── middlewares_delay.html
│ │ ├── middlewares_retry.html
│ │ └── middlewares_throttlingCache.html
│ ├── types
│ │ ├── addons_perfs.PerfCallback.html
│ │ ├── index.Config.html
│ │ ├── index.ConfiguredMiddleware.html
│ │ ├── index.FetchLike.html
│ │ ├── index.Middleware.html
│ │ ├── index.WretchAddon.html
│ │ ├── index.WretchDeferredCallback.html
│ │ ├── index.WretchErrorCallback.html
│ │ ├── index.WretchOptions.html
│ │ ├── index.WretchResponse.html
│ │ ├── middlewares_dedupe.DedupeKeyFunction.html
│ │ ├── middlewares_dedupe.DedupeMiddleware.html
│ │ ├── middlewares_dedupe.DedupeOptions.html
│ │ ├── middlewares_dedupe.DedupeResolverFunction.html
│ │ ├── middlewares_dedupe.DedupeSkipFunction.html
│ │ ├── middlewares_delay.DelayMiddleware.html
│ │ ├── middlewares_retry.DelayRampFunction.html
│ │ ├── middlewares_retry.OnRetryFunction.html
│ │ ├── middlewares_retry.OnRetryFunctionResponse.html
│ │ ├── middlewares_retry.RetryMiddleware.html
│ │ ├── middlewares_retry.RetryOptions.html
│ │ ├── middlewares_retry.SkipFunction.html
│ │ ├── middlewares_retry.UntilFunction.html
│ │ ├── middlewares_throttlingCache.ThrottlingCacheClearFunction.html
│ │ ├── middlewares_throttlingCache.ThrottlingCacheConditionFunction.html
│ │ ├── middlewares_throttlingCache.ThrottlingCacheInvalidateFunction.html
│ │ ├── middlewares_throttlingCache.ThrottlingCacheKeyFunction.html
│ │ ├── middlewares_throttlingCache.ThrottlingCacheMiddleware.html
│ │ ├── middlewares_throttlingCache.ThrottlingCacheOptions.html
│ │ └── middlewares_throttlingCache.ThrottlingCacheSkipFunction.html
│ └── variables
│ │ ├── addons_basicAuth.default.html
│ │ ├── addons_formData.default.html
│ │ ├── addons_formUrl.default.html
│ │ └── addons_queryString.default.html
├── assets
│ ├── ts-logo.svg
│ └── wretch.svg
├── index.css
└── index.html
├── eslint.config.js
├── karma.conf.cjs
├── package-lock.json
├── package.json
├── rollup.config.js
├── scripts
├── conventional-changelog-wretch
│ ├── index.js
│ ├── package.json
│ └── templates
│ │ ├── commit.hbs
│ │ ├── header.hbs
│ │ └── template.hbs
└── mockServer.js
├── src
├── addons
│ ├── abort.ts
│ ├── basicAuth.ts
│ ├── formData.ts
│ ├── formUrl.ts
│ ├── index.ts
│ ├── perfs.ts
│ ├── progress.ts
│ └── queryString.ts
├── config.ts
├── constants.ts
├── core.ts
├── index.all.ts
├── index.cts
├── index.ts
├── middleware.ts
├── middlewares
│ ├── dedupe.ts
│ ├── delay.ts
│ ├── index.ts
│ ├── retry.ts
│ └── throttlingCache.ts
├── resolver.ts
├── types.ts
└── utils.ts
├── test
├── assets
│ └── duck.jpg
├── browser
│ └── wretch.spec.js
├── deno
│ └── wretch_test.ts
├── mock.js
├── node
│ ├── middlewares
│ │ ├── dedupe.spec.ts
│ │ ├── delay.spec.ts
│ │ ├── mock.ts
│ │ ├── retry.spec.ts
│ │ └── throttling.spec.ts
│ └── wretch.spec.ts
├── resolver.cjs
└── tsconfig.json
├── tsconfig.cjs.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | end_of_line = lf
8 | insert_final_newline = true
9 | charset = utf-8
10 | indent_style = space
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | test/* linguist-vendored
2 | scripts/* linguist-vendored
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: elbywan
2 |
--------------------------------------------------------------------------------
/.github/workflows/browser.yml:
--------------------------------------------------------------------------------
1 | name: Browser test
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | - dev
9 | workflow_dispatch:
10 |
11 | jobs:
12 | browsers-test:
13 | runs-on: macos-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: actions/setup-node@v3
17 | with:
18 | cache: "npm"
19 | - name: Install firefox
20 | run: brew install --cask firefox
21 | - name: Install dependencies
22 | run: npm ci
23 | - name: Build
24 | run: npm run build
25 | - name: Run browser tests
26 | env:
27 | FIREFOX_BIN: /Applications/Firefox.app/Contents/MacOS/firefox
28 | run: npm run test:karma
29 |
--------------------------------------------------------------------------------
/.github/workflows/deno.yml:
--------------------------------------------------------------------------------
1 | name: Deno tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | - dev
9 | workflow_dispatch:
10 |
11 | jobs:
12 | deno-tests:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: actions/setup-node@v3
17 | with:
18 | node-version: ${{ matrix.version }}
19 | cache: "npm"
20 | - uses: denoland/setup-deno@v1
21 | with:
22 | deno-version: v1.x
23 | - name: Install dependencies
24 | run: npm ci
25 | - name: Build
26 | run: npm run build
27 | - name: Unit tests
28 | run: npm run test:deno
29 |
--------------------------------------------------------------------------------
/.github/workflows/node.yml:
--------------------------------------------------------------------------------
1 | name: Node.JS test and lint
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | - dev
9 | workflow_dispatch:
10 |
11 | jobs:
12 | nodejs-test:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | # Remember to update README.md and package.json when updating the version matrix
17 | version: [16, 18, 20, 22]
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.version }}
23 | cache: "npm"
24 | - name: Install dependencies
25 | run: npm ci
26 | - name: Lint
27 | run: npm run lint
28 | - name: Build
29 | run: npm run build
30 | - name: Unit tests
31 | run: npm run test
32 | - name: Coveralls
33 | if: ${{ success() }}
34 | uses: coverallsapp/github-action@master
35 | with:
36 | parallel: true
37 | flag-name: node-${{ matrix.version }}
38 | github-token: ${{ secrets.GITHUB_TOKEN }}
39 | path-to-lcov: "./coverage/lcov.info"
40 |
41 | finish:
42 | needs: nodejs-test
43 | runs-on: ubuntu-latest
44 | steps:
45 | - name: Coveralls Finished
46 | uses: coverallsapp/github-action@master
47 | with:
48 | github-token: ${{ secrets.github_token }}
49 | parallel-finished: true
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # IDE
61 | .vscode
62 |
63 | # Rollup typescript cache
64 | .rpt2_cache
65 |
66 | # Distribution files
67 | /dist
68 | /test/browser/src
69 |
70 | # test files
71 | /test.html
72 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elbywan/wretch/bf1dfe8fe532fadc6b6391fb678a785c3efc9b4b/.nojekyll
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 🎉 Thank you for wishing to collaborate to the wretch library!
4 |
5 | **Ideas, feedback and help are more than welcome.**
6 |
7 | ## How to
8 |
9 | ### 1 - Clone the project
10 |
11 | ```bash
12 | git clone https://github.com/elbywan/wretch
13 | cd wretch
14 | ```
15 |
16 | ### 2 - Install dependencies
17 |
18 | ```bash
19 | npm install
20 | ```
21 |
22 | ### 3 - Change the code as you see fit
23 |
24 | ### 4 - Lint, Build and Test
25 |
26 | ```bash
27 | npm start
28 | # To run browser specs
29 | npm run test:karma # same as the ci
30 | npm run test:karma:watch # in watch mode
31 | ```
32 |
33 | The code **must** pass the linter and specs.
34 |
35 | If you add a new functionality, please write some tests!
36 |
37 | If a linter rule which is not already set in the `.eslintrc.js` file is bothering you, feel free to propose a change.
38 |
39 | ### 5 - Commit & Pull request
40 |
41 | If the modification is related to an existing issue, please mention the number in the commit message. (for instance: `closes #10`)
42 |
43 | Also in order to generate a nice changelog file, please begin your commit message with an emoji corresponding to the change:
44 |
45 | - :fire: `:fire:` -> breaking change
46 | - :bug: `:bug:` -> bug fix
47 | - :factory: `:factory:` -> new feature
48 | - :art: `:art:` -> code improvement
49 | - :checkered_flag: `:checkered_flag:` -> performance update
50 | - :white_check_mark: `:white_check_mark:` -> test improvement
51 | - :memo: `:memo:` -> documentation update
52 | - :arrow_up: `:arrow_up:` -> package update
53 |
54 | Furthermore, starting the actual message content with an upper case and using the present tense and imperative mood would be great.
55 |
56 | And last but not least, always rebase your branch on top of the origin/master branch!
57 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Julien Elbaz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/assets/browserstack-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elbywan/wretch/bf1dfe8fe532fadc6b6391fb678a785c3efc9b4b/assets/browserstack-logo.png
--------------------------------------------------------------------------------
/assets/wretch.svg:
--------------------------------------------------------------------------------
1 | Layer 1
--------------------------------------------------------------------------------
/browserstack.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_path": "test/browser/SpecRunner.html?random=false",
3 | "test_framework": "jasmine2",
4 | "exit_with_fail": true,
5 | "browsers": [
6 | "firefox_latest",
7 | "chrome_latest",
8 | "safari_latest",
9 | "edge_latest"
10 | ]
11 | }
--------------------------------------------------------------------------------
/docs/api/.nojekyll:
--------------------------------------------------------------------------------
1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
--------------------------------------------------------------------------------
/docs/api/assets/highlight.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --light-hl-0: #008000;
3 | --dark-hl-0: #6A9955;
4 | --light-hl-1: #0000FF;
5 | --dark-hl-1: #569CD6;
6 | --light-hl-2: #000000;
7 | --dark-hl-2: #D4D4D4;
8 | --light-hl-3: #0070C1;
9 | --dark-hl-3: #4FC1FF;
10 | --light-hl-4: #795E26;
11 | --dark-hl-4: #DCDCAA;
12 | --light-hl-5: #A31515;
13 | --dark-hl-5: #CE9178;
14 | --light-hl-6: #001080;
15 | --dark-hl-6: #9CDCFE;
16 | --light-hl-7: #098658;
17 | --dark-hl-7: #B5CEA8;
18 | --light-hl-8: #AF00DB;
19 | --dark-hl-8: #C586C0;
20 | --light-hl-9: #000000FF;
21 | --dark-hl-9: #D4D4D4;
22 | --light-hl-10: #267F99;
23 | --dark-hl-10: #4EC9B0;
24 | --light-hl-11: #000000;
25 | --dark-hl-11: #C8C8C8;
26 | --light-code-background: #FFFFFF;
27 | --dark-code-background: #1E1E1E;
28 | }
29 |
30 | @media (prefers-color-scheme: light) { :root {
31 | --hl-0: var(--light-hl-0);
32 | --hl-1: var(--light-hl-1);
33 | --hl-2: var(--light-hl-2);
34 | --hl-3: var(--light-hl-3);
35 | --hl-4: var(--light-hl-4);
36 | --hl-5: var(--light-hl-5);
37 | --hl-6: var(--light-hl-6);
38 | --hl-7: var(--light-hl-7);
39 | --hl-8: var(--light-hl-8);
40 | --hl-9: var(--light-hl-9);
41 | --hl-10: var(--light-hl-10);
42 | --hl-11: var(--light-hl-11);
43 | --code-background: var(--light-code-background);
44 | } }
45 |
46 | @media (prefers-color-scheme: dark) { :root {
47 | --hl-0: var(--dark-hl-0);
48 | --hl-1: var(--dark-hl-1);
49 | --hl-2: var(--dark-hl-2);
50 | --hl-3: var(--dark-hl-3);
51 | --hl-4: var(--dark-hl-4);
52 | --hl-5: var(--dark-hl-5);
53 | --hl-6: var(--dark-hl-6);
54 | --hl-7: var(--dark-hl-7);
55 | --hl-8: var(--dark-hl-8);
56 | --hl-9: var(--dark-hl-9);
57 | --hl-10: var(--dark-hl-10);
58 | --hl-11: var(--dark-hl-11);
59 | --code-background: var(--dark-code-background);
60 | } }
61 |
62 | :root[data-theme='light'] {
63 | --hl-0: var(--light-hl-0);
64 | --hl-1: var(--light-hl-1);
65 | --hl-2: var(--light-hl-2);
66 | --hl-3: var(--light-hl-3);
67 | --hl-4: var(--light-hl-4);
68 | --hl-5: var(--light-hl-5);
69 | --hl-6: var(--light-hl-6);
70 | --hl-7: var(--light-hl-7);
71 | --hl-8: var(--light-hl-8);
72 | --hl-9: var(--light-hl-9);
73 | --hl-10: var(--light-hl-10);
74 | --hl-11: var(--light-hl-11);
75 | --code-background: var(--light-code-background);
76 | }
77 |
78 | :root[data-theme='dark'] {
79 | --hl-0: var(--dark-hl-0);
80 | --hl-1: var(--dark-hl-1);
81 | --hl-2: var(--dark-hl-2);
82 | --hl-3: var(--dark-hl-3);
83 | --hl-4: var(--dark-hl-4);
84 | --hl-5: var(--dark-hl-5);
85 | --hl-6: var(--dark-hl-6);
86 | --hl-7: var(--dark-hl-7);
87 | --hl-8: var(--dark-hl-8);
88 | --hl-9: var(--dark-hl-9);
89 | --hl-10: var(--dark-hl-10);
90 | --hl-11: var(--dark-hl-11);
91 | --code-background: var(--dark-code-background);
92 | }
93 |
94 | .hl-0 { color: var(--hl-0); }
95 | .hl-1 { color: var(--hl-1); }
96 | .hl-2 { color: var(--hl-2); }
97 | .hl-3 { color: var(--hl-3); }
98 | .hl-4 { color: var(--hl-4); }
99 | .hl-5 { color: var(--hl-5); }
100 | .hl-6 { color: var(--hl-6); }
101 | .hl-7 { color: var(--hl-7); }
102 | .hl-8 { color: var(--hl-8); }
103 | .hl-9 { color: var(--hl-9); }
104 | .hl-10 { color: var(--hl-10); }
105 | .hl-11 { color: var(--hl-11); }
106 | pre, code { background: var(--code-background); }
107 |
--------------------------------------------------------------------------------
/docs/api/assets/navigation.js:
--------------------------------------------------------------------------------
1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA6WX227bMAyG38W9zQ5t0w7NXZeuwLAN3doFuyiKQImVRIhiZ7LSNRj67oOt2NGJEjXfJj8/ijRJUY9/M0lfZDbKSJ6XRZUNsi2Rq2yUbcp8x2n1Tv3+diU3PBtka1bk2ehskM1XjOeCFtnosUNcz0oh72lV8mcqgqQTW6q4w9Or4fn74evARP4SVM5XCGAnhHAfScXm1zu5uq5Nw0RHC0FvS7G5IZIgmLY0hJwIjiRqSgj4nYpFhcAZOhAmyqWgVYX71B41BP6xo2L/IAUrloizetQQmNTVgUAaOgg2SygiU/vmNIBd4MvIkEahuELSlUHkFllKR10YdygPDNGSQsjfSWVkq4OnbQoE4k2bf/uMSlZIKhZkbiMNueXg4jI2L4NUJQ4xc7ogO65FvdgVc8nKwsIddCbqcvj65OsdMIedApVHaKC7IR+5pk1S5M9EMDLzntYb/fmZEX3bt2DwrQAVO3DvuKF3VMOiX+AdExv3RPBg2BPB0VG7E80fdM3U9f1DromYiJvhB8bb/IuK1ndvu7Eq3lEbirNWjQnnMzJfH5lyv/XhWqEFfH/14fTiLGlEKCZmRLRTHs7eQYBLILireNLYgm2jftOxw2Ki124jMAGaBpUDeK1yc6Czbbt+7aOT4RbSc8GKnL64WWh+RgUeuAgVJH75KcUnIUp/6eicRhWH3dNqWxYVHa8I838LHWqoQ/BxWSzY0m5phVL/xbpYqXaC5t9YnnP6hwga4pnKGP22juYrWwPI7u8YJ3Y2/IlUgq2W0FGaAMe6oQsqBM2hAatjbS3OQ1NjGLwhxLHvts3cClEPEhyvLd0QsNX0uGEUDjNcN11peG4X7U/UcLmh+W5Lv9D97eEkceSJzwZ6aiitr9ojeMMkTHe+eAR91Ie57cWZnBqPYdjTw5ptk71YRrAHTvaJH8C2CLLvySbt8K4NxL8r7qkUKbXpWiDZbptjfWiWkK9Gn/QNXIsgG98ClhyiJlYkshZ/rkQpJWfFckzmKzrmlKR0V8Qc67Usclab9PDsQSC9fy6eCWc5kfT/3XsZSP9poz5ojPSYVPchW6Q/fC+AhkhPiU0StoZ8TgrJeIIXWw9x8+YSQQA7IUziZI8CKR3EEfVoQnBaHcSRZp4RRNeiR9amStN3/VJbpofr2MS2TngHi/jAP0KARSzCR+7hsW0s4sU2xLnz93bElW4UfwuYlXR8CnjwvpKyngXxLpw2EmRdAkujLwE11DKIB28cFoq9JnsObUUenxvTRoKP3L/SurErrGMSix5cayEPlkEiH3rBRv1gn7Xgegv5sQxQfMRw0eDI0YLtckVOaW/g2obQhjzGtgre3z2K6yl8q3tSbsupJUZ1FG7pdxNj+wpxYhnDPwHSz2GzEs8SehAkH8aFJZ4GuYpEjpGwnCAeC8nu8ZMl9nRIdo2cO6iHRLLzlPkEtr1/mtieg4OgmTBPr0//AFxyWgSYJQAA"
--------------------------------------------------------------------------------
/docs/api/modules/addons_abort.html:
--------------------------------------------------------------------------------
1 |
addons/abort | wretch
5 |
--------------------------------------------------------------------------------
/docs/api/modules/addons_basicAuth.html:
--------------------------------------------------------------------------------
1 | addons/basicAuth | wretch
4 |
--------------------------------------------------------------------------------
/docs/api/modules/addons_formData.html:
--------------------------------------------------------------------------------
1 | addons/formData | wretch
4 |
--------------------------------------------------------------------------------
/docs/api/modules/addons_formUrl.html:
--------------------------------------------------------------------------------
1 | addons/formUrl | wretch
4 |
--------------------------------------------------------------------------------
/docs/api/modules/addons_perfs.html:
--------------------------------------------------------------------------------
1 | addons/perfs | wretch
5 |
--------------------------------------------------------------------------------
/docs/api/modules/addons_progress.html:
--------------------------------------------------------------------------------
1 | addons/progress | wretch
4 |
--------------------------------------------------------------------------------
/docs/api/modules/addons_queryString.html:
--------------------------------------------------------------------------------
1 | addons/queryString | wretch Module addons/queryString
4 |
--------------------------------------------------------------------------------
/docs/api/modules/middlewares_delay.html:
--------------------------------------------------------------------------------
1 | middlewares/delay | wretch
4 |
--------------------------------------------------------------------------------
/docs/api/types/addons_perfs.PerfCallback.html:
--------------------------------------------------------------------------------
1 | PerfCallback | wretch Perf Callback : ( ( timing : any ) => void )
2 |
--------------------------------------------------------------------------------
/docs/api/types/index.ConfiguredMiddleware.html:
--------------------------------------------------------------------------------
1 | ConfiguredMiddleware | wretch Type Alias ConfiguredMiddleware
5 |
--------------------------------------------------------------------------------
/docs/api/types/index.FetchLike.html:
--------------------------------------------------------------------------------
1 | FetchLike | wretch
3 |
--------------------------------------------------------------------------------
/docs/api/types/index.WretchOptions.html:
--------------------------------------------------------------------------------
1 | WretchOptions | wretch Wretch Options : Record < string , any > & RequestInit
3 |
--------------------------------------------------------------------------------
/docs/api/types/index.WretchResponse.html:
--------------------------------------------------------------------------------
1 | WretchResponse | wretch Type Alias WretchResponse Wretch Response : Response & { [key : string ]: any ; }
3 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_dedupe.DedupeKeyFunction.html:
--------------------------------------------------------------------------------
1 | DedupeKeyFunction | wretch Type Alias DedupeKeyFunction Dedupe Key Function : ( ( url : string ,
opts : WretchOptions ) => string )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_dedupe.DedupeResolverFunction.html:
--------------------------------------------------------------------------------
1 | DedupeResolverFunction | wretch Type Alias DedupeResolverFunction Dedupe Resolver Function : ( ( response : Response ) => Response )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_dedupe.DedupeSkipFunction.html:
--------------------------------------------------------------------------------
1 | DedupeSkipFunction | wretch Type Alias DedupeSkipFunction Dedupe Skip Function : ( ( url : string ,
opts : WretchOptions ) => boolean )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_retry.DelayRampFunction.html:
--------------------------------------------------------------------------------
1 | DelayRampFunction | wretch Type Alias DelayRampFunction Delay Ramp Function : ( ( delay : number , nbOfAttempts : number ) => number )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_retry.OnRetryFunctionResponse.html:
--------------------------------------------------------------------------------
1 | OnRetryFunctionResponse | wretch Type Alias OnRetryFunctionResponse On Retry Function Response : { options ?: WretchOptions ; url ?: string ; } | undefined
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_retry.SkipFunction.html:
--------------------------------------------------------------------------------
1 | SkipFunction | wretch
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_retry.UntilFunction.html:
--------------------------------------------------------------------------------
1 | UntilFunction | wretch Until Function : ( ( response ?: Response , error ?: Error ) => boolean | Promise < boolean > )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_throttlingCache.ThrottlingCacheClearFunction.html:
--------------------------------------------------------------------------------
1 | ThrottlingCacheClearFunction | wretch Type Alias ThrottlingCacheClearFunction Throttling Cache Clear Function : ( ( url : string ,
opts : WretchOptions ) => boolean )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_throttlingCache.ThrottlingCacheConditionFunction.html:
--------------------------------------------------------------------------------
1 | ThrottlingCacheConditionFunction | wretch Type Alias ThrottlingCacheConditionFunction Throttling Cache Condition Function : ( ( response : WretchOptions ) => boolean )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_throttlingCache.ThrottlingCacheKeyFunction.html:
--------------------------------------------------------------------------------
1 | ThrottlingCacheKeyFunction | wretch Type Alias ThrottlingCacheKeyFunction Throttling Cache Key Function : ( ( url : string ,
opts : WretchOptions ) => string )
2 |
--------------------------------------------------------------------------------
/docs/api/types/middlewares_throttlingCache.ThrottlingCacheSkipFunction.html:
--------------------------------------------------------------------------------
1 | ThrottlingCacheSkipFunction | wretch Type Alias ThrottlingCacheSkipFunction Throttling Cache Skip Function : ( ( url : string ,
opts : WretchOptions ) => boolean )
2 |
--------------------------------------------------------------------------------
/docs/assets/ts-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | TypeScript logo
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/assets/wretch.svg:
--------------------------------------------------------------------------------
1 | Layer 1
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from "@eslint/js"
4 | import tseslint from "typescript-eslint"
5 | import globals from "globals"
6 |
7 | delete globals.browser["AudioWorkletGlobalScope "]
8 |
9 | export default tseslint.config(
10 | eslint.configs.recommended,
11 | ...tseslint.configs.recommended,
12 | {
13 | rules: {
14 | indent: ["error", 2],
15 | "linebreak-style": ["error", "unix"],
16 | quotes: ["error", "double"],
17 | semi: ["error", "never"],
18 | "no-console": "warn",
19 | "arrow-parens": ["error", "as-needed"],
20 | "no-var": "error",
21 | "prefer-const": "error",
22 | "object-curly-spacing": ["error", "always"],
23 | "@typescript-eslint/no-explicit-any": "off",
24 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
25 | },
26 | languageOptions: {
27 | globals: {
28 | ...globals.browser,
29 | "AudioWorkletGlobalScope": false
30 | }
31 | },
32 | ignores: [ "dist/"],
33 | }
34 | )
35 |
--------------------------------------------------------------------------------
/karma.conf.cjs:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | config.set({
3 | files: [
4 | 'dist/bundle/wretch.all.min.js',
5 | 'test/browser/*.spec.js'
6 | ],
7 | frameworks: ['jasmine'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-firefox-launcher'),
12 | // require('karma-safari-launcher'),
13 | require('karma-safarinative-launcher'),
14 | ],
15 | reporters: ['progress'],
16 | port: 9877,
17 | colors: true,
18 | logLevel: config.LOG_INFO,
19 | browsers: ['ChromeHeadless', 'FirefoxHeadless', 'SafariNative'],
20 | autoWatch: false,
21 | concurrency: 1
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import terser from '@rollup/plugin-terser';
3 | import { nodeResolve } from '@rollup/plugin-node-resolve';
4 |
5 | const addons = ["abort", "basicAuth", "formData", "formUrl", "perfs", "queryString", "progress"]
6 | const middlewares = ["dedupe", "delay", "retry", "throttlingCache"]
7 |
8 | const common = {
9 | plugins: [
10 | typescript({
11 | importHelpers: true
12 | }),
13 | nodeResolve(),
14 | terser({
15 | ecma: 2018,
16 | output: {
17 | comments: false,
18 | },
19 | compress: {
20 | booleans_as_integers: true,
21 | passes: 2
22 | }
23 | })
24 | ],
25 | external: ["url"]
26 | }
27 |
28 | const formats = ["umd", "cjs", "esm"]
29 | const outputs = output => formats.map(format => ({
30 | ...output,
31 | format,
32 | file:
33 | format === "cjs" ? output.file.replace(".js", ".cjs") :
34 | format === "esm" ? output.file.replace(".js", ".mjs") :
35 | output.file
36 | }))
37 |
38 | export default [
39 | {
40 | input: "./src/index.ts",
41 | output: outputs({
42 | file: "dist/bundle/wretch.min.js",
43 | format: "umd",
44 | name: "wretch",
45 | exports: "default",
46 | sourcemap: true
47 | }),
48 | ...common
49 | },
50 | {
51 | input: "./src/index.all.ts",
52 | output: [
53 | ...outputs({
54 | file: "dist/bundle/wretch.all.min.js",
55 | format: "umd",
56 | name: "wretch",
57 | exports: "default",
58 | sourcemap: true
59 | })
60 | ],
61 | ...common
62 | },
63 | ...addons.map(addon => ({
64 | input: `./src/addons/${addon}.ts`,
65 | output: outputs({
66 | file: `dist/bundle/addons/${addon}.min.js`,
67 | format: "umd",
68 | name: `wretch${addon.charAt(0).toLocaleUpperCase() + addon.slice(1)}Addon`,
69 | exports: "default",
70 | sourcemap: true
71 | }),
72 | ...common
73 | })),
74 | ...middlewares.map(middleware => ({
75 | input: `./src/middlewares/${middleware}.ts`,
76 | output: outputs({
77 | file: `dist/bundle/middlewares/${middleware}.min.js`,
78 | format: "umd",
79 | name: `wretch${middleware.charAt(0).toLocaleUpperCase() + middleware.slice(1)}Middleware`,
80 | exports: "named",
81 | sourcemap: true
82 | }),
83 | ...common
84 | }))
85 | ]
86 |
--------------------------------------------------------------------------------
/scripts/conventional-changelog-wretch/index.js:
--------------------------------------------------------------------------------
1 | const resolve = require("path").resolve
2 | const Q = require("q")
3 | const readFile = Q.denodeify(require("fs").readFile)
4 |
5 | const emojiMatch = {
6 | ":fire:": "Breaking change(s)",
7 | ":bug:": "Bug fix(es)",
8 | ":factory:": "New feature(s)",
9 | ":art:": "Code improvement(s)",
10 | ":checkered_flag:": "Performance update(s)",
11 | ":white_check_mark:": "Test improvement(s)",
12 | ":memo:": "Documentation update(s)",
13 | ":arrow_up:": "Version update(s)"
14 | }
15 |
16 | function presetOpts(cb) {
17 |
18 | const parserOpts = {
19 | headerPattern: /^(:.*?:) (.*)$/,
20 | headerCorrespondence: [
21 | "emoji",
22 | "shortDesc"
23 | ]
24 | }
25 |
26 | const writerOpts = {
27 | transform: function (commit) {
28 | if (!commit.emoji || typeof commit.emoji !== "string")
29 | return
30 |
31 | const emojiText = emojiMatch[commit.emoji];
32 | commit.emoji = commit.emoji.substring(0, 72);
33 | const emojiLength = commit.emoji.length;
34 |
35 | if (typeof commit.hash === 'string') {
36 | commit.hash = commit.hash.substring(0, 7);
37 | }
38 |
39 | if (typeof commit.shortDesc === 'string') {
40 | commit.shortDesc = commit.shortDesc.substring(0, 72 - emojiLength);
41 | }
42 |
43 | commit.emoji = commit.emoji + " " + emojiText
44 |
45 | return commit
46 | },
47 | groupBy: "emoji",
48 | commitGroupsSort: "title",
49 | commitsSort: ["emoji", "shortDesc"]
50 | }
51 |
52 | Q.all([
53 | readFile(resolve(__dirname, "templates/template.hbs"), "utf-8"),
54 | readFile(resolve(__dirname, "templates/header.hbs"), "utf-8"),
55 | readFile(resolve(__dirname, "templates/commit.hbs"), "utf-8")
56 | ]).spread(function (template, header, commit) {
57 | writerOpts.mainTemplate = template
58 | writerOpts.headerPartial = header
59 | writerOpts.commitPartial = commit
60 |
61 | cb(null, {
62 | parserOpts: parserOpts,
63 | writerOpts: writerOpts
64 | })
65 | })
66 | }
67 |
68 | module.exports = presetOpts
69 |
--------------------------------------------------------------------------------
/scripts/conventional-changelog-wretch/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "conventional-changelog-wretch",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "conventional-changelog wretch preset",
6 | "main": "index.js",
7 | "files": [
8 | "index.js",
9 | "templates"
10 | ]
11 | }
--------------------------------------------------------------------------------
/scripts/conventional-changelog-wretch/templates/commit.hbs:
--------------------------------------------------------------------------------
1 | * {{#if shortDesc}}{{shortDesc}}{{else}}{{header}}{{/if}}
2 |
3 | {{~!-- commit hash --}} {{#if @root.linkReferences}}([{{hash}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/{{@root.commit}}/{{hash}})){{else}}{{hash~}}{{/if}}
4 |
5 | {{~!-- commit references --}}{{#if references}}, closes{{~#each references}} {{#if @root.linkReferences}}[{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if this.repository}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}{{else}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}{{/if}}/{{@root.issue}}/{{this.issue}}){{else}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}{{/if}}{{/each}}{{/if}}
6 |
--------------------------------------------------------------------------------
/scripts/conventional-changelog-wretch/templates/header.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#if isPatch}}##{{else}}#{{/if}} {{#if @root.linkCompare}}[{{version}}]({{@root.host}}/{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/compare/{{previousTag}}...{{currentTag}}){{else}}{{version}}{{/if}}{{#if title}} "{{title}}"{{/if}}{{#if date}} ({{date}}){{/if}}
3 |
--------------------------------------------------------------------------------
/scripts/conventional-changelog-wretch/templates/template.hbs:
--------------------------------------------------------------------------------
1 | {{> header}}
2 |
3 | {{#each commitGroups}}
4 |
5 | {{#if title}}
6 | ### {{title}}
7 |
8 | {{/if}}
9 | {{#each commits}}
10 | {{> commit root=@root}}
11 | {{/each}}
12 | {{/each}}
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/scripts/mockServer.js:
--------------------------------------------------------------------------------
1 | import { launch } from "../test/mock.js"
2 |
3 | launch(9876)
4 |
--------------------------------------------------------------------------------
/src/addons/abort.ts:
--------------------------------------------------------------------------------
1 | import type { Wretch, WretchAddon, WretchErrorCallback, WretchResponseChain } from "../types.js"
2 |
3 | export interface AbortWretch {
4 | /**
5 | * Associates a custom controller with the request.
6 | *
7 | * Useful when you need to use
8 | * your own AbortController, otherwise wretch will create a new controller itself.
9 | *
10 | * ```js
11 | * const controller = new AbortController()
12 | *
13 | * // Associates the same controller with multiple requests
14 | * wretch("url1")
15 | * .addon(AbortAddon())
16 | * .signal(controller)
17 | * .get()
18 | * .json()
19 | * wretch("url2")
20 | * .addon(AbortAddon())
21 | * .signal(controller)
22 | * .get()
23 | * .json()
24 | *
25 | * // Aborts both requests
26 | * controller.abort()
27 | * ```
28 | *
29 | * @param controller - An instance of AbortController
30 | */
31 | signal: (this: T & Wretch, controller: AbortController) => this
32 | }
33 |
34 | export interface AbortResolver {
35 | /**
36 | * Aborts the request after a fixed time.
37 | *
38 | * If you use a custom AbortController associated with the request, pass it as the second argument.
39 | *
40 | * ```js
41 | * // 1 second timeout
42 | * wretch("...").addon(AbortAddon()).get().setTimeout(1000).json(_ =>
43 | * // will not be called if the request timeouts
44 | * )
45 | * ```
46 | *
47 | * @param time - Time in milliseconds
48 | * @param controller - An instance of AbortController
49 | */
50 | setTimeout: (this: C & WretchResponseChain, time: number, controller?: AbortController) => this
51 | /**
52 | * Returns the provided or generated AbortController plus the wretch response chain as a pair.
53 | *
54 | * ```js
55 | * // We need the controller outside the chain
56 | * const [c, w] = wretch("url")
57 | * .addon(AbortAddon())
58 | * .get()
59 | * .controller()
60 | *
61 | * // Resume with the chain
62 | * w.onAbort(_ => console.log("ouch")).json()
63 | *
64 | * // Later on…
65 | * c.abort()
66 | * ```
67 | */
68 | controller: (this: C & WretchResponseChain) => [any, this]
69 | /**
70 | * Catches an AbortError and performs a callback.
71 | */
72 | onAbort: (this: C & WretchResponseChain, cb: WretchErrorCallback) => this
73 | }
74 |
75 | /**
76 | * Adds the ability to abort requests using AbortController and signals under the hood.
77 | *
78 | *
79 | * _Only compatible with browsers that support
80 | * [AbortControllers](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
81 | * Otherwise, you could use a (partial)
82 | * [polyfill](https://www.npmjs.com/package/abortcontroller-polyfill)._
83 | *
84 | * ```js
85 | * import AbortAddon from "wretch/addons/abort"
86 | *
87 | * const [c, w] = wretch("...")
88 | * .addon(AbortAddon())
89 | * .get()
90 | * .onAbort((_) => console.log("Aborted !"))
91 | * .controller();
92 | *
93 | * w.text((_) => console.log("should never be called"));
94 | * c.abort();
95 | *
96 | * // Or :
97 | *
98 | * const controller = new AbortController();
99 | *
100 | * wretch("...")
101 | * .addon(AbortAddon())
102 | * .signal(controller)
103 | * .get()
104 | * .onAbort((_) => console.log("Aborted !"))
105 | * .text((_) => console.log("should never be called"));
106 | *
107 | * controller.abort();
108 | * ```
109 | */
110 | const abort: () => WretchAddon = () => {
111 | return {
112 | beforeRequest(wretch, options, state) {
113 | const fetchController = wretch._config.polyfill("AbortController", false, true)
114 | if (!options["signal"] && fetchController) {
115 | options["signal"] = fetchController.signal
116 | }
117 | const timeout = {
118 | ref: null,
119 | clear() {
120 | if (timeout.ref) {
121 | clearTimeout(timeout.ref)
122 | timeout.ref = null
123 | }
124 | }
125 | }
126 | state.abort = {
127 | timeout,
128 | fetchController
129 | }
130 | return wretch
131 | },
132 | wretch: {
133 | signal(controller) {
134 | return { ...this, _options: { ...this._options, signal: controller.signal } }
135 | },
136 | },
137 | resolver: {
138 | setTimeout(time, controller = this._sharedState.abort.fetchController) {
139 | const { timeout } = this._sharedState.abort
140 | timeout.clear()
141 | timeout.ref = setTimeout(() => controller.abort(), time)
142 | return this
143 | },
144 | controller() { return [this._sharedState.abort.fetchController, this] },
145 | onAbort(cb) { return this.error("AbortError", cb) }
146 | },
147 | }
148 | }
149 |
150 | export default abort
151 |
--------------------------------------------------------------------------------
/src/addons/basicAuth.ts:
--------------------------------------------------------------------------------
1 | import type { Config, ConfiguredMiddleware, Wretch, WretchAddon } from "../types.js"
2 |
3 | function utf8ToBase64(input: string) {
4 | const utf8Bytes = new TextEncoder().encode(input)
5 | return btoa(String.fromCharCode(...utf8Bytes))
6 | }
7 |
8 | export interface BasicAuthAddon {
9 | /**
10 | * Sets the `Authorization` header to `Basic ` + .
11 | * Additionally, allows using URLs with credentials in them.
12 | *
13 | * ```js
14 | * const user = "user"
15 | * const pass = "pass"
16 | *
17 | * // Automatically sets the Authorization header to "Basic " +
18 | * wretch("...").addon(BasicAuthAddon).basicAuth(user, pass).get()
19 | *
20 | * // Allows using URLs with credentials in them
21 | * wretch(`https://${user}:${pass}@...`).addon(BasicAuthAddon).get()
22 | * ```
23 | *
24 | * @param username - Username to use for basic auth
25 | * @param password - Password to use for basic auth
26 | */
27 | basicAuth(
28 | this: T & Wretch,
29 | username: string,
30 | password: string
31 | ): this
32 | }
33 |
34 | const makeBasicAuthMiddleware: (config: Config) => ConfiguredMiddleware = config => next => (url, opts) => {
35 | const _URL = config.polyfill("URL")
36 | let parsedUrl: URL | null
37 | try {
38 | parsedUrl = new _URL(url)
39 | } catch {
40 | parsedUrl = null
41 | }
42 |
43 | if (parsedUrl?.username || parsedUrl?.password) {
44 | const basicAuthBase64 = utf8ToBase64(
45 | `${decodeURIComponent(parsedUrl.username)}:${decodeURIComponent(parsedUrl.password)}`,
46 | )
47 | opts.headers = {
48 | ...opts.headers,
49 | Authorization: `Basic ${basicAuthBase64}`,
50 | }
51 | parsedUrl.username = ""
52 | parsedUrl.password = ""
53 | url = parsedUrl.toString()
54 | }
55 |
56 | return next(url, opts)
57 | }
58 |
59 |
60 | /**
61 | * Adds the ability to use basic auth with the `Authorization` header.
62 | *
63 | * ```js
64 | * import BasicAuthAddon from "wretch/addons/basicAuth"
65 | *
66 | * wretch().addon(BasicAuthAddon)
67 | * ```
68 | */
69 | const basicAuth: WretchAddon = {
70 | beforeRequest(wretch) {
71 | return wretch.middlewares([makeBasicAuthMiddleware(wretch._config)])
72 | },
73 | wretch: {
74 | basicAuth(username, password) {
75 | const basicAuthBase64 = utf8ToBase64(`${username}:${password}`)
76 | return this.auth(`Basic ${basicAuthBase64}`)
77 | },
78 | },
79 | }
80 |
81 | export default basicAuth
82 |
--------------------------------------------------------------------------------
/src/addons/formData.ts:
--------------------------------------------------------------------------------
1 | import type { Wretch, Config, WretchAddon } from "../types.js"
2 |
3 | function convertFormData(
4 | formObject: object,
5 | recursive: string[] | boolean = false,
6 | config: Config,
7 | formData = config.polyfill("FormData", true, true),
8 | ancestors = [] as string[],
9 | ) {
10 | Object.entries(formObject).forEach(([key, value]) => {
11 | let formKey = ancestors.reduce((acc, ancestor) => (
12 | acc ? `${acc}[${ancestor}]` : ancestor
13 | ), null)
14 | formKey = formKey ? `${formKey}[${key}]` : key
15 | if (value instanceof Array || (globalThis.FileList && value instanceof FileList)) {
16 | for (const item of value as File[])
17 | formData.append(formKey, item)
18 | } else if (
19 | recursive &&
20 | typeof value === "object" &&
21 | (
22 | !(recursive instanceof Array) ||
23 | !recursive.includes(key)
24 | )
25 | ) {
26 | if (value !== null) {
27 | convertFormData(value, recursive, config, formData, [...ancestors, key])
28 | }
29 | } else {
30 | formData.append(formKey, value)
31 | }
32 | })
33 |
34 | return formData
35 | }
36 |
37 | export interface FormDataAddon {
38 | /**
39 | * Converts the javascript object to a FormData and sets the request body.
40 | *
41 | * ```js
42 | * const form = {
43 | * hello: "world",
44 | * duck: "Muscovy",
45 | * };
46 | *
47 | * wretch("...").addons(FormDataAddon).formData(form).post();
48 | * ```
49 | *
50 | * The `recursive` argument when set to `true` will enable recursion through all
51 | * nested objects and produce `object[key]` keys. It can be set to an array of
52 | * string to exclude specific keys.
53 | *
54 | * > Warning: Be careful to exclude `Blob` instances in the Browser, and
55 | * > `ReadableStream` and `Buffer` instances when using the node.js compatible
56 | * > `form-data` package.
57 | *
58 | * ```js
59 | * const form = {
60 | * duck: "Muscovy",
61 | * duckProperties: {
62 | * beak: {
63 | * color: "yellow",
64 | * },
65 | * legs: 2,
66 | * },
67 | * ignored: {
68 | * key: 0,
69 | * },
70 | * };
71 | *
72 | * // Will append the following keys to the FormData payload:
73 | * // "duck", "duckProperties[beak][color]", "duckProperties[legs]"
74 | * wretch("...").addons(FormDataAddon).formData(form, ["ignored"]).post();
75 | * ```
76 | *
77 | * > Note: This addon does not support specifying a custom `filename`.
78 | * > If you need to do so, you can use the `body` method directly:
79 | * > ```js
80 | * > const form = new FormData();
81 | * > form.append("hello", "world", "hello.txt");
82 | * > wretch("...").body(form).post();
83 | * > ```
84 | * > See: https://developer.mozilla.org/en-US/docs/Web/API/FormData/append#example
85 | *
86 | * @param formObject - An object which will be converted to a FormData
87 | * @param recursive - If `true`, will recurse through all nested objects. Can be set as an array of string to exclude specific keys.
88 | */
89 | formData(this: T & Wretch, formObject: object, recursive?: string[] | boolean): this
90 | }
91 |
92 | /**
93 | * Adds the ability to convert a an object to a FormData and use it as a request body.
94 | *
95 | * ```js
96 | * import FormDataAddon from "wretch/addons/formData"
97 | *
98 | * wretch().addon(FormDataAddon)
99 | * ```
100 | */
101 | const formData: WretchAddon = {
102 | wretch: {
103 | formData(formObject, recursive = false) {
104 | return this.body(convertFormData(formObject, recursive, this._config))
105 | }
106 | }
107 | }
108 |
109 | export default formData
110 |
--------------------------------------------------------------------------------
/src/addons/formUrl.ts:
--------------------------------------------------------------------------------
1 | import type { Wretch, WretchAddon } from "../types.js"
2 |
3 | function encodeQueryValue(key: string, value: unknown) {
4 | return encodeURIComponent(key) +
5 | "=" +
6 | encodeURIComponent(
7 | typeof value === "object" ?
8 | JSON.stringify(value) :
9 | "" + value
10 | )
11 | }
12 | function convertFormUrl(formObject: object) {
13 | return Object.keys(formObject)
14 | .map(key => {
15 | const value = formObject[key]
16 | if (value instanceof Array) {
17 | return value.map(v => encodeQueryValue(key, v)).join("&")
18 | }
19 | return encodeQueryValue(key, value)
20 | })
21 | .join("&")
22 | }
23 |
24 | export interface FormUrlAddon {
25 | /**
26 | * Converts the input parameter to an url encoded string and sets the content-type
27 | * header and body. If the input argument is already a string, skips the conversion
28 | * part.
29 | *
30 | * ```js
31 | * const form = { a: 1, b: { c: 2 } };
32 | * const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D";
33 | *
34 | * // Automatically sets the content-type header to "application/x-www-form-urlencoded"
35 | * wretch("...").addon(FormUrlAddon).formUrl(form).post();
36 | * wretch("...").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post();
37 | * ```
38 | *
39 | * @param input - An object to convert into an url encoded string or an already encoded string
40 | */
41 | formUrl(this: T & Wretch, input: (object | string)): this
42 | }
43 |
44 | /**
45 | * Adds the ability to convert a an object to a FormUrl and use it as a request body.
46 | *
47 | * ```js
48 | * import FormUrlAddon from "wretch/addons/formUrl"
49 | *
50 | * wretch().addon(FormUrlAddon)
51 | * ```
52 | */
53 | const formUrl: WretchAddon = {
54 | wretch: {
55 | formUrl(input) {
56 | return this
57 | .body(typeof input === "string" ? input : convertFormUrl(input))
58 | .content("application/x-www-form-urlencoded")
59 | }
60 | }
61 | }
62 |
63 | export default formUrl
64 |
--------------------------------------------------------------------------------
/src/addons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as abortAddon } from "./abort.js"
2 | export type { AbortWretch, AbortResolver } from "./abort.js"
3 | export { default as basicAuthAddon } from "./basicAuth.js"
4 | export type { BasicAuthAddon } from "./basicAuth.js"
5 | export { default as formDataAddon } from "./formData.js"
6 | export type { FormDataAddon } from "./formData.js"
7 | export { default as formUrlAddon } from "./formUrl.js"
8 | export type { FormUrlAddon } from "./formUrl.js"
9 | export { default as perfsAddon } from "./perfs.js"
10 | export type { PerfsAddon } from "./perfs.js"
11 | export { default as queryStringAddon } from "./queryString.js"
12 | export type { QueryStringAddon } from "./queryString.js"
13 | export { default as progressAddon } from "./progress.js"
14 | export type { ProgressResolver } from "./progress.js"
15 |
--------------------------------------------------------------------------------
/src/addons/perfs.ts:
--------------------------------------------------------------------------------
1 | import type { WretchResponseChain, WretchAddon, Config } from "../types.js"
2 |
3 | export type PerfCallback = (timing: any) => void
4 |
5 | export interface PerfsAddon {
6 | /**
7 | * Performs a callback on the API performance timings of the request.
8 | *
9 | * Warning: Still experimental on browsers and node.js
10 | */
11 | perfs: (this: C & WretchResponseChain, cb?: PerfCallback) => this,
12 | }
13 |
14 | /**
15 | * Adds the ability to measure requests using the Performance Timings API.
16 | *
17 | * Uses the Performance API
18 | * ([browsers](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) &
19 | * [node.js](https://nodejs.org/api/perf_hooks.html)) to expose timings related to
20 | * the underlying request.
21 | *
22 | * Browser timings are very accurate, node.js only contains raw measures.
23 | *
24 | * ```js
25 | * import PerfsAddon from "wretch/addons/perfs"
26 | *
27 | * // Use perfs() before the response types (text, json, ...)
28 | * wretch("...")
29 | * .addon(PerfsAddon())
30 | * .get()
31 | * .perfs((timings) => {
32 | * // Will be called when the timings are ready.
33 | * console.log(timings.startTime);
34 | * })
35 | * .res();
36 | *
37 | * ```
38 | *
39 | * For node.js, there is a little extra work to do :
40 | *
41 | * ```js
42 | * // Node.js only
43 | * const { performance, PerformanceObserver } = require("perf_hooks");
44 | *
45 | * wretch.polyfills({
46 | * fetch: function (url, opts) {
47 | * performance.mark(url + " - begin");
48 | * return fetch(url, opts).then(res => {
49 | * performance.mark(url + " - end");
50 | * setTimeout(() => performance.measure(res.url, url + " - begin", url + " - end"), 0);
51 | * return res;
52 | * });
53 | * },
54 | * // other polyfills…
55 | * performance: performance,
56 | * PerformanceObserver: PerformanceObserver,
57 | * });
58 | * ```
59 | */
60 | const perfs: () => WretchAddon = () => {
61 | const callbacks = new Map()
62 | let observer /*: PerformanceObserver | null*/ = null
63 |
64 | const onMatch = (
65 | entries /*: PerformanceObserverEntryList */,
66 | name: string,
67 | callback: PerfCallback,
68 | performance: typeof globalThis.performance
69 | ) => {
70 | if (!entries.getEntriesByName)
71 | return false
72 | const matches = entries.getEntriesByName(name)
73 | if (matches && matches.length > 0) {
74 | callback(matches.reverse()[0])
75 | if (performance.clearMeasures)
76 | performance.clearMeasures(name)
77 | callbacks.delete(name)
78 |
79 | if (callbacks.size < 1) {
80 | observer.disconnect()
81 | if (performance.clearResourceTimings) {
82 | performance.clearResourceTimings()
83 | }
84 | }
85 | return true
86 | }
87 | return false
88 | }
89 |
90 | const initObserver = (
91 | performance: (typeof globalThis.performance) | null | undefined,
92 | performanceObserver /*: (typeof PerformanceObserver) | null | undefined */
93 | ) => {
94 | if (!observer && performance && performanceObserver) {
95 | observer = new performanceObserver(entries => {
96 | callbacks.forEach((callback, name) => {
97 | onMatch(entries, name, callback, performance)
98 | })
99 | })
100 | if (performance.clearResourceTimings) {
101 | performance.clearResourceTimings()
102 | }
103 | }
104 |
105 | return observer
106 | }
107 |
108 | const monitor = (
109 | name: string | null | undefined,
110 | callback: PerfCallback | null | undefined,
111 | config: Config
112 | ) => {
113 | if (!name || !callback)
114 | return
115 |
116 | const performance = config.polyfill("performance", false)
117 | const performanceObserver = config.polyfill("PerformanceObserver", false)
118 |
119 | if (!initObserver(performance, performanceObserver))
120 | return
121 |
122 | if (!onMatch(performance, name, callback, performance)) {
123 | if (callbacks.size < 1)
124 | observer.observe({ entryTypes: ["resource", "measure"] })
125 | callbacks.set(name, callback)
126 | }
127 | }
128 |
129 | return {
130 | resolver: {
131 | perfs(cb) {
132 | this._fetchReq
133 | .then(() =>
134 | monitor(this._wretchReq._url, cb, this._wretchReq._config)
135 | )
136 | .catch(() => {/* swallow */ })
137 | return this
138 | },
139 | }
140 | }
141 | }
142 |
143 | export default perfs
144 |
--------------------------------------------------------------------------------
/src/addons/progress.ts:
--------------------------------------------------------------------------------
1 | import type { ConfiguredMiddleware, WretchAddon, WretchResponseChain } from "../types.js"
2 |
3 | export interface ProgressResolver {
4 | /**
5 | * Provides a way to register a callback to be invoked one or multiple times during the download.
6 | * The callback receives the current progress as two arguments, the number of bytes loaded and the total number of bytes to load.
7 | *
8 | * _Under the hood: this method adds a middleware to the chain that will intercept the response and replace the body with a new one that will emit the progress event._
9 | *
10 | * ```js
11 | * import ProgressAddon from "wretch/addons/progress"
12 | *
13 | * wretch("some_url")
14 | * // Register the addon
15 | * .addon(ProgressAddon())
16 | * .get()
17 | * // Log the progress as a percentage of completion
18 | * .progress((loaded, total) => console.log(`${(loaded / total * 100).toFixed(0)}%`))
19 | * ```
20 | *
21 | * @param onProgress - A callback that will be called one or multiple times with the number of bytes loaded and the total number of bytes to load.
22 | */
23 | progress: (
24 | this: C & WretchResponseChain,
25 | onProgress: (loaded: number, total: number) => void
26 | ) => this
27 | }
28 |
29 | /**
30 | * Adds the ability to monitor progress when downloading a response.
31 | *
32 | * _Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._
33 | *
34 | * ```js
35 | * import ProgressAddon from "wretch/addons/progress"
36 | *
37 | * wretch("some_url")
38 | * // Register the addon
39 | * .addon(ProgressAddon())
40 | * .get()
41 | * // Log the progress as a percentage of completion
42 | * .progress((loaded, total) => console.log(`${(loaded / total * 100).toFixed(0)}%`))
43 | * ```
44 | */
45 | const progress: () => WretchAddon = () => {
46 | function transformMiddleware(state: Record) : ConfiguredMiddleware {
47 | return next => (url, opts) => {
48 | let loaded = 0
49 | let total = 0
50 | return next(url, opts).then(response => {
51 | try {
52 | const contentLength = response.headers.get("content-length")
53 | total = contentLength ? +contentLength : null
54 | const transform = new TransformStream({
55 | transform(chunk, controller) {
56 | loaded += chunk.length
57 | if (total < loaded) {
58 | total = loaded
59 | }
60 | if (state.progress) {
61 | state.progress(loaded, total)
62 | }
63 | controller.enqueue(chunk)
64 | }
65 | })
66 | return new Response(response.body.pipeThrough(transform), response)
67 | } catch (e) {
68 | return response
69 | }
70 | })
71 | }
72 | }
73 |
74 | return {
75 | beforeRequest(wretch, _, state) {
76 | return wretch.middlewares([transformMiddleware(state)])
77 | },
78 | resolver: {
79 | progress(onProgress: (loaded: number, total: number) => void) {
80 | this._sharedState.progress = onProgress
81 | return this
82 | }
83 | },
84 | }
85 | }
86 |
87 | export default progress
88 |
--------------------------------------------------------------------------------
/src/addons/queryString.ts:
--------------------------------------------------------------------------------
1 | import type { Wretch, Config, WretchAddon } from "../types.js"
2 |
3 | function stringify(value?: string | null): string | null {
4 | return typeof value !== "undefined" ? value : ""
5 | }
6 |
7 | const appendQueryParams = (url: string, qp: object | string, replace: boolean, omitUndefinedOrNullValues: boolean, config: Config) => {
8 | let queryString: string
9 |
10 | if (typeof qp === "string") {
11 | queryString = qp
12 | } else {
13 | const usp = config.polyfill("URLSearchParams", true, true)
14 | for (const key in qp) {
15 | const value = qp[key]
16 | if (omitUndefinedOrNullValues && (value === null || value === undefined)) continue
17 | if (qp[key] instanceof Array) {
18 | for (const val of value)
19 | usp.append(key, stringify(val))
20 | } else {
21 | usp.append(key, stringify(value))
22 | }
23 | }
24 | queryString = usp.toString()
25 | }
26 |
27 | const split = url.split("?")
28 |
29 | if (!queryString)
30 | return replace ? split[0] : url
31 |
32 | if (replace || split.length < 2)
33 | return split[0] + "?" + queryString
34 |
35 | return url + "&" + queryString
36 | }
37 |
38 | export interface QueryStringAddon {
39 | /**
40 | * Converts a javascript object to query parameters, then appends this query string
41 | * to the current url. String values are used as the query string verbatim.
42 | *
43 | * Pass `true` as the second argument to replace existing query parameters.
44 | * Pass `true` as the third argument to completely omit the key=value pair for undefined or null values.
45 | *
46 | * ```
47 | * import QueryAddon from "wretch/addons/queryString"
48 | *
49 | * let w = wretch("http://example.com").addon(QueryStringAddon);
50 | * // url is http://example.com
51 | * w = w.query({ a: 1, b: 2 });
52 | * // url is now http://example.com?a=1&b=2
53 | * w = w.query({ c: 3, d: [4, 5] });
54 | * // url is now http://example.com?a=1&b=2c=3&d=4&d=5
55 | * w = w.query("five&six&seven=eight");
56 | * // url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight
57 | * w = w.query({ reset: true }, true);
58 | * // url is now http://example.com?reset=true
59 | * ```
60 | *
61 | * ##### **Note that .query is not meant to handle complex cases with nested objects.**
62 | *
63 | * For this kind of usage, you can use `wretch` in conjunction with other libraries
64 | * (like [`qs`](https://github.com/ljharb/qs)).
65 | *
66 | * ```js
67 | * // Using wretch with qs
68 | *
69 | * const queryObject = { some: { nested: "objects" } };
70 | * const w = wretch("https://example.com/").addon(QueryStringAddon)
71 | *
72 | * // Use .qs inside .query :
73 | *
74 | * w.query(qs.stringify(queryObject));
75 | *
76 | * // Use .defer :
77 | *
78 | * const qsWretch = w.defer((w, url, { qsQuery, qsOptions }) => (
79 | * qsQuery ? w.query(qs.stringify(qsQuery, qsOptions)) : w
80 | * ));
81 | *
82 | * qsWretch
83 | * .url("https://example.com/")
84 | * .options({ qs: { query: queryObject } });
85 | * ```
86 | *
87 | * @param qp - An object which will be converted, or a string which will be used verbatim.
88 | */
89 | query(this: T & Wretch, qp: object | string, replace?: boolean, omitUndefinedOrNullValues?: boolean): this
90 | }
91 |
92 | /**
93 | * Adds the ability to append query parameters from a javascript object.
94 | *
95 | * ```js
96 | * import QueryAddon from "wretch/addons/queryString"
97 | *
98 | * wretch().addon(QueryAddon)
99 | * ```
100 | */
101 | const queryString: WretchAddon = {
102 | wretch: {
103 | query(qp, replace = false, omitUndefinedOrNullValues = false) {
104 | return { ...this, _url: appendQueryParams(this._url, qp, replace, omitUndefinedOrNullValues, this._config) }
105 | }
106 | }
107 | }
108 |
109 | export default queryString
110 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { mix } from "./utils.js"
2 | import type { Config, ErrorType, WretchOptions } from "./types.js"
3 |
4 | declare const global
5 |
6 | const config: Config = {
7 | // Default options
8 | options: {},
9 | // Error type
10 | errorType: "text",
11 | // Polyfills
12 | polyfills: {
13 | // fetch: null,
14 | // FormData: null,
15 | // URL: null,
16 | // URLSearchParams: null,
17 | // performance: null,
18 | // PerformanceObserver: null,
19 | // AbortController: null,
20 | },
21 | polyfill(p: string, doThrow: boolean = true, instance: boolean = false, ...args: any[]) {
22 | const res = this.polyfills[p] ||
23 | (typeof self !== "undefined" ? self[p] : null) ||
24 | (typeof global !== "undefined" ? global[p] : null)
25 | if (doThrow && !res) throw new Error(p + " is not defined")
26 | return instance && res ? new res(...args) : res
27 | }
28 | }
29 |
30 | /**
31 | * Sets the default fetch options that will be stored internally when instantiating wretch objects.
32 | *
33 | * ```js
34 | * import wretch from "wretch"
35 | *
36 | * wretch.options({ headers: { "Accept": "application/json" } });
37 | *
38 | * // The fetch request is sent with both headers.
39 | * wretch("...", { headers: { "X-Custom": "Header" } }).get().res();
40 | * ```
41 | *
42 | * @param options Default options
43 | * @param replace If true, completely replaces the existing options instead of mixing in
44 | */
45 | export function setOptions(options: WretchOptions, replace = false) {
46 | config.options = replace ? options : mix(config.options, options)
47 | }
48 |
49 | /**
50 | * Sets the default polyfills that will be stored internally when instantiating wretch objects.
51 | * Useful for browserless environments like `node.js`.
52 | *
53 | * Needed for libraries like [fetch-ponyfill](https://github.com/qubyte/fetch-ponyfill).
54 | *
55 | * ```js
56 | * import wretch from "wretch"
57 | *
58 | * wretch.polyfills({
59 | * fetch: require("node-fetch"),
60 | * FormData: require("form-data"),
61 | * URLSearchParams: require("url").URLSearchParams,
62 | * });
63 | *
64 | * // Uses the above polyfills.
65 | * wretch("...").get().res();
66 | * ```
67 | *
68 | * @param polyfills An object containing the polyfills
69 | * @param replace If true, replaces the current polyfills instead of mixing in
70 | */
71 | export function setPolyfills(polyfills: object, replace = false) {
72 | config.polyfills = replace ? polyfills : mix(config.polyfills, polyfills)
73 | }
74 |
75 | /**
76 | * Sets the default method (text, json, …) used to parse the data contained in the response body in case of an HTTP error.
77 | * As with other static methods, it will affect wretch instances created after calling this function.
78 | *
79 | * _Note: if the response Content-Type header is set to "application/json", the body will be parsed as json regardless of the errorType._
80 | *
81 | * ```js
82 | * import wretch from "wretch"
83 | *
84 | * wretch.errorType("json")
85 | *
86 | * wretch("http://server/which/returns/an/error/with/a/json/body")
87 | * .get()
88 | * .res()
89 | * .catch(error => {
90 | * // error[errorType] (here, json) contains the parsed body
91 | * console.log(error.json)
92 | * })
93 | * ```
94 | *
95 | * If null, defaults to "text".
96 | */
97 | export function setErrorType(errorType: ErrorType) {
98 | config.errorType = errorType
99 | }
100 |
101 | export default config
102 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const JSON_MIME = "application/json"
2 | export const CONTENT_TYPE_HEADER = "Content-Type"
3 | export const FETCH_ERROR = Symbol()
4 | export const CATCHER_FALLBACK = Symbol()
5 |
--------------------------------------------------------------------------------
/src/core.ts:
--------------------------------------------------------------------------------
1 | import { mix, extractContentType, isLikelyJsonMime } from "./utils.js"
2 | import { JSON_MIME, CONTENT_TYPE_HEADER, CATCHER_FALLBACK } from "./constants.js"
3 | import { resolver } from "./resolver.js"
4 | import config from "./config.js"
5 | import type { Wretch, ErrorType } from "./types.js"
6 |
7 | export const core: Wretch = {
8 | _url: "",
9 | _options: {},
10 | _config: config,
11 | _catchers: new Map(),
12 | _resolvers: [],
13 | _deferred: [],
14 | _middlewares: [],
15 | _addons: [],
16 | addon(addon) {
17 | return { ...this, _addons: [...this._addons, addon], ...addon.wretch }
18 | },
19 | errorType(errorType: ErrorType) {
20 | return {
21 | ...this,
22 | _config: {
23 | ...this._config,
24 | errorType
25 | }
26 | }
27 | },
28 | polyfills(polyfills, replace = false) {
29 | return {
30 | ...this,
31 | _config: {
32 | ...this._config,
33 | polyfills: replace ? polyfills : mix(this._config.polyfills, polyfills)
34 | }
35 | }
36 | },
37 | url(_url, replace = false) {
38 | if (replace)
39 | return { ...this, _url }
40 | const split = this._url.split("?")
41 | return {
42 | ...this,
43 | _url: split.length > 1 ?
44 | split[0] + _url + "?" + split[1] :
45 | this._url + _url
46 | }
47 | },
48 | options(options, replace = false) {
49 | return { ...this, _options: replace ? options : mix(this._options, options) }
50 | },
51 | headers(headerValues) {
52 | const headers =
53 | !headerValues ? {} :
54 | Array.isArray(headerValues) ? Object.fromEntries(headerValues) :
55 | "entries" in headerValues ? Object.fromEntries((headerValues as Headers).entries()) :
56 | headerValues
57 | return { ...this, _options: mix(this._options, { headers }) }
58 | },
59 | accept(headerValue) {
60 | return this.headers({ Accept: headerValue })
61 | },
62 | content(headerValue) {
63 | return this.headers({ [CONTENT_TYPE_HEADER]: headerValue })
64 | },
65 | auth(headerValue) {
66 | return this.headers({ Authorization: headerValue })
67 | },
68 | catcher(errorId, catcher) {
69 | const newMap = new Map(this._catchers)
70 | newMap.set(errorId, catcher)
71 | return { ...this, _catchers: newMap }
72 | },
73 | catcherFallback(catcher) {
74 | return this.catcher(CATCHER_FALLBACK, catcher)
75 | },
76 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
77 | resolve(resolver, clear: boolean = false) {
78 | return { ...this, _resolvers: clear ? [resolver] : [...this._resolvers, resolver] }
79 | },
80 | defer(callback, clear: boolean = false) {
81 | return {
82 | ...this,
83 | _deferred: clear ? [callback] : [...this._deferred, callback]
84 | }
85 | },
86 | middlewares(middlewares, clear = false) {
87 | return {
88 | ...this,
89 | _middlewares: clear ? middlewares : [...this._middlewares, ...middlewares]
90 | }
91 | },
92 | fetch(method: string = this._options.method, url = "", body = null) {
93 | let base = this.url(url).options({ method })
94 | // "Jsonify" the body if it is an object and if it is likely that the content type targets json.
95 | const contentType = extractContentType(base._options.headers)
96 | const formDataClass = this._config.polyfill("FormData", false)
97 | const jsonify =
98 | typeof body === "object" &&
99 | !(formDataClass && body instanceof formDataClass) &&
100 | (!base._options.headers || !contentType || isLikelyJsonMime(contentType))
101 | base =
102 | !body ? base :
103 | jsonify ? base.json(body, contentType) :
104 | base.body(body)
105 | return resolver(
106 | base
107 | ._deferred
108 | .reduce((acc: Wretch, curr) => curr(acc, acc._url, acc._options), base)
109 | )
110 | },
111 | get(url = "") {
112 | return this.fetch("GET", url)
113 | },
114 | delete(url = "") {
115 | return this.fetch("DELETE", url)
116 | },
117 | put(body, url = "") {
118 | return this.fetch("PUT", url, body)
119 | },
120 | post(body, url = "") {
121 | return this.fetch("POST", url, body)
122 | },
123 | patch(body, url = "") {
124 | return this.fetch("PATCH", url, body)
125 | },
126 | head(url = "") {
127 | return this.fetch("HEAD", url)
128 | },
129 | opts(url = "") {
130 | return this.fetch("OPTIONS", url)
131 | },
132 | body(contents) {
133 | return { ...this, _options: { ...this._options, body: contents } }
134 | },
135 | json(jsObject, contentType) {
136 | const currentContentType = extractContentType(this._options.headers)
137 | return this.content(
138 | contentType ||
139 | isLikelyJsonMime(currentContentType) && currentContentType ||
140 | JSON_MIME
141 | ).body(JSON.stringify(jsObject))
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/index.all.ts:
--------------------------------------------------------------------------------
1 | import { setOptions, setErrorType, setPolyfills } from "./config.js"
2 | import { core } from "./core.js"
3 | import * as Addons from "./addons/index.js"
4 | import { WretchError } from "./resolver.js"
5 |
6 | function factory(_url = "", _options = {}) {
7 | return { ...core, _url, _options }
8 | .addon(Addons.abortAddon())
9 | .addon(Addons.basicAuthAddon)
10 | .addon(Addons.formDataAddon)
11 | .addon(Addons.formUrlAddon)
12 | .addon(Addons.perfsAddon())
13 | .addon(Addons.queryStringAddon)
14 | .addon(Addons.progressAddon())
15 | }
16 |
17 | factory["default"] = factory
18 | factory.options = setOptions
19 | factory.errorType = setErrorType
20 | factory.polyfills = setPolyfills
21 | factory.WretchError = WretchError
22 |
23 | export default factory
24 |
--------------------------------------------------------------------------------
/src/index.cts:
--------------------------------------------------------------------------------
1 | import factory from "./index.js"
2 |
3 | module.exports = factory.default
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { setOptions, setErrorType, setPolyfills } from "./config.js"
2 | import { core } from "./core.js"
3 | import { WretchError } from "./resolver.js"
4 | import type { Wretch, WretchOptions } from "./types.js"
5 |
6 | export type {
7 | Wretch,
8 | Config,
9 | ConfiguredMiddleware,
10 | FetchLike,
11 | Middleware,
12 | WretchResponseChain,
13 | WretchOptions,
14 | WretchError,
15 | WretchErrorCallback,
16 | WretchResponse,
17 | WretchDeferredCallback,
18 | WretchAddon
19 | } from "./types.js"
20 |
21 | /**
22 | * Creates a new wretch instance with a base url and base
23 | * [fetch options](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch).
24 | *
25 | * ```ts
26 | * import wretch from "wretch"
27 | *
28 | * // Reusable instance
29 | * const w = wretch("https://domain.com", { mode: "cors" })
30 | * ```
31 | *
32 | * @param _url The base url
33 | * @param _options The base fetch options
34 | * @returns A fresh wretch instance
35 | */
36 | function factory(_url = "", _options: WretchOptions = {}): Wretch {
37 | return { ...core, _url, _options }
38 | }
39 |
40 | factory["default"] = factory
41 | factory.options = setOptions
42 | factory.errorType = setErrorType
43 | factory.polyfills = setPolyfills
44 | factory.WretchError = WretchError
45 |
46 | export default factory
47 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { ConfiguredMiddleware, FetchLike } from "./types.js"
2 |
3 | /**
4 | * @private @internal
5 | */
6 | export const middlewareHelper = (middlewares: ConfiguredMiddleware[]) => (fetchFunction: FetchLike): FetchLike => {
7 | return middlewares.reduceRight((acc, curr) => curr(acc), fetchFunction) || fetchFunction
8 | }
9 |
--------------------------------------------------------------------------------
/src/middlewares/dedupe.ts:
--------------------------------------------------------------------------------
1 | import type { ConfiguredMiddleware, WretchOptions } from "../types.js"
2 |
3 | /* Types */
4 |
5 | export type DedupeSkipFunction = (url: string, opts: WretchOptions) => boolean
6 | export type DedupeKeyFunction = (url: string, opts: WretchOptions) => string
7 | export type DedupeResolverFunction = (response: Response) => Response
8 | export type DedupeOptions = {
9 | skip?: DedupeSkipFunction,
10 | key?: DedupeKeyFunction,
11 | resolver?: DedupeResolverFunction
12 | }
13 | /**
14 | * ## Dedupe middleware
15 | *
16 | * #### Prevents having multiple identical requests on the fly at the same time.
17 | *
18 | * **Options**
19 | *
20 | * - *skip* `(url, opts) => boolean`
21 | *
22 | * > If skip returns true, then the dedupe check is skipped.
23 | *
24 | * - *key* `(url, opts) => string`
25 | *
26 | * > Returns a key that is used to identify the request.
27 | *
28 | * - *resolver* `(response: Response) => Response`
29 | *
30 | * > This function is called when resolving the fetch response from duplicate calls.
31 | * By default it clones the response to allow reading the body from multiple sources.
32 | */
33 | export type DedupeMiddleware = (options?: DedupeOptions) => ConfiguredMiddleware
34 |
35 | /* Defaults */
36 |
37 | const defaultSkip: DedupeSkipFunction = (_, opts) => (
38 | opts.skipDedupe || opts.method !== "GET"
39 | )
40 | const defaultKey: DedupeKeyFunction = (url: string, opts) => opts.method + "@" + url
41 | const defaultResolver: DedupeResolverFunction = response => response.clone()
42 |
43 | export const dedupe: DedupeMiddleware = ({ skip = defaultSkip, key = defaultKey, resolver = defaultResolver } = {}) => {
44 |
45 | const inflight = new Map()
46 |
47 | return next => (url, opts) => {
48 |
49 | if (skip(url, opts)) {
50 | return next(url, opts)
51 | }
52 |
53 | const _key = key(url, opts)
54 |
55 | if (!inflight.has(_key)) {
56 | inflight.set(_key, [])
57 | } else {
58 | return new Promise((resolve, reject) => {
59 | inflight.get(_key).push([resolve, reject])
60 | })
61 | }
62 |
63 | try {
64 | return next(url, opts)
65 | .then(response => {
66 | // Resolve pending promises
67 | inflight.get(_key).forEach(([resolve]) => resolve(resolver(response)))
68 | // Remove the inflight pending promises
69 | inflight.delete(_key)
70 | // Return the original response
71 | return response
72 | })
73 | .catch(error => {
74 | // Reject pending promises on error
75 | inflight.get(_key).forEach(([, reject]) => reject(error))
76 | inflight.delete(_key)
77 | throw error
78 | })
79 | } catch (error) {
80 | inflight.delete(_key)
81 | return Promise.reject(error)
82 | }
83 |
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/middlewares/delay.ts:
--------------------------------------------------------------------------------
1 | import type { ConfiguredMiddleware } from "../types.js"
2 |
3 | /* Types */
4 |
5 | /**
6 | * ## Delay middleware
7 | *
8 | * ### Delays the request by a specific amount of time.
9 | *
10 | * **Options**
11 | *
12 | * - *time* `milliseconds`
13 | *
14 | * > The request will be delayed by that amount of time.
15 | */
16 | export type DelayMiddleware = (time: number) => ConfiguredMiddleware
17 |
18 |
19 | export const delay: DelayMiddleware = time => next => (url, opts) => {
20 | return new Promise(res => setTimeout(() => res(next(url, opts)), time))
21 | }
22 |
--------------------------------------------------------------------------------
/src/middlewares/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./delay.js"
2 | export * from "./dedupe.js"
3 | export * from "./throttlingCache.js"
4 | export * from "./retry.js"
5 |
--------------------------------------------------------------------------------
/src/resolver.ts:
--------------------------------------------------------------------------------
1 | import { middlewareHelper } from "./middleware.js"
2 | import { mix } from "./utils.js"
3 | import type { Wretch, WretchResponse, WretchResponseChain, WretchError as WretchErrorType } from "./types.js"
4 | import { FETCH_ERROR, CATCHER_FALLBACK } from "./constants.js"
5 |
6 | /**
7 | * This class inheriting from Error is thrown when the fetch response is not "ok".
8 | * It extends Error and adds status, text and body fields.
9 | */
10 | export class WretchError extends Error implements WretchErrorType {
11 | status: number
12 | response: WretchResponse
13 | url: string
14 | text?: string
15 | json?: any
16 | }
17 |
18 | export const resolver = (wretch: T & Wretch) => {
19 | const sharedState = Object.create(null)
20 |
21 | wretch = wretch._addons.reduce((w, addon) =>
22 | addon.beforeRequest &&
23 | addon.beforeRequest(w, wretch._options, sharedState)
24 | || w,
25 | wretch)
26 |
27 | const {
28 | _url: url,
29 | _options: opts,
30 | _config: config,
31 | _catchers: _catchers,
32 | _resolvers: resolvers,
33 | _middlewares: middlewares,
34 | _addons: addons
35 | } = wretch
36 |
37 | const catchers = new Map(_catchers)
38 | const finalOptions = mix(config.options, opts)
39 |
40 | // The generated fetch request
41 | let finalUrl = url
42 | const _fetchReq = middlewareHelper(middlewares)((url, options) => {
43 | finalUrl = url
44 | return config.polyfill("fetch")(url, options)
45 | })(url, finalOptions)
46 | // Throws on an http error
47 | const referenceError = new Error()
48 | const throwingPromise: Promise = _fetchReq
49 | .catch(error => {
50 | throw { [FETCH_ERROR]: error }
51 | })
52 | .then(response => {
53 | if (!response.ok) {
54 | const err = new WretchError()
55 | // Enhance the error object
56 | err["cause"] = referenceError
57 | err.stack = err.stack + "\nCAUSE: " + referenceError.stack
58 | err.response = response
59 | err.status = response.status
60 | err.url = finalUrl
61 |
62 | if (response.type === "opaque") {
63 | throw err
64 | }
65 |
66 | const jsonErrorType = config.errorType === "json" || response.headers.get("Content-Type")?.split(";")[0] === "application/json"
67 | const bodyPromise =
68 | !config.errorType ? Promise.resolve(response.body) :
69 | jsonErrorType ? response.text() :
70 | response[config.errorType]()
71 |
72 | return bodyPromise.then((body: unknown) => {
73 | err.message = typeof body === "string" ? body : response.statusText
74 | if(body) {
75 | if(jsonErrorType && typeof body === "string") {
76 | err.text = body
77 | err.json = JSON.parse(body)
78 | } else {
79 | err[config.errorType] = body
80 | }
81 | }
82 | throw err
83 | })
84 | }
85 | return response
86 | })
87 | // Wraps the Promise in order to dispatch the error to a matching catcher
88 | const catchersWrapper = (promise: Promise): Promise => {
89 | return promise.catch(err => {
90 | const fetchErrorFlag = Object.prototype.hasOwnProperty.call(err, FETCH_ERROR)
91 | const error = fetchErrorFlag ? err[FETCH_ERROR] : err
92 |
93 | const catcher =
94 | (error?.status && catchers.get(error.status)) ||
95 | catchers.get(error?.name) || (
96 | fetchErrorFlag && catchers.has(FETCH_ERROR) && catchers.get(FETCH_ERROR)
97 | )
98 |
99 | if (catcher)
100 | return catcher(error, wretch)
101 |
102 | const catcherFallback = catchers.get(CATCHER_FALLBACK)
103 | if (catcherFallback)
104 | return catcherFallback(error, wretch)
105 |
106 | throw error
107 | })
108 | }
109 | // Enforces the proper promise type when a body parsing method is called.
110 | type BodyParser = (funName: "json" | "blob" | "formData" | "arrayBuffer" | "text" | null) => (cb?: (type: Type) => Result) => Promise>
111 | const bodyParser: BodyParser = funName => cb => funName ?
112 | // If a callback is provided, then callback with the body result otherwise return the parsed body itself.
113 | catchersWrapper(throwingPromise.then(_ => _ && _[funName]()).then(_ => cb ? cb(_) : _)) :
114 | // No body parsing method - return the response
115 | catchersWrapper(throwingPromise.then(_ => cb ? cb(_ as any) : _))
116 |
117 | const responseChain: WretchResponseChain = {
118 | _wretchReq: wretch,
119 | _fetchReq,
120 | _sharedState: sharedState,
121 | res: bodyParser(null),
122 | json: bodyParser("json"),
123 | blob: bodyParser("blob"),
124 | formData: bodyParser("formData"),
125 | arrayBuffer: bodyParser("arrayBuffer"),
126 | text: bodyParser("text"),
127 | error(errorId, cb) {
128 | catchers.set(errorId, cb)
129 | return this
130 | },
131 | badRequest(cb) { return this.error(400, cb) },
132 | unauthorized(cb) { return this.error(401, cb) },
133 | forbidden(cb) { return this.error(403, cb) },
134 | notFound(cb) { return this.error(404, cb) },
135 | timeout(cb) { return this.error(408, cb) },
136 | internalError(cb) { return this.error(500, cb) },
137 | fetchError(cb) { return this.error(FETCH_ERROR, cb) },
138 | }
139 |
140 | const enhancedResponseChain: R extends undefined ? Chain & WretchResponseChain : R = addons.reduce((chain, addon) => ({
141 | ...chain,
142 | ...(typeof addon.resolver === "function" ? (addon.resolver as (_: WretchResponseChain) => any)(chain) : addon.resolver)
143 | }), responseChain)
144 |
145 | return resolvers.reduce((chain, r) => r(chain, wretch), enhancedResponseChain)
146 | }
147 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { CONTENT_TYPE_HEADER } from "./constants.js"
2 |
3 | export function extractContentType(headers: HeadersInit = {}): string | undefined {
4 | const normalizedHeaders = headers instanceof Array ? Object.fromEntries(headers) : headers
5 | return Object.entries(normalizedHeaders).find(([k]) =>
6 | k.toLowerCase() === CONTENT_TYPE_HEADER.toLowerCase()
7 | )?.[1]
8 | }
9 |
10 | export function isLikelyJsonMime(value: string): boolean {
11 | return /^application\/.*json.*/.test(value)
12 | }
13 |
14 | export const mix = function (one: object, two: object, mergeArrays: boolean = false) {
15 | return Object.entries(two).reduce((acc, [key, newValue]) => {
16 | const value = one[key]
17 | if (Array.isArray(value) && Array.isArray(newValue)) {
18 | acc[key] = mergeArrays ? [...value, ...newValue] : newValue
19 | } else if (typeof value === "object" && typeof newValue === "object") {
20 | acc[key] = mix(value, newValue, mergeArrays)
21 | } else {
22 | acc[key] = newValue
23 | }
24 |
25 | return acc
26 | }, { ...one })
27 | }
28 |
--------------------------------------------------------------------------------
/test/assets/duck.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elbywan/wretch/bf1dfe8fe532fadc6b6391fb678a785c3efc9b4b/test/assets/duck.jpg
--------------------------------------------------------------------------------
/test/node/middlewares/dedupe.spec.ts:
--------------------------------------------------------------------------------
1 | import * as http from "http"
2 | import wretch, { WretchOptions } from "../../../src"
3 | import { dedupe } from "../../../src/middlewares"
4 | import { mock } from "./mock"
5 |
6 | export default describe("Dedupe Middleware", () => {
7 | const PORT = 0
8 | let server: http.Server | null = null
9 | let logs: any[] = []
10 |
11 | const log = (url: string, options: WretchOptions) => {
12 | logs.push([url, options.method])
13 | }
14 |
15 | const baseAddress = () => {
16 | const { address, port } = (server as any).address()
17 | return "http://" + address + ":" + port
18 | }
19 |
20 | beforeAll(done => {
21 | server = http.createServer((req, res) => {
22 | req.pipe(res)
23 | })
24 | server.listen(PORT, "127.0.0.1")
25 | server.once("listening", () => {
26 | done()
27 | })
28 | server.once("error", () => {
29 | done()
30 | })
31 | })
32 |
33 | afterAll(() => {
34 | server?.close()
35 | })
36 |
37 | beforeEach(() => {
38 | logs = []
39 | })
40 |
41 | it("should prevent sending multiple requests", async () => {
42 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe()])
43 | const results = await Promise.all([
44 | w.get("/one").res(),
45 | w.get("/one").res(),
46 | w.get("/one").res(),
47 | w.get("/two").res(),
48 | w.get("/two").res(),
49 | w.get("/three").res(),
50 | w.post("body", "/one").res(),
51 | w.post("body", "/one").res(),
52 | ])
53 |
54 | expect(logs).toEqual([
55 | [baseAddress() + "/one", "GET"],
56 | [baseAddress() + "/two", "GET"],
57 | [baseAddress() + "/three", "GET"],
58 | [baseAddress() + "/one", "POST"],
59 | [baseAddress() + "/one", "POST"]
60 | ])
61 |
62 | results.forEach((result, i) => {
63 | expect(result).toMatchObject({
64 | url: baseAddress() + "/" + ((i < 3 || i > 5) ? "one" : i < 5 ? "two" : "three"),
65 | status: 200,
66 | statusText: "OK",
67 | })
68 | })
69 | })
70 |
71 | it("should skip some requests", async () => {
72 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe({
73 | skip: (url, options) => { return options.skip || url.endsWith("/toto") }
74 | })])
75 | await Promise.all([
76 | w.get("/one").res(),
77 | w.get("/one").res(),
78 | w.get("/one").res(),
79 | w.options({ skip: true }).get("/one").res(),
80 | w.get("/toto").res(),
81 | w.get("/toto").res()
82 | ])
83 |
84 | expect(logs).toEqual([
85 | [baseAddress() + "/one", "GET"],
86 | [baseAddress() + "/one", "GET"],
87 | [baseAddress() + "/toto", "GET"],
88 | [baseAddress() + "/toto", "GET"],
89 | ])
90 | })
91 |
92 | it("should key requests", async () => {
93 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe({
94 | key: () => { return "/same-key" }
95 | })])
96 |
97 | const results = await Promise.all([
98 | w.get("/one").res(),
99 | w.get("/two").res(),
100 | w.get("/three").res()
101 | ])
102 |
103 | expect(logs).toEqual([
104 | [baseAddress() + "/one", "GET"]
105 | ])
106 |
107 | results.forEach(result => {
108 | expect(result).toMatchObject({
109 | url: baseAddress() + "/one",
110 | status: 200,
111 | statusText: "OK",
112 | })
113 | })
114 | })
115 |
116 | it("should allow custom resolvers", async () => {
117 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe({
118 | resolver: res => res
119 | })])
120 |
121 | const results = await Promise.all([
122 | w.get("/one").res(),
123 | w.get("/one").res(),
124 | w.get("/one").res()
125 | ])
126 |
127 | expect(results[0]).toStrictEqual(results[1])
128 | expect(results[0]).toStrictEqual(results[2])
129 | })
130 | })
131 |
--------------------------------------------------------------------------------
/test/node/middlewares/delay.spec.ts:
--------------------------------------------------------------------------------
1 | import wretch from "../../../src"
2 | import { delay } from "../../../src/middlewares"
3 |
4 | export default describe("Delay Middleware", () => {
5 | it("should delay requests", async () => {
6 | let before = 0
7 | let after = 0
8 | await wretch("").polyfills({ fetch: () => Promise.resolve({ ok: true }) }).middlewares([
9 | next => (url, options) => {
10 | before = new Date().getTime()
11 | return next(url, options).then(response => {
12 | after = new Date().getTime()
13 | return response
14 | })
15 | },
16 | delay(1000)
17 | ]).get().res()
18 | expect(after - before).toBeGreaterThanOrEqual(1000)
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/test/node/middlewares/mock.ts:
--------------------------------------------------------------------------------
1 | import nodeFetch from "node-fetch"
2 |
3 | export const mock = (cb, fetch = nodeFetch) => {
4 | return (url, options) => {
5 | cb(url, options)
6 | return fetch(url, options)
7 | }
8 | }
--------------------------------------------------------------------------------
/test/resolver.cjs:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const srcDir = path.resolve(__dirname, "..", "src")
3 |
4 | module.exports = (path, options) => {
5 | if (options.basedir.startsWith(srcDir) && path.endsWith(".js")) {
6 | return options.defaultResolver(path.substring(0, path.length - 3) + ".ts", options);
7 | }
8 | return options.defaultResolver(path, options);
9 | };
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "lib": [
5 | "es2015",
6 | "dom",
7 | "dom.iterable"
8 | ],
9 | "module": "commonjs",
10 | "noImplicitAny": false,
11 | "sourceMap": true,
12 | "allowJs": true,
13 | "types": [
14 | "node",
15 | "jest"
16 | ],
17 | },
18 | "include": [
19 | "./node/**/*.spec.ts"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "outDir": "dist/cjs",
6 | "declarationDir": "dist/cjs"
7 | },
8 | "exclude": []
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "lib": ["es2020", "dom", "dom.iterable"],
5 | "module": "es2015",
6 | "outDir": "dist",
7 | "declaration": true,
8 | "declarationDir": "dist",
9 | "noImplicitAny": false,
10 | "sourceMap": true,
11 | "inlineSources": true,
12 | "moduleResolution": "node",
13 | "isolatedModules": true
14 | },
15 | "include": ["src/**/*"],
16 | "exclude": ["src/**/index.cts"]
17 | }
18 |
--------------------------------------------------------------------------------
A ready to use middleware which is called before the request is sent. 2 | Input is the next middleware in the chain, then url and options. 3 | Output is a promise.
4 |