├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── bower.json ├── config.js ├── dist ├── amd │ └── aurelia-fetch-client.js ├── aurelia-fetch-client.d.ts ├── commonjs │ └── aurelia-fetch-client.js ├── es2015 │ └── aurelia-fetch-client.js ├── es2017 │ └── aurelia-fetch-client.js ├── native-modules │ └── aurelia-fetch-client.js ├── system │ └── aurelia-fetch-client.js ├── umd-es2015 │ └── aurelia-fetch-client.js └── umd │ └── aurelia-fetch-client.js ├── doc ├── CHANGELOG.md ├── api.json ├── dom.d.ts ├── url.d.ts └── whatwg-fetch.d.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── aurelia-fetch-client.ts ├── http-client-configuration.ts ├── http-client.ts ├── interfaces.ts ├── retry-interceptor.ts └── util.ts ├── test ├── http-client.spec.ts ├── setup.ts └── util.spec.ts ├── tsconfig.json ├── tslint.json └── typings.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | map-1: &filter_only_develop 4 | filters: 5 | branches: 6 | only: develop 7 | 8 | map-2: &filter_only_tag 9 | filters: 10 | branches: 11 | ignore: /.*/ 12 | tags: 13 | only: /^v?[0-9]+(\.[0-9]+)*$/ 14 | 15 | orbs: 16 | v1: aurelia/v1@volatile 17 | 18 | workflows: 19 | main: 20 | jobs: 21 | - v1/build_test 22 | - v1/build_merge: 23 | <<: *filter_only_develop 24 | requires: 25 | - v1/build_test 26 | - v1/npm_publish: 27 | <<: *filter_only_tag 28 | name: npm_publish_dry 29 | args: "--dry-run" 30 | - request_publish_latest: 31 | <<: *filter_only_tag 32 | type: approval 33 | requires: 34 | - npm_publish_dry 35 | - v1/npm_publish: 36 | <<: *filter_only_tag 37 | name: npm_publish 38 | context: Aurelia 39 | requires: 40 | - request_publish_latest 41 | - v1/merge_back: 42 | <<: *filter_only_tag 43 | requires: 44 | - npm_publish 45 | 46 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | [**.*] 13 | indent_style = space 14 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | bower_components 4 | .idea 5 | .DS_STORE 6 | .rollupcache 7 | build/reports 8 | dist 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | jspm_packages 2 | bower_components 3 | node_modules 4 | .idea 5 | .rollupcache 6 | .vscode 7 | .circleci 8 | src 9 | test 10 | build 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## 1.8.2 (2019-03-15) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **all:** change es2015 back to native-modules ([14245e8](https://github.com/aurelia/fetch-client/commit/14245e8)) 11 | * **build:** adjust build script, add umd es2015, fix unpkg field ([b62089f](https://github.com/aurelia/fetch-client/commit/b62089f)) 12 | * **ci:** adjust test scripts, separate single/watch mode ([0309253](https://github.com/aurelia/fetch-client/commit/0309253)) 13 | * **retry-interceptor:** conform to Interceptor interface ([daae14b](https://github.com/aurelia/fetch-client/commit/daae14b)) 14 | 15 | 16 | 17 | # 1.8.0 (2019-01-18) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **http-client:** call trackRequestEnd when fetch fails ([cf64989](https://github.com/aurelia/fetch-client/commit/cf64989)) 23 | 24 | 25 | 26 | # 1.7.0 (2018-12-01) 27 | 28 | 29 | 30 | # 1.6.0 (2018-09-25) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **doc:** fix polyfill example with whatwg-fetch ([df50f6d](https://github.com/aurelia/fetch-client/commit/df50f6d)) 36 | 37 | 38 | 39 | # 1.5.0 (2018-09-11) 40 | 41 | 42 | 43 | # 1.4.0 (2018-06-14) 44 | 45 | 46 | ### Features 47 | 48 | * **fetch-client:** add retry functionality ([d16447a](https://github.com/aurelia/fetch-client/commit/d16447a)) 49 | * **http-client:** Expose buildRequest helper API ([33d364d](https://github.com/aurelia/fetch-client/commit/33d364d)) 50 | * **http-client:** Expose HttpClient to interceptors ([36518bc](https://github.com/aurelia/fetch-client/commit/36518bc)) 51 | * **http-client:** Forward Request from response interceptor ([cc91034](https://github.com/aurelia/fetch-client/commit/cc91034)) 52 | * **interface:** add signal to RequestInit interface ([7a056c0](https://github.com/aurelia/fetch-client/commit/7a056c0)) 53 | 54 | 55 | 56 | ## 1.3.1 (2018-01-30) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **http-client:** Rework application/json header ([946273a](https://github.com/aurelia/fetch-client/commit/946273a)), closes [#90](https://github.com/aurelia/fetch-client/issues/90) 62 | 63 | 64 | 65 | # 1.3.0 (2018-01-24) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **util:** Discontinue using Blob for JSON ([03ae35f](https://github.com/aurelia/fetch-client/commit/03ae35f)), closes [#90](https://github.com/aurelia/fetch-client/issues/90) 71 | 72 | 73 | 74 | # 1.2.0 (2017-12-20) 75 | 76 | 77 | ### Features 78 | 79 | * **HttpClient:** add JSON.stringify replacer ([2fc49a9](https://github.com/aurelia/fetch-client/commit/2fc49a9)) 80 | 81 | 82 | 83 | ## 1.1.3 (2017-08-22) 84 | 85 | 86 | 87 | ## 1.1.2 (2017-03-23) 88 | 89 | 90 | 91 | ## 1.1.1 (2017-02-21) 92 | 93 | 94 | 95 | # 1.1.0 (2016-12-03) 96 | 97 | 98 | ### Features 99 | 100 | * passing current config to configure(function(config)) ([124c28b](https://github.com/aurelia/fetch-client/commit/124c28b)), closes [#74](https://github.com/aurelia/fetch-client/issues/74) 101 | 102 | 103 | 104 | ## 1.0.1 (2016-08-26) 105 | 106 | 107 | 108 | # 1.0.0 (2016-07-27) 109 | 110 | 111 | 112 | # 1.0.0-rc.1.0.1 (2016-07-12) 113 | 114 | 115 | 116 | # 1.0.0-rc.1.0.0 (2016-06-22) 117 | 118 | 119 | 120 | # 1.0.0-beta.2.0.1 (2016-06-16) 121 | 122 | 123 | 124 | # 1.0.0-beta.2.0.0 (2016-06-13) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * **http-client:** silence bluebird warning ([#62](https://github.com/aurelia/fetch-client/issues/62)) ([608b133](https://github.com/aurelia/fetch-client/commit/608b133)) 130 | 131 | 132 | 133 | # 1.0.0-beta.1.2.5 (2016-05-10) 134 | 135 | 136 | 137 | # 1.0.0-beta.1.2.4 (2016-05-10) 138 | 139 | 140 | 141 | # 1.0.0-beta.1.2.2 (2016-04-29) 142 | 143 | 144 | 145 | # 1.0.0-beta.1.2.1 (2016-04-08) 146 | 147 | 148 | 149 | # 1.0.0-beta.1.2.0 (2016-03-22) 150 | 151 | 152 | 153 | # 1.0.0-beta.1.1.1 (2016-03-01) 154 | 155 | 156 | ### Bug Fixes 157 | 158 | * **all:** remove core-js dependency ([f91bd74](https://github.com/aurelia/fetch-client/commit/f91bd74)) 159 | * **http-client:** don't combine request url with base url when request url is absolute ([d1be3b4](https://github.com/aurelia/fetch-client/commit/d1be3b4)) 160 | * **http-client:** handle last null param in fetch method ([5b5d133](https://github.com/aurelia/fetch-client/commit/5b5d133)) 161 | 162 | 163 | 164 | # 1.0.0-beta.1.1.0 (2016-01-29) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **http-client:** ensure default content-type is respected ([f001eba](https://github.com/aurelia/fetch-client/commit/f001eba)), closes [#32](https://github.com/aurelia/fetch-client/issues/32) 170 | 171 | 172 | ### Features 173 | 174 | * **all:** update jspm meta; core-js ([dd62f23](https://github.com/aurelia/fetch-client/commit/dd62f23)) 175 | * **interceptors:** provide Request to response interceptors ([2d24bea](https://github.com/aurelia/fetch-client/commit/2d24bea)), closes [#33](https://github.com/aurelia/fetch-client/issues/33) 176 | 177 | 178 | 179 | # 1.0.0-beta.1.0.2 (2015-12-17) 180 | 181 | 182 | ### Bug Fixes 183 | 184 | * **http-client:** correct type check ([d38d1b3](https://github.com/aurelia/fetch-client/commit/d38d1b3)) 185 | * **http-client:** work around bug in IE/Edge where Blob types are ignored ([36407e2](https://github.com/aurelia/fetch-client/commit/36407e2)) 186 | 187 | 188 | 189 | # 1.0.0-beta.1.0.1 (2015-12-03) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * **build:** fix duplicate type definition error when building docs ([15d7213](https://github.com/aurelia/fetch-client/commit/15d7213)) 195 | * **build:** include fetch API typings with this library's typings ([b2869d5](https://github.com/aurelia/fetch-client/commit/b2869d5)), closes [#15](https://github.com/aurelia/fetch-client/issues/15) [#23](https://github.com/aurelia/fetch-client/issues/23) 196 | 197 | 198 | 199 | # 1.0.0-beta.1 (2015-11-16) 200 | 201 | 202 | ### Features 203 | 204 | * **http-client:** throw an error with a helpful message when used in an environment with no fetch support ([843451d](https://github.com/aurelia/fetch-client/commit/843451d)), closes [#21](https://github.com/aurelia/fetch-client/issues/21) 205 | 206 | 207 | 208 | # 0.4.0 (2015-11-10) 209 | 210 | 211 | ### Features 212 | 213 | * **http-client:** clean up configuration ([48c8204](https://github.com/aurelia/fetch-client/commit/48c8204)) 214 | 215 | 216 | 217 | # 0.3.0 (2015-10-13) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * **http-client:** fix bug where default headers were sometimes not applied correctly ([c3ed06c](https://github.com/aurelia/fetch-client/commit/c3ed06c)) 223 | * **http-client:** fix firefox crash ([939f1a9](https://github.com/aurelia/fetch-client/commit/939f1a9)) 224 | * **request-init:** adjust type annotation on headers to allow objects ([aeffb65](https://github.com/aurelia/fetch-client/commit/aeffb65)), closes [#16](https://github.com/aurelia/fetch-client/issues/16) 225 | 226 | 227 | ### Features 228 | 229 | * **HttpClient:** allow functions as default header values ([4f9153a](https://github.com/aurelia/fetch-client/commit/4f9153a)), closes [#17](https://github.com/aurelia/fetch-client/issues/17) 230 | 231 | 232 | 233 | # 0.2.0 (2015-09-04) 234 | 235 | 236 | ### Bug Fixes 237 | 238 | * **build:** update linting, testing and tools ([12f0cd9](https://github.com/aurelia/fetch-client/commit/12f0cd9)) 239 | 240 | 241 | ### Features 242 | 243 | * **docs:** generate api.json from .d.ts file ([80ccb0c](https://github.com/aurelia/fetch-client/commit/80ccb0c)) 244 | * **docs:** generate api.json from .d.ts file ([6d1cf4c](https://github.com/aurelia/fetch-client/commit/6d1cf4c)) 245 | 246 | 247 | 248 | ## 0.1.2 (2015-08-14) 249 | 250 | 251 | ### Bug Fixes 252 | 253 | * **http-client:** inline ConfigOrCallback type definition ([6a06260](https://github.com/aurelia/fetch-client/commit/6a06260)) 254 | 255 | 256 | 257 | ## 0.1.1 (2015-07-29) 258 | 259 | 260 | ### Bug Fixes 261 | 262 | * **all:** add corejs ([abc6fcf](https://github.com/aurelia/fetch-client/commit/abc6fcf)) 263 | * **http-client:** wrap request creation in a Promise so requestError interceptors will see errors ([522212b](https://github.com/aurelia/fetch-client/commit/522212b)) 264 | * **HttpClient:** fix crash in FF caused by attempting to iterate over Headers ([f45dd86](https://github.com/aurelia/fetch-client/commit/f45dd86)) 265 | 266 | 267 | ### Features 268 | 269 | * **all:** add type annotations ([15fbbbd](https://github.com/aurelia/fetch-client/commit/15fbbbd)) 270 | * **http-client:** make configure chainable ([946ba2c](https://github.com/aurelia/fetch-client/commit/946ba2c)) 271 | 272 | 273 | 274 | # 0.1.0 (2015-07-02) 275 | 276 | 277 | ### Features 278 | 279 | * **all:** add initial implementation ([dd63fb8](https://github.com/aurelia/fetch-client/commit/dd63fb8)) 280 | * **http-client-configuration:** add chainable helpers for all configuration properties ([26aa9df](https://github.com/aurelia/fetch-client/commit/26aa9df)) 281 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love for you to contribute and to make this project even better than it is today! If this interests you, please begin by reading [our contributing guidelines](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). The contributing document will provide you with all the information you need to get started. Once you have read that, you will need to also [sign our CLA](http://goo.gl/forms/dI8QDDSyKR) before we can accept a Pull Request from you. More information on the process is included in the [contributor's guide](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). 4 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 19 | **I'm submitting a bug report** 20 | **I'm submitting a feature request** 21 | 22 | * **Library Version:** 23 | major.minor.patch-pre 24 | 25 | 26 | **Please tell us about your environment:** 27 | * **Operating System:** 28 | OSX 10.x|Linux (distro)|Windows [7|8|8.1|10] 29 | 30 | * **Node Version:** 31 | 6.2.0 32 | 36 | 37 | * **NPM Version:** 38 | 3.8.9 39 | 43 | 44 | * **JSPM OR Webpack AND Version** 45 | JSPM 0.16.32 | webpack 2.1.0-beta.17 46 | 52 | 53 | * **Browser:** 54 | all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView 55 | 56 | * **Language:** 57 | all | TypeScript X.X | ESNext 58 | 59 | 60 | **Current behavior:** 61 | 62 | 63 | **Expected/desired behavior:** 64 | 71 | 72 | 73 | * **What is the expected behavior?** 74 | 75 | 76 | * **What is the motivation / use case for changing the behavior?** 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 - 2018 Blue Spire Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aurelia-fetch-client 2 | 3 | [![npm Version](https://img.shields.io/npm/v/aurelia-fetch-client.svg)](https://www.npmjs.com/package/aurelia-fetch-client) 4 | [![ZenHub](https://raw.githubusercontent.com/ZenHubIO/support/master/zenhub-badge.png)](https://zenhub.io) 5 | [![Join the chat at https://gitter.im/aurelia/discuss](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/aurelia/discuss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | [![CircleCI](https://circleci.com/gh/aurelia/fetch-client.svg?style=shield)](https://circleci.com/gh/aurelia/fetch-client) 7 | 8 | This library is part of the [Aurelia](http://www.aurelia.io/) platform and contains a simple client based on the Fetch standard. 9 | 10 | > To keep up to date on [Aurelia](http://www.aurelia.io/), please visit and subscribe to [the official blog](http://blog.aurelia.io/) and [our email list](http://eepurl.com/ces50j). We also invite you to [follow us on twitter](https://twitter.com/aureliaeffect). If you have questions look around our [Discourse forums](https://discourse.aurelia.io/), chat in our [community on Gitter](https://gitter.im/aurelia/discuss) or use [stack overflow](http://stackoverflow.com/search?q=aurelia). Documentation can be found [in our developer hub](http://aurelia.io/docs). If you would like to have deeper insight into our development process, please install the [ZenHub](https://zenhub.io) Chrome or Firefox Extension and visit any of our repository's boards. 11 | 12 | ## Documentation 13 | 14 | You can read documentation on the fetch client [here](http://aurelia.io/docs/plugins/http-services#aurelia-fetch-client). If you would like to help improve this documentation, the source for the above can be found in the doc folder within this repository. 15 | 16 | ## Platform Support 17 | 18 | This library can be used in the browser or on the server. A Fetch API polyfill may be needed. 19 | 20 | ## Building The Code 21 | 22 | To build the code, follow these steps. 23 | 24 | 1. Ensure that [NodeJS](http://nodejs.org/) is installed. This provides the platform on which the build tooling runs. 25 | 2. From the project folder, execute the following command: 26 | 27 | ```shell 28 | npm install 29 | ``` 30 | 3. To build the code, you can now run: 31 | 32 | ```shell 33 | npm run build 34 | ``` 35 | 4. You will find the compiled code in the `dist` folder, available in module formats: ESM, AMD, CommonJS and UMD. 36 | 37 | ## Running The Tests 38 | 39 | To run the unit tests, first ensure that you have followed the steps above in order to install all dependencies and successfully build the library. Once you have done that, proceed with these additional steps: 40 | 41 | 1. You can now run the tests with this command: 42 | 43 | ```shell 44 | npm run test 45 | ``` 46 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurelia-fetch-client", 3 | "version": "1.8.3", 4 | "description": "A simple client based on the Fetch standard.", 5 | "keywords": [ 6 | "aurelia", 7 | "http", 8 | "ajax", 9 | "fetch" 10 | ], 11 | "homepage": "http://aurelia.io", 12 | "main": "dist/commonjs/aurelia-fetch-client.js", 13 | "moduleType": "node", 14 | "license": "MIT", 15 | "authors": [ 16 | "Rob Eisenberg (http://robeisenberg.com/)" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "http://github.com/aurelia/fetch-client" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | defaultJSExtensions: true, 3 | transpiler: false, 4 | paths: { 5 | "github:*": "jspm_packages/github/*", 6 | "npm:*": "jspm_packages/npm/*" 7 | }, 8 | 9 | map: { 10 | "aurelia-pal": "npm:aurelia-pal@1.8.0", 11 | "aurelia-pal-browser": "npm:aurelia-pal-browser@1.8.0", 12 | "aurelia-polyfills": "npm:aurelia-polyfills@1.1.1", 13 | "npm:aurelia-pal-browser@1.8.0": { 14 | "aurelia-pal": "npm:aurelia-pal@1.8.0" 15 | }, 16 | "npm:aurelia-polyfills@1.1.1": { 17 | "aurelia-pal": "npm:aurelia-pal@1.8.0" 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /dist/amd/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | define(['exports', 'aurelia-pal'], function (exports, aureliaPal) { 'use strict'; 2 | 3 | function json(body, replacer) { 4 | return JSON.stringify((body !== undefined ? body : {}), replacer); 5 | } 6 | 7 | var retryStrategy = { 8 | fixed: 0, 9 | incremental: 1, 10 | exponential: 2, 11 | random: 3 12 | }; 13 | var defaultRetryConfig = { 14 | maxRetries: 3, 15 | interval: 1000, 16 | strategy: retryStrategy.fixed 17 | }; 18 | var RetryInterceptor = (function () { 19 | function RetryInterceptor(retryConfig) { 20 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 21 | if (this.retryConfig.strategy === retryStrategy.exponential && 22 | this.retryConfig.interval <= 1000) { 23 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 24 | } 25 | } 26 | RetryInterceptor.prototype.request = function (request) { 27 | var $r = request; 28 | if (!$r.retryConfig) { 29 | $r.retryConfig = Object.assign({}, this.retryConfig); 30 | $r.retryConfig.counter = 0; 31 | } 32 | $r.retryConfig.requestClone = request.clone(); 33 | return request; 34 | }; 35 | RetryInterceptor.prototype.response = function (response, request) { 36 | delete request.retryConfig; 37 | return response; 38 | }; 39 | RetryInterceptor.prototype.responseError = function (error, request, httpClient) { 40 | var retryConfig = request.retryConfig; 41 | var requestClone = retryConfig.requestClone; 42 | return Promise.resolve().then(function () { 43 | if (retryConfig.counter < retryConfig.maxRetries) { 44 | var result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 45 | return Promise.resolve(result).then(function (doRetry) { 46 | if (doRetry) { 47 | retryConfig.counter++; 48 | return new Promise(function (resolve) { return aureliaPal.PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0); }) 49 | .then(function () { 50 | var newRequest = requestClone.clone(); 51 | if (typeof (retryConfig.beforeRetry) === 'function') { 52 | return retryConfig.beforeRetry(newRequest, httpClient); 53 | } 54 | return newRequest; 55 | }) 56 | .then(function (newRequest) { 57 | return httpClient.fetch(Object.assign(newRequest, { retryConfig: retryConfig })); 58 | }); 59 | } 60 | delete request.retryConfig; 61 | throw error; 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | }; 68 | return RetryInterceptor; 69 | }()); 70 | function calculateDelay(retryConfig) { 71 | var interval = retryConfig.interval, strategy = retryConfig.strategy, minRandomInterval = retryConfig.minRandomInterval, maxRandomInterval = retryConfig.maxRandomInterval, counter = retryConfig.counter; 72 | if (typeof (strategy) === 'function') { 73 | return retryConfig.strategy(counter); 74 | } 75 | switch (strategy) { 76 | case (retryStrategy.fixed): 77 | return retryStrategies[retryStrategy.fixed](interval); 78 | case (retryStrategy.incremental): 79 | return retryStrategies[retryStrategy.incremental](counter, interval); 80 | case (retryStrategy.exponential): 81 | return retryStrategies[retryStrategy.exponential](counter, interval); 82 | case (retryStrategy.random): 83 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 84 | default: 85 | throw new Error('Unrecognized retry strategy'); 86 | } 87 | } 88 | var retryStrategies = [ 89 | function (interval) { return interval; }, 90 | function (retryCount, interval) { return interval * retryCount; }, 91 | function (retryCount, interval) { return retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000; }, 92 | function (retryCount, interval, minRandomInterval, maxRandomInterval) { 93 | if (minRandomInterval === void 0) { minRandomInterval = 0; } 94 | if (maxRandomInterval === void 0) { maxRandomInterval = 60000; } 95 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 96 | } 97 | ]; 98 | 99 | var HttpClientConfiguration = (function () { 100 | function HttpClientConfiguration() { 101 | this.baseUrl = ''; 102 | this.defaults = {}; 103 | this.interceptors = []; 104 | } 105 | HttpClientConfiguration.prototype.withBaseUrl = function (baseUrl) { 106 | this.baseUrl = baseUrl; 107 | return this; 108 | }; 109 | HttpClientConfiguration.prototype.withDefaults = function (defaults) { 110 | this.defaults = defaults; 111 | return this; 112 | }; 113 | HttpClientConfiguration.prototype.withInterceptor = function (interceptor) { 114 | this.interceptors.push(interceptor); 115 | return this; 116 | }; 117 | HttpClientConfiguration.prototype.useStandardConfiguration = function () { 118 | var standardConfig = { credentials: 'same-origin' }; 119 | Object.assign(this.defaults, standardConfig, this.defaults); 120 | return this.rejectErrorResponses(); 121 | }; 122 | HttpClientConfiguration.prototype.rejectErrorResponses = function () { 123 | return this.withInterceptor({ response: rejectOnError }); 124 | }; 125 | HttpClientConfiguration.prototype.withRetry = function (config) { 126 | var interceptor = new RetryInterceptor(config); 127 | return this.withInterceptor(interceptor); 128 | }; 129 | return HttpClientConfiguration; 130 | }()); 131 | function rejectOnError(response) { 132 | if (!response.ok) { 133 | throw response; 134 | } 135 | return response; 136 | } 137 | 138 | var HttpClient = (function () { 139 | function HttpClient() { 140 | this.activeRequestCount = 0; 141 | this.isRequesting = false; 142 | this.isConfigured = false; 143 | this.baseUrl = ''; 144 | this.defaults = null; 145 | this.interceptors = []; 146 | if (typeof fetch === 'undefined') { 147 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 148 | } 149 | } 150 | HttpClient.prototype.configure = function (config) { 151 | var normalizedConfig; 152 | if (typeof config === 'object') { 153 | normalizedConfig = { defaults: config }; 154 | } 155 | else if (typeof config === 'function') { 156 | normalizedConfig = new HttpClientConfiguration(); 157 | normalizedConfig.baseUrl = this.baseUrl; 158 | normalizedConfig.defaults = Object.assign({}, this.defaults); 159 | normalizedConfig.interceptors = this.interceptors; 160 | var c = config(normalizedConfig); 161 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 162 | normalizedConfig = c; 163 | } 164 | } 165 | else { 166 | throw new Error('invalid config'); 167 | } 168 | var defaults = normalizedConfig.defaults; 169 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 170 | throw new Error('Default headers must be a plain object.'); 171 | } 172 | var interceptors = normalizedConfig.interceptors; 173 | if (interceptors && interceptors.length) { 174 | if (interceptors.filter(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }).length > 1) { 175 | throw new Error('Only one RetryInterceptor is allowed.'); 176 | } 177 | var retryInterceptorIndex = interceptors.findIndex(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }); 178 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 179 | throw new Error('The retry interceptor must be the last interceptor defined.'); 180 | } 181 | } 182 | this.baseUrl = normalizedConfig.baseUrl; 183 | this.defaults = defaults; 184 | this.interceptors = normalizedConfig.interceptors || []; 185 | this.isConfigured = true; 186 | return this; 187 | }; 188 | HttpClient.prototype.fetch = function (input, init) { 189 | var _this = this; 190 | trackRequestStart(this); 191 | var request = this.buildRequest(input, init); 192 | return processRequest(request, this.interceptors, this).then(function (result) { 193 | var response = null; 194 | if (Response.prototype.isPrototypeOf(result)) { 195 | response = Promise.resolve(result); 196 | } 197 | else if (Request.prototype.isPrototypeOf(result)) { 198 | request = result; 199 | response = fetch(result); 200 | } 201 | else { 202 | throw new Error("An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [" + result + "]"); 203 | } 204 | return processResponse(response, _this.interceptors, request, _this); 205 | }) 206 | .then(function (result) { 207 | if (Request.prototype.isPrototypeOf(result)) { 208 | return _this.fetch(result); 209 | } 210 | return result; 211 | }) 212 | .then(function (result) { 213 | trackRequestEnd(_this); 214 | return result; 215 | }, function (error) { 216 | trackRequestEnd(_this); 217 | throw error; 218 | }); 219 | }; 220 | HttpClient.prototype.buildRequest = function (input, init) { 221 | var defaults = this.defaults || {}; 222 | var request; 223 | var body; 224 | var requestContentType; 225 | var parsedDefaultHeaders = parseHeaderValues(defaults.headers); 226 | if (Request.prototype.isPrototypeOf(input)) { 227 | request = input; 228 | requestContentType = new Headers(request.headers).get('Content-Type'); 229 | } 230 | else { 231 | if (!init) { 232 | init = {}; 233 | } 234 | body = init.body; 235 | var bodyObj = body ? { body: body } : null; 236 | var requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 237 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 238 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 239 | } 240 | if (!requestContentType) { 241 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 242 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 243 | } 244 | else if (body && isJSON(body)) { 245 | request.headers.set('Content-Type', 'application/json'); 246 | } 247 | } 248 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 249 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 250 | request.headers.set('Content-Type', body.type); 251 | } 252 | return request; 253 | }; 254 | HttpClient.prototype.get = function (input, init) { 255 | return this.fetch(input, init); 256 | }; 257 | HttpClient.prototype.post = function (input, body, init) { 258 | return callFetch(this, input, body, init, 'POST'); 259 | }; 260 | HttpClient.prototype.put = function (input, body, init) { 261 | return callFetch(this, input, body, init, 'PUT'); 262 | }; 263 | HttpClient.prototype.patch = function (input, body, init) { 264 | return callFetch(this, input, body, init, 'PATCH'); 265 | }; 266 | HttpClient.prototype.delete = function (input, body, init) { 267 | return callFetch(this, input, body, init, 'DELETE'); 268 | }; 269 | return HttpClient; 270 | }()); 271 | var absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 272 | function trackRequestStart(client) { 273 | client.isRequesting = !!(++client.activeRequestCount); 274 | if (client.isRequesting) { 275 | var evt_1 = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 276 | setTimeout(function () { return aureliaPal.DOM.dispatchEvent(evt_1); }, 1); 277 | } 278 | } 279 | function trackRequestEnd(client) { 280 | client.isRequesting = !!(--client.activeRequestCount); 281 | if (!client.isRequesting) { 282 | var evt_2 = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 283 | setTimeout(function () { return aureliaPal.DOM.dispatchEvent(evt_2); }, 1); 284 | } 285 | } 286 | function parseHeaderValues(headers) { 287 | var parsedHeaders = {}; 288 | for (var name_1 in headers || {}) { 289 | if (headers.hasOwnProperty(name_1)) { 290 | parsedHeaders[name_1] = (typeof headers[name_1] === 'function') ? headers[name_1]() : headers[name_1]; 291 | } 292 | } 293 | return parsedHeaders; 294 | } 295 | function getRequestUrl(baseUrl, url) { 296 | if (absoluteUrlRegexp.test(url)) { 297 | return url; 298 | } 299 | return (baseUrl || '') + url; 300 | } 301 | function setDefaultHeaders(headers, defaultHeaders) { 302 | for (var name_2 in defaultHeaders || {}) { 303 | if (defaultHeaders.hasOwnProperty(name_2) && !headers.has(name_2)) { 304 | headers.set(name_2, defaultHeaders[name_2]); 305 | } 306 | } 307 | } 308 | function processRequest(request, interceptors, http) { 309 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 310 | } 311 | function processResponse(response, interceptors, request, http) { 312 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 313 | } 314 | function applyInterceptors(input, interceptors, successName, errorName) { 315 | var interceptorArgs = []; 316 | for (var _i = 4; _i < arguments.length; _i++) { 317 | interceptorArgs[_i - 4] = arguments[_i]; 318 | } 319 | return (interceptors || []) 320 | .reduce(function (chain, interceptor) { 321 | var successHandler = interceptor[successName]; 322 | var errorHandler = interceptor[errorName]; 323 | return chain.then(successHandler && (function (value) { return successHandler.call.apply(successHandler, [interceptor, value].concat(interceptorArgs)); }) || identity, errorHandler && (function (reason) { return errorHandler.call.apply(errorHandler, [interceptor, reason].concat(interceptorArgs)); }) || thrower); 324 | }, Promise.resolve(input)); 325 | } 326 | function isJSON(str) { 327 | try { 328 | JSON.parse(str); 329 | } 330 | catch (err) { 331 | return false; 332 | } 333 | return true; 334 | } 335 | function identity(x) { 336 | return x; 337 | } 338 | function thrower(x) { 339 | throw x; 340 | } 341 | function callFetch(client, input, body, init, method) { 342 | if (!init) { 343 | init = {}; 344 | } 345 | init.method = method; 346 | if (body) { 347 | init.body = body; 348 | } 349 | return client.fetch(input, init); 350 | } 351 | 352 | exports.json = json; 353 | exports.retryStrategy = retryStrategy; 354 | exports.RetryInterceptor = RetryInterceptor; 355 | exports.HttpClientConfiguration = HttpClientConfiguration; 356 | exports.HttpClient = HttpClient; 357 | 358 | Object.defineProperty(exports, '__esModule', { value: true }); 359 | 360 | }); 361 | -------------------------------------------------------------------------------- /dist/aurelia-fetch-client.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A class for configuring HttpClients. 3 | */ 4 | export declare class HttpClientConfiguration { 5 | /** 6 | * The base URL to be prepended to each Request's url before sending. 7 | */ 8 | baseUrl: string; 9 | /** 10 | * Default values to apply to init objects when creating Requests. Note that 11 | * defaults cannot be applied when Request objects are manually created because 12 | * Request provides its own defaults and discards the original init object. 13 | * See also https://developer.mozilla.org/en-US/docs/Web/API/Request/Request 14 | */ 15 | defaults: RequestInit; 16 | /** 17 | * Interceptors to be added to the HttpClient. 18 | */ 19 | interceptors: Interceptor[]; 20 | /** 21 | * Sets the baseUrl. 22 | * 23 | * @param baseUrl The base URL. 24 | * @returns The chainable instance of this configuration object. 25 | * @chainable 26 | */ 27 | withBaseUrl(baseUrl: string): HttpClientConfiguration; 28 | /** 29 | * Sets the defaults. 30 | * 31 | * @param defaults The defaults. 32 | * @returns The chainable instance of this configuration object. 33 | * @chainable 34 | */ 35 | withDefaults(defaults: RequestInit): HttpClientConfiguration; 36 | /** 37 | * Adds an interceptor to be run on all requests or responses. 38 | * 39 | * @param interceptor An object with request, requestError, 40 | * response, or responseError methods. request and requestError act as 41 | * resolve and reject handlers for the Request before it is sent. 42 | * response and responseError act as resolve and reject handlers for 43 | * the Response after it has been received. 44 | * @returns The chainable instance of this configuration object. 45 | * @chainable 46 | */ 47 | withInterceptor(interceptor: Interceptor): HttpClientConfiguration; 48 | /** 49 | * Applies a configuration that addresses common application needs, including 50 | * configuring same-origin credentials, and using rejectErrorResponses. 51 | * @returns The chainable instance of this configuration object. 52 | * @chainable 53 | */ 54 | useStandardConfiguration(): HttpClientConfiguration; 55 | /** 56 | * Causes Responses whose status codes fall outside the range 200-299 to reject. 57 | * The fetch API only rejects on network errors or other conditions that prevent 58 | * the request from completing, meaning consumers must inspect Response.ok in the 59 | * Promise continuation to determine if the server responded with a success code. 60 | * This method adds a response interceptor that causes Responses with error codes 61 | * to be rejected, which is common behavior in HTTP client libraries. 62 | * @returns The chainable instance of this configuration object. 63 | * @chainable 64 | */ 65 | rejectErrorResponses(): HttpClientConfiguration; 66 | withRetry(config?: RetryConfiguration): HttpClientConfiguration; 67 | } 68 | /** 69 | * An HTTP client based on the Fetch API. 70 | */ 71 | export declare class HttpClient { 72 | /** 73 | * The current number of active requests. 74 | * Requests being processed by interceptors are considered active. 75 | */ 76 | activeRequestCount: number; 77 | /** 78 | * Indicates whether or not the client is currently making one or more requests. 79 | */ 80 | isRequesting: boolean; 81 | /** 82 | * Indicates whether or not the client has been configured. 83 | */ 84 | isConfigured: boolean; 85 | /** 86 | * The base URL set by the config. 87 | */ 88 | baseUrl: string; 89 | /** 90 | * The default request init to merge with values specified at request time. 91 | */ 92 | defaults: RequestInit; 93 | /** 94 | * The interceptors to be run during requests. 95 | */ 96 | interceptors: Interceptor[]; 97 | /** 98 | * Creates an instance of HttpClient. 99 | */ 100 | constructor(); 101 | /** 102 | * Configure this client with default settings to be used by all requests. 103 | * 104 | * @param config A configuration object, or a function that takes a config 105 | * object and configures it. 106 | * @returns The chainable instance of this HttpClient. 107 | * @chainable 108 | */ 109 | configure(config: RequestInit | ((config: HttpClientConfiguration) => void) | HttpClientConfiguration): HttpClient; 110 | /** 111 | * Starts the process of fetching a resource. Default configuration parameters 112 | * will be applied to the Request. The constructed Request will be passed to 113 | * registered request interceptors before being sent. The Response will be passed 114 | * to registered Response interceptors before it is returned. 115 | * 116 | * See also https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 117 | * 118 | * @param input The resource that you wish to fetch. Either a 119 | * Request object, or a string containing the URL of the resource. 120 | * @param init An options object containing settings to be applied to 121 | * the Request. 122 | * @returns A Promise for the Response from the fetch request. 123 | */ 124 | fetch(input: Request | string, init?: RequestInit): Promise; 125 | buildRequest(input: string | Request, init: RequestInit): Request; 126 | /** 127 | * Calls fetch as a GET request. 128 | * 129 | * @param input The resource that you wish to fetch. Either a 130 | * Request object, or a string containing the URL of the resource. 131 | * @param init An options object containing settings to be applied to 132 | * the Request. 133 | * @returns A Promise for the Response from the fetch request. 134 | */ 135 | get(input: Request | string, init?: RequestInit): Promise; 136 | /** 137 | * Calls fetch with request method set to POST. 138 | * 139 | * @param input The resource that you wish to fetch. Either a 140 | * Request object, or a string containing the URL of the resource. 141 | * @param body The body of the request. 142 | * @param init An options object containing settings to be applied to 143 | * the Request. 144 | * @returns A Promise for the Response from the fetch request. 145 | */ 146 | post(input: Request | string, body?: any, init?: RequestInit): Promise; 147 | /** 148 | * Calls fetch with request method set to PUT. 149 | * 150 | * @param input The resource that you wish to fetch. Either a 151 | * Request object, or a string containing the URL of the resource. 152 | * @param body The body of the request. 153 | * @param init An options object containing settings to be applied to 154 | * the Request. 155 | * @returns A Promise for the Response from the fetch request. 156 | */ 157 | put(input: Request | string, body?: any, init?: RequestInit): Promise; 158 | /** 159 | * Calls fetch with request method set to PATCH. 160 | * 161 | * @param input The resource that you wish to fetch. Either a 162 | * Request object, or a string containing the URL of the resource. 163 | * @param body The body of the request. 164 | * @param init An options object containing settings to be applied to 165 | * the Request. 166 | * @returns A Promise for the Response from the fetch request. 167 | */ 168 | patch(input: Request | string, body?: any, init?: RequestInit): Promise; 169 | /** 170 | * Calls fetch with request method set to DELETE. 171 | * 172 | * @param input The resource that you wish to fetch. Either a 173 | * Request object, or a string containing the URL of the resource. 174 | * @param body The body of the request. 175 | * @param init An options object containing settings to be applied to 176 | * the Request. 177 | * @returns A Promise for the Response from the fetch request. 178 | */ 179 | delete(input: Request | string, body?: any, init?: RequestInit): Promise; 180 | } 181 | /** 182 | * Interceptors can process requests before they are sent, and responses 183 | * before they are returned to callers. 184 | */ 185 | export interface Interceptor { 186 | /** 187 | * Called with the request before it is sent. Request interceptors can modify and 188 | * return the request, or return a new one to be sent. If desired, the interceptor 189 | * may return a Response in order to short-circuit the HTTP request itself. 190 | * 191 | * @param request The request to be sent. 192 | * @returns The existing request, a new request or a response; or a Promise for any of these. 193 | */ 194 | request?: (request: Request) => Request | Response | Promise; 195 | /** 196 | * Handles errors generated by previous request interceptors. This function acts 197 | * as a Promise rejection handler. It may rethrow the error to propagate the 198 | * failure, or return a new Request or Response to recover. 199 | * 200 | * @param error The rejection value from the previous interceptor. 201 | * @returns The existing request, a new request or a response; or a Promise for any of these. 202 | */ 203 | requestError?: (error: any) => Request | Response | Promise; 204 | /** 205 | * Called with the response after it is received. Response interceptors can modify 206 | * and return the Response, or create a new one to be returned to the caller. 207 | * 208 | * @param response The response. 209 | * @returns The response; or a Promise for one. 210 | */ 211 | response?: (response: Response, request?: Request) => Response | Promise; 212 | /** 213 | * Handles fetch errors and errors generated by previous interceptors. This 214 | * function acts as a Promise rejection handler. It may rethrow the error 215 | * to propagate the failure, or return a new Response to recover. 216 | * 217 | * @param error The rejection value from the fetch request or from a 218 | * previous interceptor. 219 | * @returns The response; or a Promise for one. 220 | */ 221 | responseError?: (error: any, request?: Request, httpClient?: HttpClient) => Response | Promise; 222 | } 223 | export declare type ValidInterceptorMethodName = keyof Interceptor; 224 | /** 225 | * The init object used to initialize a fetch Request. 226 | * See https://developer.mozilla.org/en-US/docs/Web/API/Request/Request 227 | */ 228 | export interface RequestInit { 229 | /** 230 | * The request method, e.g., GET, POST. 231 | */ 232 | method?: string; 233 | /** 234 | * Any headers you want to add to your request, contained within a Headers object or an object literal with ByteString values. 235 | */ 236 | headers?: Headers | Object; 237 | /** 238 | * Any body that you want to add to your request: this can be a Blob, BufferSource, FormData, 239 | * URLSearchParams, ReadableStream, or USVString object. 240 | * 241 | * Note that a request using the GET or HEAD method cannot have a body. 242 | */ 243 | body?: Blob | BufferSource | FormData | URLSearchParams | ReadableStream | string | null; 244 | /** 245 | * The mode you want to use for the request, e.g., cors, no-cors, same-origin, or navigate. 246 | * The default is cors. 247 | * 248 | * In Chrome the default is no-cors before Chrome 47 and same-origin starting with Chrome 47. 249 | */ 250 | mode?: string; 251 | /** 252 | * The request credentials you want to use for the request: omit, same-origin, or include. 253 | * The default is omit. 254 | * 255 | * In Chrome the default is same-origin before Chrome 47 and include starting with Chrome 47. 256 | */ 257 | credentials?: string; 258 | /** 259 | * The cache mode you want to use for the request: default, no-store, reload, no-cache, or force-cache. 260 | */ 261 | cache?: string; 262 | /** 263 | * The redirect mode to use: follow, error, or manual. 264 | * 265 | * In Chrome the default is follow before Chrome 47 and manual starting with Chrome 47. 266 | */ 267 | redirect?: string; 268 | /** 269 | * A USVString specifying no-referrer, client, or a URL. The default is client. 270 | */ 271 | referrer?: string; 272 | /** 273 | * Contains the subresource integrity value of the request (e.g., sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=). 274 | */ 275 | integrity?: string; 276 | /** 277 | * An AbortSignal to set request’s signal. 278 | */ 279 | signal?: AbortSignal | null; 280 | } 281 | export interface RetryConfiguration { 282 | maxRetries: number; 283 | interval?: number; 284 | strategy?: number | ((retryCount: number) => number); 285 | minRandomInterval?: number; 286 | maxRandomInterval?: number; 287 | counter?: number; 288 | requestClone?: Request; 289 | doRetry?: (response: Response, request: Request) => boolean | Promise; 290 | beforeRetry?: (request: Request, client: HttpClient) => Request | Promise; 291 | } 292 | /** 293 | * Serialize an object to JSON. Useful for easily creating JSON fetch request bodies. 294 | * 295 | * @param body The object to be serialized to JSON. 296 | * @param replacer The JSON.stringify replacer used when serializing. 297 | * @returns A JSON string. 298 | */ 299 | export declare function json(body: any, replacer?: any): string; 300 | export declare const retryStrategy: { 301 | fixed: 0; 302 | incremental: 1; 303 | exponential: 2; 304 | random: 3; 305 | }; 306 | export declare class RetryInterceptor implements Interceptor { 307 | retryConfig: RetryConfiguration; 308 | constructor(retryConfig?: RetryConfiguration); 309 | request(request: Request): Request; 310 | response(response: Response, request?: Request): Response; 311 | responseError(error: Response, request?: Request, httpClient?: HttpClient): Promise; 312 | } -------------------------------------------------------------------------------- /dist/commonjs/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var aureliaPal = require('aurelia-pal'); 6 | 7 | function json(body, replacer) { 8 | return JSON.stringify((body !== undefined ? body : {}), replacer); 9 | } 10 | 11 | var retryStrategy = { 12 | fixed: 0, 13 | incremental: 1, 14 | exponential: 2, 15 | random: 3 16 | }; 17 | var defaultRetryConfig = { 18 | maxRetries: 3, 19 | interval: 1000, 20 | strategy: retryStrategy.fixed 21 | }; 22 | var RetryInterceptor = (function () { 23 | function RetryInterceptor(retryConfig) { 24 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 25 | if (this.retryConfig.strategy === retryStrategy.exponential && 26 | this.retryConfig.interval <= 1000) { 27 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 28 | } 29 | } 30 | RetryInterceptor.prototype.request = function (request) { 31 | var $r = request; 32 | if (!$r.retryConfig) { 33 | $r.retryConfig = Object.assign({}, this.retryConfig); 34 | $r.retryConfig.counter = 0; 35 | } 36 | $r.retryConfig.requestClone = request.clone(); 37 | return request; 38 | }; 39 | RetryInterceptor.prototype.response = function (response, request) { 40 | delete request.retryConfig; 41 | return response; 42 | }; 43 | RetryInterceptor.prototype.responseError = function (error, request, httpClient) { 44 | var retryConfig = request.retryConfig; 45 | var requestClone = retryConfig.requestClone; 46 | return Promise.resolve().then(function () { 47 | if (retryConfig.counter < retryConfig.maxRetries) { 48 | var result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 49 | return Promise.resolve(result).then(function (doRetry) { 50 | if (doRetry) { 51 | retryConfig.counter++; 52 | return new Promise(function (resolve) { return aureliaPal.PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0); }) 53 | .then(function () { 54 | var newRequest = requestClone.clone(); 55 | if (typeof (retryConfig.beforeRetry) === 'function') { 56 | return retryConfig.beforeRetry(newRequest, httpClient); 57 | } 58 | return newRequest; 59 | }) 60 | .then(function (newRequest) { 61 | return httpClient.fetch(Object.assign(newRequest, { retryConfig: retryConfig })); 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | } 68 | delete request.retryConfig; 69 | throw error; 70 | }); 71 | }; 72 | return RetryInterceptor; 73 | }()); 74 | function calculateDelay(retryConfig) { 75 | var interval = retryConfig.interval, strategy = retryConfig.strategy, minRandomInterval = retryConfig.minRandomInterval, maxRandomInterval = retryConfig.maxRandomInterval, counter = retryConfig.counter; 76 | if (typeof (strategy) === 'function') { 77 | return retryConfig.strategy(counter); 78 | } 79 | switch (strategy) { 80 | case (retryStrategy.fixed): 81 | return retryStrategies[retryStrategy.fixed](interval); 82 | case (retryStrategy.incremental): 83 | return retryStrategies[retryStrategy.incremental](counter, interval); 84 | case (retryStrategy.exponential): 85 | return retryStrategies[retryStrategy.exponential](counter, interval); 86 | case (retryStrategy.random): 87 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 88 | default: 89 | throw new Error('Unrecognized retry strategy'); 90 | } 91 | } 92 | var retryStrategies = [ 93 | function (interval) { return interval; }, 94 | function (retryCount, interval) { return interval * retryCount; }, 95 | function (retryCount, interval) { return retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000; }, 96 | function (retryCount, interval, minRandomInterval, maxRandomInterval) { 97 | if (minRandomInterval === void 0) { minRandomInterval = 0; } 98 | if (maxRandomInterval === void 0) { maxRandomInterval = 60000; } 99 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 100 | } 101 | ]; 102 | 103 | var HttpClientConfiguration = (function () { 104 | function HttpClientConfiguration() { 105 | this.baseUrl = ''; 106 | this.defaults = {}; 107 | this.interceptors = []; 108 | } 109 | HttpClientConfiguration.prototype.withBaseUrl = function (baseUrl) { 110 | this.baseUrl = baseUrl; 111 | return this; 112 | }; 113 | HttpClientConfiguration.prototype.withDefaults = function (defaults) { 114 | this.defaults = defaults; 115 | return this; 116 | }; 117 | HttpClientConfiguration.prototype.withInterceptor = function (interceptor) { 118 | this.interceptors.push(interceptor); 119 | return this; 120 | }; 121 | HttpClientConfiguration.prototype.useStandardConfiguration = function () { 122 | var standardConfig = { credentials: 'same-origin' }; 123 | Object.assign(this.defaults, standardConfig, this.defaults); 124 | return this.rejectErrorResponses(); 125 | }; 126 | HttpClientConfiguration.prototype.rejectErrorResponses = function () { 127 | return this.withInterceptor({ response: rejectOnError }); 128 | }; 129 | HttpClientConfiguration.prototype.withRetry = function (config) { 130 | var interceptor = new RetryInterceptor(config); 131 | return this.withInterceptor(interceptor); 132 | }; 133 | return HttpClientConfiguration; 134 | }()); 135 | function rejectOnError(response) { 136 | if (!response.ok) { 137 | throw response; 138 | } 139 | return response; 140 | } 141 | 142 | var HttpClient = (function () { 143 | function HttpClient() { 144 | this.activeRequestCount = 0; 145 | this.isRequesting = false; 146 | this.isConfigured = false; 147 | this.baseUrl = ''; 148 | this.defaults = null; 149 | this.interceptors = []; 150 | if (typeof fetch === 'undefined') { 151 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 152 | } 153 | } 154 | HttpClient.prototype.configure = function (config) { 155 | var normalizedConfig; 156 | if (typeof config === 'object') { 157 | normalizedConfig = { defaults: config }; 158 | } 159 | else if (typeof config === 'function') { 160 | normalizedConfig = new HttpClientConfiguration(); 161 | normalizedConfig.baseUrl = this.baseUrl; 162 | normalizedConfig.defaults = Object.assign({}, this.defaults); 163 | normalizedConfig.interceptors = this.interceptors; 164 | var c = config(normalizedConfig); 165 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 166 | normalizedConfig = c; 167 | } 168 | } 169 | else { 170 | throw new Error('invalid config'); 171 | } 172 | var defaults = normalizedConfig.defaults; 173 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 174 | throw new Error('Default headers must be a plain object.'); 175 | } 176 | var interceptors = normalizedConfig.interceptors; 177 | if (interceptors && interceptors.length) { 178 | if (interceptors.filter(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }).length > 1) { 179 | throw new Error('Only one RetryInterceptor is allowed.'); 180 | } 181 | var retryInterceptorIndex = interceptors.findIndex(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }); 182 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 183 | throw new Error('The retry interceptor must be the last interceptor defined.'); 184 | } 185 | } 186 | this.baseUrl = normalizedConfig.baseUrl; 187 | this.defaults = defaults; 188 | this.interceptors = normalizedConfig.interceptors || []; 189 | this.isConfigured = true; 190 | return this; 191 | }; 192 | HttpClient.prototype.fetch = function (input, init) { 193 | var _this = this; 194 | trackRequestStart(this); 195 | var request = this.buildRequest(input, init); 196 | return processRequest(request, this.interceptors, this).then(function (result) { 197 | var response = null; 198 | if (Response.prototype.isPrototypeOf(result)) { 199 | response = Promise.resolve(result); 200 | } 201 | else if (Request.prototype.isPrototypeOf(result)) { 202 | request = result; 203 | response = fetch(result); 204 | } 205 | else { 206 | throw new Error("An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [" + result + "]"); 207 | } 208 | return processResponse(response, _this.interceptors, request, _this); 209 | }) 210 | .then(function (result) { 211 | if (Request.prototype.isPrototypeOf(result)) { 212 | return _this.fetch(result); 213 | } 214 | return result; 215 | }) 216 | .then(function (result) { 217 | trackRequestEnd(_this); 218 | return result; 219 | }, function (error) { 220 | trackRequestEnd(_this); 221 | throw error; 222 | }); 223 | }; 224 | HttpClient.prototype.buildRequest = function (input, init) { 225 | var defaults = this.defaults || {}; 226 | var request; 227 | var body; 228 | var requestContentType; 229 | var parsedDefaultHeaders = parseHeaderValues(defaults.headers); 230 | if (Request.prototype.isPrototypeOf(input)) { 231 | request = input; 232 | requestContentType = new Headers(request.headers).get('Content-Type'); 233 | } 234 | else { 235 | if (!init) { 236 | init = {}; 237 | } 238 | body = init.body; 239 | var bodyObj = body ? { body: body } : null; 240 | var requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 241 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 242 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 243 | } 244 | if (!requestContentType) { 245 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 246 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 247 | } 248 | else if (body && isJSON(body)) { 249 | request.headers.set('Content-Type', 'application/json'); 250 | } 251 | } 252 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 253 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 254 | request.headers.set('Content-Type', body.type); 255 | } 256 | return request; 257 | }; 258 | HttpClient.prototype.get = function (input, init) { 259 | return this.fetch(input, init); 260 | }; 261 | HttpClient.prototype.post = function (input, body, init) { 262 | return callFetch(this, input, body, init, 'POST'); 263 | }; 264 | HttpClient.prototype.put = function (input, body, init) { 265 | return callFetch(this, input, body, init, 'PUT'); 266 | }; 267 | HttpClient.prototype.patch = function (input, body, init) { 268 | return callFetch(this, input, body, init, 'PATCH'); 269 | }; 270 | HttpClient.prototype.delete = function (input, body, init) { 271 | return callFetch(this, input, body, init, 'DELETE'); 272 | }; 273 | return HttpClient; 274 | }()); 275 | var absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 276 | function trackRequestStart(client) { 277 | client.isRequesting = !!(++client.activeRequestCount); 278 | if (client.isRequesting) { 279 | var evt_1 = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 280 | setTimeout(function () { return aureliaPal.DOM.dispatchEvent(evt_1); }, 1); 281 | } 282 | } 283 | function trackRequestEnd(client) { 284 | client.isRequesting = !!(--client.activeRequestCount); 285 | if (!client.isRequesting) { 286 | var evt_2 = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 287 | setTimeout(function () { return aureliaPal.DOM.dispatchEvent(evt_2); }, 1); 288 | } 289 | } 290 | function parseHeaderValues(headers) { 291 | var parsedHeaders = {}; 292 | for (var name_1 in headers || {}) { 293 | if (headers.hasOwnProperty(name_1)) { 294 | parsedHeaders[name_1] = (typeof headers[name_1] === 'function') ? headers[name_1]() : headers[name_1]; 295 | } 296 | } 297 | return parsedHeaders; 298 | } 299 | function getRequestUrl(baseUrl, url) { 300 | if (absoluteUrlRegexp.test(url)) { 301 | return url; 302 | } 303 | return (baseUrl || '') + url; 304 | } 305 | function setDefaultHeaders(headers, defaultHeaders) { 306 | for (var name_2 in defaultHeaders || {}) { 307 | if (defaultHeaders.hasOwnProperty(name_2) && !headers.has(name_2)) { 308 | headers.set(name_2, defaultHeaders[name_2]); 309 | } 310 | } 311 | } 312 | function processRequest(request, interceptors, http) { 313 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 314 | } 315 | function processResponse(response, interceptors, request, http) { 316 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 317 | } 318 | function applyInterceptors(input, interceptors, successName, errorName) { 319 | var interceptorArgs = []; 320 | for (var _i = 4; _i < arguments.length; _i++) { 321 | interceptorArgs[_i - 4] = arguments[_i]; 322 | } 323 | return (interceptors || []) 324 | .reduce(function (chain, interceptor) { 325 | var successHandler = interceptor[successName]; 326 | var errorHandler = interceptor[errorName]; 327 | return chain.then(successHandler && (function (value) { return successHandler.call.apply(successHandler, [interceptor, value].concat(interceptorArgs)); }) || identity, errorHandler && (function (reason) { return errorHandler.call.apply(errorHandler, [interceptor, reason].concat(interceptorArgs)); }) || thrower); 328 | }, Promise.resolve(input)); 329 | } 330 | function isJSON(str) { 331 | try { 332 | JSON.parse(str); 333 | } 334 | catch (err) { 335 | return false; 336 | } 337 | return true; 338 | } 339 | function identity(x) { 340 | return x; 341 | } 342 | function thrower(x) { 343 | throw x; 344 | } 345 | function callFetch(client, input, body, init, method) { 346 | if (!init) { 347 | init = {}; 348 | } 349 | init.method = method; 350 | if (body) { 351 | init.body = body; 352 | } 353 | return client.fetch(input, init); 354 | } 355 | 356 | exports.json = json; 357 | exports.retryStrategy = retryStrategy; 358 | exports.RetryInterceptor = RetryInterceptor; 359 | exports.HttpClientConfiguration = HttpClientConfiguration; 360 | exports.HttpClient = HttpClient; 361 | -------------------------------------------------------------------------------- /dist/es2015/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | import { PLATFORM, DOM } from 'aurelia-pal'; 2 | 3 | function json(body, replacer) { 4 | return JSON.stringify((body !== undefined ? body : {}), replacer); 5 | } 6 | 7 | const retryStrategy = { 8 | fixed: 0, 9 | incremental: 1, 10 | exponential: 2, 11 | random: 3 12 | }; 13 | const defaultRetryConfig = { 14 | maxRetries: 3, 15 | interval: 1000, 16 | strategy: retryStrategy.fixed 17 | }; 18 | class RetryInterceptor { 19 | constructor(retryConfig) { 20 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 21 | if (this.retryConfig.strategy === retryStrategy.exponential && 22 | this.retryConfig.interval <= 1000) { 23 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 24 | } 25 | } 26 | request(request) { 27 | let $r = request; 28 | if (!$r.retryConfig) { 29 | $r.retryConfig = Object.assign({}, this.retryConfig); 30 | $r.retryConfig.counter = 0; 31 | } 32 | $r.retryConfig.requestClone = request.clone(); 33 | return request; 34 | } 35 | response(response, request) { 36 | delete request.retryConfig; 37 | return response; 38 | } 39 | responseError(error, request, httpClient) { 40 | const { retryConfig } = request; 41 | const { requestClone } = retryConfig; 42 | return Promise.resolve().then(() => { 43 | if (retryConfig.counter < retryConfig.maxRetries) { 44 | const result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 45 | return Promise.resolve(result).then(doRetry => { 46 | if (doRetry) { 47 | retryConfig.counter++; 48 | return new Promise(resolve => PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0)) 49 | .then(() => { 50 | let newRequest = requestClone.clone(); 51 | if (typeof (retryConfig.beforeRetry) === 'function') { 52 | return retryConfig.beforeRetry(newRequest, httpClient); 53 | } 54 | return newRequest; 55 | }) 56 | .then(newRequest => { 57 | return httpClient.fetch(Object.assign(newRequest, { retryConfig })); 58 | }); 59 | } 60 | delete request.retryConfig; 61 | throw error; 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | } 68 | } 69 | function calculateDelay(retryConfig) { 70 | const { interval, strategy, minRandomInterval, maxRandomInterval, counter } = retryConfig; 71 | if (typeof (strategy) === 'function') { 72 | return retryConfig.strategy(counter); 73 | } 74 | switch (strategy) { 75 | case (retryStrategy.fixed): 76 | return retryStrategies[retryStrategy.fixed](interval); 77 | case (retryStrategy.incremental): 78 | return retryStrategies[retryStrategy.incremental](counter, interval); 79 | case (retryStrategy.exponential): 80 | return retryStrategies[retryStrategy.exponential](counter, interval); 81 | case (retryStrategy.random): 82 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 83 | default: 84 | throw new Error('Unrecognized retry strategy'); 85 | } 86 | } 87 | const retryStrategies = [ 88 | interval => interval, 89 | (retryCount, interval) => interval * retryCount, 90 | (retryCount, interval) => retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000, 91 | (retryCount, interval, minRandomInterval = 0, maxRandomInterval = 60000) => { 92 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 93 | } 94 | ]; 95 | 96 | class HttpClientConfiguration { 97 | constructor() { 98 | this.baseUrl = ''; 99 | this.defaults = {}; 100 | this.interceptors = []; 101 | } 102 | withBaseUrl(baseUrl) { 103 | this.baseUrl = baseUrl; 104 | return this; 105 | } 106 | withDefaults(defaults) { 107 | this.defaults = defaults; 108 | return this; 109 | } 110 | withInterceptor(interceptor) { 111 | this.interceptors.push(interceptor); 112 | return this; 113 | } 114 | useStandardConfiguration() { 115 | let standardConfig = { credentials: 'same-origin' }; 116 | Object.assign(this.defaults, standardConfig, this.defaults); 117 | return this.rejectErrorResponses(); 118 | } 119 | rejectErrorResponses() { 120 | return this.withInterceptor({ response: rejectOnError }); 121 | } 122 | withRetry(config) { 123 | const interceptor = new RetryInterceptor(config); 124 | return this.withInterceptor(interceptor); 125 | } 126 | } 127 | function rejectOnError(response) { 128 | if (!response.ok) { 129 | throw response; 130 | } 131 | return response; 132 | } 133 | 134 | class HttpClient { 135 | constructor() { 136 | this.activeRequestCount = 0; 137 | this.isRequesting = false; 138 | this.isConfigured = false; 139 | this.baseUrl = ''; 140 | this.defaults = null; 141 | this.interceptors = []; 142 | if (typeof fetch === 'undefined') { 143 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 144 | } 145 | } 146 | configure(config) { 147 | let normalizedConfig; 148 | if (typeof config === 'object') { 149 | normalizedConfig = { defaults: config }; 150 | } 151 | else if (typeof config === 'function') { 152 | normalizedConfig = new HttpClientConfiguration(); 153 | normalizedConfig.baseUrl = this.baseUrl; 154 | normalizedConfig.defaults = Object.assign({}, this.defaults); 155 | normalizedConfig.interceptors = this.interceptors; 156 | let c = config(normalizedConfig); 157 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 158 | normalizedConfig = c; 159 | } 160 | } 161 | else { 162 | throw new Error('invalid config'); 163 | } 164 | let defaults = normalizedConfig.defaults; 165 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 166 | throw new Error('Default headers must be a plain object.'); 167 | } 168 | let interceptors = normalizedConfig.interceptors; 169 | if (interceptors && interceptors.length) { 170 | if (interceptors.filter(x => RetryInterceptor.prototype.isPrototypeOf(x)).length > 1) { 171 | throw new Error('Only one RetryInterceptor is allowed.'); 172 | } 173 | const retryInterceptorIndex = interceptors.findIndex(x => RetryInterceptor.prototype.isPrototypeOf(x)); 174 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 175 | throw new Error('The retry interceptor must be the last interceptor defined.'); 176 | } 177 | } 178 | this.baseUrl = normalizedConfig.baseUrl; 179 | this.defaults = defaults; 180 | this.interceptors = normalizedConfig.interceptors || []; 181 | this.isConfigured = true; 182 | return this; 183 | } 184 | fetch(input, init) { 185 | trackRequestStart(this); 186 | let request = this.buildRequest(input, init); 187 | return processRequest(request, this.interceptors, this).then(result => { 188 | let response = null; 189 | if (Response.prototype.isPrototypeOf(result)) { 190 | response = Promise.resolve(result); 191 | } 192 | else if (Request.prototype.isPrototypeOf(result)) { 193 | request = result; 194 | response = fetch(result); 195 | } 196 | else { 197 | throw new Error(`An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [${result}]`); 198 | } 199 | return processResponse(response, this.interceptors, request, this); 200 | }) 201 | .then(result => { 202 | if (Request.prototype.isPrototypeOf(result)) { 203 | return this.fetch(result); 204 | } 205 | return result; 206 | }) 207 | .then(result => { 208 | trackRequestEnd(this); 209 | return result; 210 | }, error => { 211 | trackRequestEnd(this); 212 | throw error; 213 | }); 214 | } 215 | buildRequest(input, init) { 216 | let defaults = this.defaults || {}; 217 | let request; 218 | let body; 219 | let requestContentType; 220 | let parsedDefaultHeaders = parseHeaderValues(defaults.headers); 221 | if (Request.prototype.isPrototypeOf(input)) { 222 | request = input; 223 | requestContentType = new Headers(request.headers).get('Content-Type'); 224 | } 225 | else { 226 | if (!init) { 227 | init = {}; 228 | } 229 | body = init.body; 230 | let bodyObj = body ? { body } : null; 231 | let requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 232 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 233 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 234 | } 235 | if (!requestContentType) { 236 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 237 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 238 | } 239 | else if (body && isJSON(body)) { 240 | request.headers.set('Content-Type', 'application/json'); 241 | } 242 | } 243 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 244 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 245 | request.headers.set('Content-Type', body.type); 246 | } 247 | return request; 248 | } 249 | get(input, init) { 250 | return this.fetch(input, init); 251 | } 252 | post(input, body, init) { 253 | return callFetch(this, input, body, init, 'POST'); 254 | } 255 | put(input, body, init) { 256 | return callFetch(this, input, body, init, 'PUT'); 257 | } 258 | patch(input, body, init) { 259 | return callFetch(this, input, body, init, 'PATCH'); 260 | } 261 | delete(input, body, init) { 262 | return callFetch(this, input, body, init, 'DELETE'); 263 | } 264 | } 265 | const absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 266 | function trackRequestStart(client) { 267 | client.isRequesting = !!(++client.activeRequestCount); 268 | if (client.isRequesting) { 269 | let evt = DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 270 | setTimeout(() => DOM.dispatchEvent(evt), 1); 271 | } 272 | } 273 | function trackRequestEnd(client) { 274 | client.isRequesting = !!(--client.activeRequestCount); 275 | if (!client.isRequesting) { 276 | let evt = DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 277 | setTimeout(() => DOM.dispatchEvent(evt), 1); 278 | } 279 | } 280 | function parseHeaderValues(headers) { 281 | let parsedHeaders = {}; 282 | for (let name in headers || {}) { 283 | if (headers.hasOwnProperty(name)) { 284 | parsedHeaders[name] = (typeof headers[name] === 'function') ? headers[name]() : headers[name]; 285 | } 286 | } 287 | return parsedHeaders; 288 | } 289 | function getRequestUrl(baseUrl, url) { 290 | if (absoluteUrlRegexp.test(url)) { 291 | return url; 292 | } 293 | return (baseUrl || '') + url; 294 | } 295 | function setDefaultHeaders(headers, defaultHeaders) { 296 | for (let name in defaultHeaders || {}) { 297 | if (defaultHeaders.hasOwnProperty(name) && !headers.has(name)) { 298 | headers.set(name, defaultHeaders[name]); 299 | } 300 | } 301 | } 302 | function processRequest(request, interceptors, http) { 303 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 304 | } 305 | function processResponse(response, interceptors, request, http) { 306 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 307 | } 308 | function applyInterceptors(input, interceptors, successName, errorName, ...interceptorArgs) { 309 | return (interceptors || []) 310 | .reduce((chain, interceptor) => { 311 | let successHandler = interceptor[successName]; 312 | let errorHandler = interceptor[errorName]; 313 | return chain.then(successHandler && (value => successHandler.call(interceptor, value, ...interceptorArgs)) || identity, errorHandler && (reason => errorHandler.call(interceptor, reason, ...interceptorArgs)) || thrower); 314 | }, Promise.resolve(input)); 315 | } 316 | function isJSON(str) { 317 | try { 318 | JSON.parse(str); 319 | } 320 | catch (err) { 321 | return false; 322 | } 323 | return true; 324 | } 325 | function identity(x) { 326 | return x; 327 | } 328 | function thrower(x) { 329 | throw x; 330 | } 331 | function callFetch(client, input, body, init, method) { 332 | if (!init) { 333 | init = {}; 334 | } 335 | init.method = method; 336 | if (body) { 337 | init.body = body; 338 | } 339 | return client.fetch(input, init); 340 | } 341 | 342 | export { json, retryStrategy, RetryInterceptor, HttpClientConfiguration, HttpClient }; 343 | -------------------------------------------------------------------------------- /dist/es2017/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | import { PLATFORM, DOM } from 'aurelia-pal'; 2 | 3 | function json(body, replacer) { 4 | return JSON.stringify((body !== undefined ? body : {}), replacer); 5 | } 6 | 7 | const retryStrategy = { 8 | fixed: 0, 9 | incremental: 1, 10 | exponential: 2, 11 | random: 3 12 | }; 13 | const defaultRetryConfig = { 14 | maxRetries: 3, 15 | interval: 1000, 16 | strategy: retryStrategy.fixed 17 | }; 18 | class RetryInterceptor { 19 | constructor(retryConfig) { 20 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 21 | if (this.retryConfig.strategy === retryStrategy.exponential && 22 | this.retryConfig.interval <= 1000) { 23 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 24 | } 25 | } 26 | request(request) { 27 | let $r = request; 28 | if (!$r.retryConfig) { 29 | $r.retryConfig = Object.assign({}, this.retryConfig); 30 | $r.retryConfig.counter = 0; 31 | } 32 | $r.retryConfig.requestClone = request.clone(); 33 | return request; 34 | } 35 | response(response, request) { 36 | delete request.retryConfig; 37 | return response; 38 | } 39 | responseError(error, request, httpClient) { 40 | const { retryConfig } = request; 41 | const { requestClone } = retryConfig; 42 | return Promise.resolve().then(() => { 43 | if (retryConfig.counter < retryConfig.maxRetries) { 44 | const result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 45 | return Promise.resolve(result).then(doRetry => { 46 | if (doRetry) { 47 | retryConfig.counter++; 48 | return new Promise(resolve => PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0)) 49 | .then(() => { 50 | let newRequest = requestClone.clone(); 51 | if (typeof (retryConfig.beforeRetry) === 'function') { 52 | return retryConfig.beforeRetry(newRequest, httpClient); 53 | } 54 | return newRequest; 55 | }) 56 | .then(newRequest => { 57 | return httpClient.fetch(Object.assign(newRequest, { retryConfig })); 58 | }); 59 | } 60 | delete request.retryConfig; 61 | throw error; 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | } 68 | } 69 | function calculateDelay(retryConfig) { 70 | const { interval, strategy, minRandomInterval, maxRandomInterval, counter } = retryConfig; 71 | if (typeof (strategy) === 'function') { 72 | return retryConfig.strategy(counter); 73 | } 74 | switch (strategy) { 75 | case (retryStrategy.fixed): 76 | return retryStrategies[retryStrategy.fixed](interval); 77 | case (retryStrategy.incremental): 78 | return retryStrategies[retryStrategy.incremental](counter, interval); 79 | case (retryStrategy.exponential): 80 | return retryStrategies[retryStrategy.exponential](counter, interval); 81 | case (retryStrategy.random): 82 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 83 | default: 84 | throw new Error('Unrecognized retry strategy'); 85 | } 86 | } 87 | const retryStrategies = [ 88 | interval => interval, 89 | (retryCount, interval) => interval * retryCount, 90 | (retryCount, interval) => retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000, 91 | (retryCount, interval, minRandomInterval = 0, maxRandomInterval = 60000) => { 92 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 93 | } 94 | ]; 95 | 96 | class HttpClientConfiguration { 97 | constructor() { 98 | this.baseUrl = ''; 99 | this.defaults = {}; 100 | this.interceptors = []; 101 | } 102 | withBaseUrl(baseUrl) { 103 | this.baseUrl = baseUrl; 104 | return this; 105 | } 106 | withDefaults(defaults) { 107 | this.defaults = defaults; 108 | return this; 109 | } 110 | withInterceptor(interceptor) { 111 | this.interceptors.push(interceptor); 112 | return this; 113 | } 114 | useStandardConfiguration() { 115 | let standardConfig = { credentials: 'same-origin' }; 116 | Object.assign(this.defaults, standardConfig, this.defaults); 117 | return this.rejectErrorResponses(); 118 | } 119 | rejectErrorResponses() { 120 | return this.withInterceptor({ response: rejectOnError }); 121 | } 122 | withRetry(config) { 123 | const interceptor = new RetryInterceptor(config); 124 | return this.withInterceptor(interceptor); 125 | } 126 | } 127 | function rejectOnError(response) { 128 | if (!response.ok) { 129 | throw response; 130 | } 131 | return response; 132 | } 133 | 134 | class HttpClient { 135 | constructor() { 136 | this.activeRequestCount = 0; 137 | this.isRequesting = false; 138 | this.isConfigured = false; 139 | this.baseUrl = ''; 140 | this.defaults = null; 141 | this.interceptors = []; 142 | if (typeof fetch === 'undefined') { 143 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 144 | } 145 | } 146 | configure(config) { 147 | let normalizedConfig; 148 | if (typeof config === 'object') { 149 | normalizedConfig = { defaults: config }; 150 | } 151 | else if (typeof config === 'function') { 152 | normalizedConfig = new HttpClientConfiguration(); 153 | normalizedConfig.baseUrl = this.baseUrl; 154 | normalizedConfig.defaults = Object.assign({}, this.defaults); 155 | normalizedConfig.interceptors = this.interceptors; 156 | let c = config(normalizedConfig); 157 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 158 | normalizedConfig = c; 159 | } 160 | } 161 | else { 162 | throw new Error('invalid config'); 163 | } 164 | let defaults = normalizedConfig.defaults; 165 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 166 | throw new Error('Default headers must be a plain object.'); 167 | } 168 | let interceptors = normalizedConfig.interceptors; 169 | if (interceptors && interceptors.length) { 170 | if (interceptors.filter(x => RetryInterceptor.prototype.isPrototypeOf(x)).length > 1) { 171 | throw new Error('Only one RetryInterceptor is allowed.'); 172 | } 173 | const retryInterceptorIndex = interceptors.findIndex(x => RetryInterceptor.prototype.isPrototypeOf(x)); 174 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 175 | throw new Error('The retry interceptor must be the last interceptor defined.'); 176 | } 177 | } 178 | this.baseUrl = normalizedConfig.baseUrl; 179 | this.defaults = defaults; 180 | this.interceptors = normalizedConfig.interceptors || []; 181 | this.isConfigured = true; 182 | return this; 183 | } 184 | fetch(input, init) { 185 | trackRequestStart(this); 186 | let request = this.buildRequest(input, init); 187 | return processRequest(request, this.interceptors, this).then(result => { 188 | let response = null; 189 | if (Response.prototype.isPrototypeOf(result)) { 190 | response = Promise.resolve(result); 191 | } 192 | else if (Request.prototype.isPrototypeOf(result)) { 193 | request = result; 194 | response = fetch(result); 195 | } 196 | else { 197 | throw new Error(`An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [${result}]`); 198 | } 199 | return processResponse(response, this.interceptors, request, this); 200 | }) 201 | .then(result => { 202 | if (Request.prototype.isPrototypeOf(result)) { 203 | return this.fetch(result); 204 | } 205 | return result; 206 | }) 207 | .then(result => { 208 | trackRequestEnd(this); 209 | return result; 210 | }, error => { 211 | trackRequestEnd(this); 212 | throw error; 213 | }); 214 | } 215 | buildRequest(input, init) { 216 | let defaults = this.defaults || {}; 217 | let request; 218 | let body; 219 | let requestContentType; 220 | let parsedDefaultHeaders = parseHeaderValues(defaults.headers); 221 | if (Request.prototype.isPrototypeOf(input)) { 222 | request = input; 223 | requestContentType = new Headers(request.headers).get('Content-Type'); 224 | } 225 | else { 226 | if (!init) { 227 | init = {}; 228 | } 229 | body = init.body; 230 | let bodyObj = body ? { body } : null; 231 | let requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 232 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 233 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 234 | } 235 | if (!requestContentType) { 236 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 237 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 238 | } 239 | else if (body && isJSON(body)) { 240 | request.headers.set('Content-Type', 'application/json'); 241 | } 242 | } 243 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 244 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 245 | request.headers.set('Content-Type', body.type); 246 | } 247 | return request; 248 | } 249 | get(input, init) { 250 | return this.fetch(input, init); 251 | } 252 | post(input, body, init) { 253 | return callFetch(this, input, body, init, 'POST'); 254 | } 255 | put(input, body, init) { 256 | return callFetch(this, input, body, init, 'PUT'); 257 | } 258 | patch(input, body, init) { 259 | return callFetch(this, input, body, init, 'PATCH'); 260 | } 261 | delete(input, body, init) { 262 | return callFetch(this, input, body, init, 'DELETE'); 263 | } 264 | } 265 | const absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 266 | function trackRequestStart(client) { 267 | client.isRequesting = !!(++client.activeRequestCount); 268 | if (client.isRequesting) { 269 | let evt = DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 270 | setTimeout(() => DOM.dispatchEvent(evt), 1); 271 | } 272 | } 273 | function trackRequestEnd(client) { 274 | client.isRequesting = !!(--client.activeRequestCount); 275 | if (!client.isRequesting) { 276 | let evt = DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 277 | setTimeout(() => DOM.dispatchEvent(evt), 1); 278 | } 279 | } 280 | function parseHeaderValues(headers) { 281 | let parsedHeaders = {}; 282 | for (let name in headers || {}) { 283 | if (headers.hasOwnProperty(name)) { 284 | parsedHeaders[name] = (typeof headers[name] === 'function') ? headers[name]() : headers[name]; 285 | } 286 | } 287 | return parsedHeaders; 288 | } 289 | function getRequestUrl(baseUrl, url) { 290 | if (absoluteUrlRegexp.test(url)) { 291 | return url; 292 | } 293 | return (baseUrl || '') + url; 294 | } 295 | function setDefaultHeaders(headers, defaultHeaders) { 296 | for (let name in defaultHeaders || {}) { 297 | if (defaultHeaders.hasOwnProperty(name) && !headers.has(name)) { 298 | headers.set(name, defaultHeaders[name]); 299 | } 300 | } 301 | } 302 | function processRequest(request, interceptors, http) { 303 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 304 | } 305 | function processResponse(response, interceptors, request, http) { 306 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 307 | } 308 | function applyInterceptors(input, interceptors, successName, errorName, ...interceptorArgs) { 309 | return (interceptors || []) 310 | .reduce((chain, interceptor) => { 311 | let successHandler = interceptor[successName]; 312 | let errorHandler = interceptor[errorName]; 313 | return chain.then(successHandler && (value => successHandler.call(interceptor, value, ...interceptorArgs)) || identity, errorHandler && (reason => errorHandler.call(interceptor, reason, ...interceptorArgs)) || thrower); 314 | }, Promise.resolve(input)); 315 | } 316 | function isJSON(str) { 317 | try { 318 | JSON.parse(str); 319 | } 320 | catch (err) { 321 | return false; 322 | } 323 | return true; 324 | } 325 | function identity(x) { 326 | return x; 327 | } 328 | function thrower(x) { 329 | throw x; 330 | } 331 | function callFetch(client, input, body, init, method) { 332 | if (!init) { 333 | init = {}; 334 | } 335 | init.method = method; 336 | if (body) { 337 | init.body = body; 338 | } 339 | return client.fetch(input, init); 340 | } 341 | 342 | export { json, retryStrategy, RetryInterceptor, HttpClientConfiguration, HttpClient }; 343 | -------------------------------------------------------------------------------- /dist/native-modules/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | import { PLATFORM, DOM } from 'aurelia-pal'; 2 | 3 | function json(body, replacer) { 4 | return JSON.stringify((body !== undefined ? body : {}), replacer); 5 | } 6 | 7 | var retryStrategy = { 8 | fixed: 0, 9 | incremental: 1, 10 | exponential: 2, 11 | random: 3 12 | }; 13 | var defaultRetryConfig = { 14 | maxRetries: 3, 15 | interval: 1000, 16 | strategy: retryStrategy.fixed 17 | }; 18 | var RetryInterceptor = (function () { 19 | function RetryInterceptor(retryConfig) { 20 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 21 | if (this.retryConfig.strategy === retryStrategy.exponential && 22 | this.retryConfig.interval <= 1000) { 23 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 24 | } 25 | } 26 | RetryInterceptor.prototype.request = function (request) { 27 | var $r = request; 28 | if (!$r.retryConfig) { 29 | $r.retryConfig = Object.assign({}, this.retryConfig); 30 | $r.retryConfig.counter = 0; 31 | } 32 | $r.retryConfig.requestClone = request.clone(); 33 | return request; 34 | }; 35 | RetryInterceptor.prototype.response = function (response, request) { 36 | delete request.retryConfig; 37 | return response; 38 | }; 39 | RetryInterceptor.prototype.responseError = function (error, request, httpClient) { 40 | var retryConfig = request.retryConfig; 41 | var requestClone = retryConfig.requestClone; 42 | return Promise.resolve().then(function () { 43 | if (retryConfig.counter < retryConfig.maxRetries) { 44 | var result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 45 | return Promise.resolve(result).then(function (doRetry) { 46 | if (doRetry) { 47 | retryConfig.counter++; 48 | return new Promise(function (resolve) { return PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0); }) 49 | .then(function () { 50 | var newRequest = requestClone.clone(); 51 | if (typeof (retryConfig.beforeRetry) === 'function') { 52 | return retryConfig.beforeRetry(newRequest, httpClient); 53 | } 54 | return newRequest; 55 | }) 56 | .then(function (newRequest) { 57 | return httpClient.fetch(Object.assign(newRequest, { retryConfig: retryConfig })); 58 | }); 59 | } 60 | delete request.retryConfig; 61 | throw error; 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | }; 68 | return RetryInterceptor; 69 | }()); 70 | function calculateDelay(retryConfig) { 71 | var interval = retryConfig.interval, strategy = retryConfig.strategy, minRandomInterval = retryConfig.minRandomInterval, maxRandomInterval = retryConfig.maxRandomInterval, counter = retryConfig.counter; 72 | if (typeof (strategy) === 'function') { 73 | return retryConfig.strategy(counter); 74 | } 75 | switch (strategy) { 76 | case (retryStrategy.fixed): 77 | return retryStrategies[retryStrategy.fixed](interval); 78 | case (retryStrategy.incremental): 79 | return retryStrategies[retryStrategy.incremental](counter, interval); 80 | case (retryStrategy.exponential): 81 | return retryStrategies[retryStrategy.exponential](counter, interval); 82 | case (retryStrategy.random): 83 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 84 | default: 85 | throw new Error('Unrecognized retry strategy'); 86 | } 87 | } 88 | var retryStrategies = [ 89 | function (interval) { return interval; }, 90 | function (retryCount, interval) { return interval * retryCount; }, 91 | function (retryCount, interval) { return retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000; }, 92 | function (retryCount, interval, minRandomInterval, maxRandomInterval) { 93 | if (minRandomInterval === void 0) { minRandomInterval = 0; } 94 | if (maxRandomInterval === void 0) { maxRandomInterval = 60000; } 95 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 96 | } 97 | ]; 98 | 99 | var HttpClientConfiguration = (function () { 100 | function HttpClientConfiguration() { 101 | this.baseUrl = ''; 102 | this.defaults = {}; 103 | this.interceptors = []; 104 | } 105 | HttpClientConfiguration.prototype.withBaseUrl = function (baseUrl) { 106 | this.baseUrl = baseUrl; 107 | return this; 108 | }; 109 | HttpClientConfiguration.prototype.withDefaults = function (defaults) { 110 | this.defaults = defaults; 111 | return this; 112 | }; 113 | HttpClientConfiguration.prototype.withInterceptor = function (interceptor) { 114 | this.interceptors.push(interceptor); 115 | return this; 116 | }; 117 | HttpClientConfiguration.prototype.useStandardConfiguration = function () { 118 | var standardConfig = { credentials: 'same-origin' }; 119 | Object.assign(this.defaults, standardConfig, this.defaults); 120 | return this.rejectErrorResponses(); 121 | }; 122 | HttpClientConfiguration.prototype.rejectErrorResponses = function () { 123 | return this.withInterceptor({ response: rejectOnError }); 124 | }; 125 | HttpClientConfiguration.prototype.withRetry = function (config) { 126 | var interceptor = new RetryInterceptor(config); 127 | return this.withInterceptor(interceptor); 128 | }; 129 | return HttpClientConfiguration; 130 | }()); 131 | function rejectOnError(response) { 132 | if (!response.ok) { 133 | throw response; 134 | } 135 | return response; 136 | } 137 | 138 | var HttpClient = (function () { 139 | function HttpClient() { 140 | this.activeRequestCount = 0; 141 | this.isRequesting = false; 142 | this.isConfigured = false; 143 | this.baseUrl = ''; 144 | this.defaults = null; 145 | this.interceptors = []; 146 | if (typeof fetch === 'undefined') { 147 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 148 | } 149 | } 150 | HttpClient.prototype.configure = function (config) { 151 | var normalizedConfig; 152 | if (typeof config === 'object') { 153 | normalizedConfig = { defaults: config }; 154 | } 155 | else if (typeof config === 'function') { 156 | normalizedConfig = new HttpClientConfiguration(); 157 | normalizedConfig.baseUrl = this.baseUrl; 158 | normalizedConfig.defaults = Object.assign({}, this.defaults); 159 | normalizedConfig.interceptors = this.interceptors; 160 | var c = config(normalizedConfig); 161 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 162 | normalizedConfig = c; 163 | } 164 | } 165 | else { 166 | throw new Error('invalid config'); 167 | } 168 | var defaults = normalizedConfig.defaults; 169 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 170 | throw new Error('Default headers must be a plain object.'); 171 | } 172 | var interceptors = normalizedConfig.interceptors; 173 | if (interceptors && interceptors.length) { 174 | if (interceptors.filter(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }).length > 1) { 175 | throw new Error('Only one RetryInterceptor is allowed.'); 176 | } 177 | var retryInterceptorIndex = interceptors.findIndex(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }); 178 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 179 | throw new Error('The retry interceptor must be the last interceptor defined.'); 180 | } 181 | } 182 | this.baseUrl = normalizedConfig.baseUrl; 183 | this.defaults = defaults; 184 | this.interceptors = normalizedConfig.interceptors || []; 185 | this.isConfigured = true; 186 | return this; 187 | }; 188 | HttpClient.prototype.fetch = function (input, init) { 189 | var _this = this; 190 | trackRequestStart(this); 191 | var request = this.buildRequest(input, init); 192 | return processRequest(request, this.interceptors, this).then(function (result) { 193 | var response = null; 194 | if (Response.prototype.isPrototypeOf(result)) { 195 | response = Promise.resolve(result); 196 | } 197 | else if (Request.prototype.isPrototypeOf(result)) { 198 | request = result; 199 | response = fetch(result); 200 | } 201 | else { 202 | throw new Error("An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [" + result + "]"); 203 | } 204 | return processResponse(response, _this.interceptors, request, _this); 205 | }) 206 | .then(function (result) { 207 | if (Request.prototype.isPrototypeOf(result)) { 208 | return _this.fetch(result); 209 | } 210 | return result; 211 | }) 212 | .then(function (result) { 213 | trackRequestEnd(_this); 214 | return result; 215 | }, function (error) { 216 | trackRequestEnd(_this); 217 | throw error; 218 | }); 219 | }; 220 | HttpClient.prototype.buildRequest = function (input, init) { 221 | var defaults = this.defaults || {}; 222 | var request; 223 | var body; 224 | var requestContentType; 225 | var parsedDefaultHeaders = parseHeaderValues(defaults.headers); 226 | if (Request.prototype.isPrototypeOf(input)) { 227 | request = input; 228 | requestContentType = new Headers(request.headers).get('Content-Type'); 229 | } 230 | else { 231 | if (!init) { 232 | init = {}; 233 | } 234 | body = init.body; 235 | var bodyObj = body ? { body: body } : null; 236 | var requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 237 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 238 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 239 | } 240 | if (!requestContentType) { 241 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 242 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 243 | } 244 | else if (body && isJSON(body)) { 245 | request.headers.set('Content-Type', 'application/json'); 246 | } 247 | } 248 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 249 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 250 | request.headers.set('Content-Type', body.type); 251 | } 252 | return request; 253 | }; 254 | HttpClient.prototype.get = function (input, init) { 255 | return this.fetch(input, init); 256 | }; 257 | HttpClient.prototype.post = function (input, body, init) { 258 | return callFetch(this, input, body, init, 'POST'); 259 | }; 260 | HttpClient.prototype.put = function (input, body, init) { 261 | return callFetch(this, input, body, init, 'PUT'); 262 | }; 263 | HttpClient.prototype.patch = function (input, body, init) { 264 | return callFetch(this, input, body, init, 'PATCH'); 265 | }; 266 | HttpClient.prototype.delete = function (input, body, init) { 267 | return callFetch(this, input, body, init, 'DELETE'); 268 | }; 269 | return HttpClient; 270 | }()); 271 | var absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 272 | function trackRequestStart(client) { 273 | client.isRequesting = !!(++client.activeRequestCount); 274 | if (client.isRequesting) { 275 | var evt_1 = DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 276 | setTimeout(function () { return DOM.dispatchEvent(evt_1); }, 1); 277 | } 278 | } 279 | function trackRequestEnd(client) { 280 | client.isRequesting = !!(--client.activeRequestCount); 281 | if (!client.isRequesting) { 282 | var evt_2 = DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 283 | setTimeout(function () { return DOM.dispatchEvent(evt_2); }, 1); 284 | } 285 | } 286 | function parseHeaderValues(headers) { 287 | var parsedHeaders = {}; 288 | for (var name_1 in headers || {}) { 289 | if (headers.hasOwnProperty(name_1)) { 290 | parsedHeaders[name_1] = (typeof headers[name_1] === 'function') ? headers[name_1]() : headers[name_1]; 291 | } 292 | } 293 | return parsedHeaders; 294 | } 295 | function getRequestUrl(baseUrl, url) { 296 | if (absoluteUrlRegexp.test(url)) { 297 | return url; 298 | } 299 | return (baseUrl || '') + url; 300 | } 301 | function setDefaultHeaders(headers, defaultHeaders) { 302 | for (var name_2 in defaultHeaders || {}) { 303 | if (defaultHeaders.hasOwnProperty(name_2) && !headers.has(name_2)) { 304 | headers.set(name_2, defaultHeaders[name_2]); 305 | } 306 | } 307 | } 308 | function processRequest(request, interceptors, http) { 309 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 310 | } 311 | function processResponse(response, interceptors, request, http) { 312 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 313 | } 314 | function applyInterceptors(input, interceptors, successName, errorName) { 315 | var interceptorArgs = []; 316 | for (var _i = 4; _i < arguments.length; _i++) { 317 | interceptorArgs[_i - 4] = arguments[_i]; 318 | } 319 | return (interceptors || []) 320 | .reduce(function (chain, interceptor) { 321 | var successHandler = interceptor[successName]; 322 | var errorHandler = interceptor[errorName]; 323 | return chain.then(successHandler && (function (value) { return successHandler.call.apply(successHandler, [interceptor, value].concat(interceptorArgs)); }) || identity, errorHandler && (function (reason) { return errorHandler.call.apply(errorHandler, [interceptor, reason].concat(interceptorArgs)); }) || thrower); 324 | }, Promise.resolve(input)); 325 | } 326 | function isJSON(str) { 327 | try { 328 | JSON.parse(str); 329 | } 330 | catch (err) { 331 | return false; 332 | } 333 | return true; 334 | } 335 | function identity(x) { 336 | return x; 337 | } 338 | function thrower(x) { 339 | throw x; 340 | } 341 | function callFetch(client, input, body, init, method) { 342 | if (!init) { 343 | init = {}; 344 | } 345 | init.method = method; 346 | if (body) { 347 | init.body = body; 348 | } 349 | return client.fetch(input, init); 350 | } 351 | 352 | export { json, retryStrategy, RetryInterceptor, HttpClientConfiguration, HttpClient }; 353 | -------------------------------------------------------------------------------- /dist/system/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | System.register(['aurelia-pal'], function (exports, module) { 2 | 'use strict'; 3 | var PLATFORM, DOM; 4 | return { 5 | setters: [function (module) { 6 | PLATFORM = module.PLATFORM; 7 | DOM = module.DOM; 8 | }], 9 | execute: function () { 10 | 11 | exports('json', json); 12 | 13 | function json(body, replacer) { 14 | return JSON.stringify((body !== undefined ? body : {}), replacer); 15 | } 16 | 17 | var retryStrategy = exports('retryStrategy', { 18 | fixed: 0, 19 | incremental: 1, 20 | exponential: 2, 21 | random: 3 22 | }); 23 | var defaultRetryConfig = { 24 | maxRetries: 3, 25 | interval: 1000, 26 | strategy: retryStrategy.fixed 27 | }; 28 | var RetryInterceptor = exports('RetryInterceptor', (function () { 29 | function RetryInterceptor(retryConfig) { 30 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 31 | if (this.retryConfig.strategy === retryStrategy.exponential && 32 | this.retryConfig.interval <= 1000) { 33 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 34 | } 35 | } 36 | RetryInterceptor.prototype.request = function (request) { 37 | var $r = request; 38 | if (!$r.retryConfig) { 39 | $r.retryConfig = Object.assign({}, this.retryConfig); 40 | $r.retryConfig.counter = 0; 41 | } 42 | $r.retryConfig.requestClone = request.clone(); 43 | return request; 44 | }; 45 | RetryInterceptor.prototype.response = function (response, request) { 46 | delete request.retryConfig; 47 | return response; 48 | }; 49 | RetryInterceptor.prototype.responseError = function (error, request, httpClient) { 50 | var retryConfig = request.retryConfig; 51 | var requestClone = retryConfig.requestClone; 52 | return Promise.resolve().then(function () { 53 | if (retryConfig.counter < retryConfig.maxRetries) { 54 | var result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 55 | return Promise.resolve(result).then(function (doRetry) { 56 | if (doRetry) { 57 | retryConfig.counter++; 58 | return new Promise(function (resolve) { return PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0); }) 59 | .then(function () { 60 | var newRequest = requestClone.clone(); 61 | if (typeof (retryConfig.beforeRetry) === 'function') { 62 | return retryConfig.beforeRetry(newRequest, httpClient); 63 | } 64 | return newRequest; 65 | }) 66 | .then(function (newRequest) { 67 | return httpClient.fetch(Object.assign(newRequest, { retryConfig: retryConfig })); 68 | }); 69 | } 70 | delete request.retryConfig; 71 | throw error; 72 | }); 73 | } 74 | delete request.retryConfig; 75 | throw error; 76 | }); 77 | }; 78 | return RetryInterceptor; 79 | }())); 80 | function calculateDelay(retryConfig) { 81 | var interval = retryConfig.interval, strategy = retryConfig.strategy, minRandomInterval = retryConfig.minRandomInterval, maxRandomInterval = retryConfig.maxRandomInterval, counter = retryConfig.counter; 82 | if (typeof (strategy) === 'function') { 83 | return retryConfig.strategy(counter); 84 | } 85 | switch (strategy) { 86 | case (retryStrategy.fixed): 87 | return retryStrategies[retryStrategy.fixed](interval); 88 | case (retryStrategy.incremental): 89 | return retryStrategies[retryStrategy.incremental](counter, interval); 90 | case (retryStrategy.exponential): 91 | return retryStrategies[retryStrategy.exponential](counter, interval); 92 | case (retryStrategy.random): 93 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 94 | default: 95 | throw new Error('Unrecognized retry strategy'); 96 | } 97 | } 98 | var retryStrategies = [ 99 | function (interval) { return interval; }, 100 | function (retryCount, interval) { return interval * retryCount; }, 101 | function (retryCount, interval) { return retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000; }, 102 | function (retryCount, interval, minRandomInterval, maxRandomInterval) { 103 | if (minRandomInterval === void 0) { minRandomInterval = 0; } 104 | if (maxRandomInterval === void 0) { maxRandomInterval = 60000; } 105 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 106 | } 107 | ]; 108 | 109 | var HttpClientConfiguration = exports('HttpClientConfiguration', (function () { 110 | function HttpClientConfiguration() { 111 | this.baseUrl = ''; 112 | this.defaults = {}; 113 | this.interceptors = []; 114 | } 115 | HttpClientConfiguration.prototype.withBaseUrl = function (baseUrl) { 116 | this.baseUrl = baseUrl; 117 | return this; 118 | }; 119 | HttpClientConfiguration.prototype.withDefaults = function (defaults) { 120 | this.defaults = defaults; 121 | return this; 122 | }; 123 | HttpClientConfiguration.prototype.withInterceptor = function (interceptor) { 124 | this.interceptors.push(interceptor); 125 | return this; 126 | }; 127 | HttpClientConfiguration.prototype.useStandardConfiguration = function () { 128 | var standardConfig = { credentials: 'same-origin' }; 129 | Object.assign(this.defaults, standardConfig, this.defaults); 130 | return this.rejectErrorResponses(); 131 | }; 132 | HttpClientConfiguration.prototype.rejectErrorResponses = function () { 133 | return this.withInterceptor({ response: rejectOnError }); 134 | }; 135 | HttpClientConfiguration.prototype.withRetry = function (config) { 136 | var interceptor = new RetryInterceptor(config); 137 | return this.withInterceptor(interceptor); 138 | }; 139 | return HttpClientConfiguration; 140 | }())); 141 | function rejectOnError(response) { 142 | if (!response.ok) { 143 | throw response; 144 | } 145 | return response; 146 | } 147 | 148 | var HttpClient = exports('HttpClient', (function () { 149 | function HttpClient() { 150 | this.activeRequestCount = 0; 151 | this.isRequesting = false; 152 | this.isConfigured = false; 153 | this.baseUrl = ''; 154 | this.defaults = null; 155 | this.interceptors = []; 156 | if (typeof fetch === 'undefined') { 157 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 158 | } 159 | } 160 | HttpClient.prototype.configure = function (config) { 161 | var normalizedConfig; 162 | if (typeof config === 'object') { 163 | normalizedConfig = { defaults: config }; 164 | } 165 | else if (typeof config === 'function') { 166 | normalizedConfig = new HttpClientConfiguration(); 167 | normalizedConfig.baseUrl = this.baseUrl; 168 | normalizedConfig.defaults = Object.assign({}, this.defaults); 169 | normalizedConfig.interceptors = this.interceptors; 170 | var c = config(normalizedConfig); 171 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 172 | normalizedConfig = c; 173 | } 174 | } 175 | else { 176 | throw new Error('invalid config'); 177 | } 178 | var defaults = normalizedConfig.defaults; 179 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 180 | throw new Error('Default headers must be a plain object.'); 181 | } 182 | var interceptors = normalizedConfig.interceptors; 183 | if (interceptors && interceptors.length) { 184 | if (interceptors.filter(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }).length > 1) { 185 | throw new Error('Only one RetryInterceptor is allowed.'); 186 | } 187 | var retryInterceptorIndex = interceptors.findIndex(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }); 188 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 189 | throw new Error('The retry interceptor must be the last interceptor defined.'); 190 | } 191 | } 192 | this.baseUrl = normalizedConfig.baseUrl; 193 | this.defaults = defaults; 194 | this.interceptors = normalizedConfig.interceptors || []; 195 | this.isConfigured = true; 196 | return this; 197 | }; 198 | HttpClient.prototype.fetch = function (input, init) { 199 | var _this = this; 200 | trackRequestStart(this); 201 | var request = this.buildRequest(input, init); 202 | return processRequest(request, this.interceptors, this).then(function (result) { 203 | var response = null; 204 | if (Response.prototype.isPrototypeOf(result)) { 205 | response = Promise.resolve(result); 206 | } 207 | else if (Request.prototype.isPrototypeOf(result)) { 208 | request = result; 209 | response = fetch(result); 210 | } 211 | else { 212 | throw new Error("An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [" + result + "]"); 213 | } 214 | return processResponse(response, _this.interceptors, request, _this); 215 | }) 216 | .then(function (result) { 217 | if (Request.prototype.isPrototypeOf(result)) { 218 | return _this.fetch(result); 219 | } 220 | return result; 221 | }) 222 | .then(function (result) { 223 | trackRequestEnd(_this); 224 | return result; 225 | }, function (error) { 226 | trackRequestEnd(_this); 227 | throw error; 228 | }); 229 | }; 230 | HttpClient.prototype.buildRequest = function (input, init) { 231 | var defaults = this.defaults || {}; 232 | var request; 233 | var body; 234 | var requestContentType; 235 | var parsedDefaultHeaders = parseHeaderValues(defaults.headers); 236 | if (Request.prototype.isPrototypeOf(input)) { 237 | request = input; 238 | requestContentType = new Headers(request.headers).get('Content-Type'); 239 | } 240 | else { 241 | if (!init) { 242 | init = {}; 243 | } 244 | body = init.body; 245 | var bodyObj = body ? { body: body } : null; 246 | var requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 247 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 248 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 249 | } 250 | if (!requestContentType) { 251 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 252 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 253 | } 254 | else if (body && isJSON(body)) { 255 | request.headers.set('Content-Type', 'application/json'); 256 | } 257 | } 258 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 259 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 260 | request.headers.set('Content-Type', body.type); 261 | } 262 | return request; 263 | }; 264 | HttpClient.prototype.get = function (input, init) { 265 | return this.fetch(input, init); 266 | }; 267 | HttpClient.prototype.post = function (input, body, init) { 268 | return callFetch(this, input, body, init, 'POST'); 269 | }; 270 | HttpClient.prototype.put = function (input, body, init) { 271 | return callFetch(this, input, body, init, 'PUT'); 272 | }; 273 | HttpClient.prototype.patch = function (input, body, init) { 274 | return callFetch(this, input, body, init, 'PATCH'); 275 | }; 276 | HttpClient.prototype.delete = function (input, body, init) { 277 | return callFetch(this, input, body, init, 'DELETE'); 278 | }; 279 | return HttpClient; 280 | }())); 281 | var absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 282 | function trackRequestStart(client) { 283 | client.isRequesting = !!(++client.activeRequestCount); 284 | if (client.isRequesting) { 285 | var evt_1 = DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 286 | setTimeout(function () { return DOM.dispatchEvent(evt_1); }, 1); 287 | } 288 | } 289 | function trackRequestEnd(client) { 290 | client.isRequesting = !!(--client.activeRequestCount); 291 | if (!client.isRequesting) { 292 | var evt_2 = DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 293 | setTimeout(function () { return DOM.dispatchEvent(evt_2); }, 1); 294 | } 295 | } 296 | function parseHeaderValues(headers) { 297 | var parsedHeaders = {}; 298 | for (var name_1 in headers || {}) { 299 | if (headers.hasOwnProperty(name_1)) { 300 | parsedHeaders[name_1] = (typeof headers[name_1] === 'function') ? headers[name_1]() : headers[name_1]; 301 | } 302 | } 303 | return parsedHeaders; 304 | } 305 | function getRequestUrl(baseUrl, url) { 306 | if (absoluteUrlRegexp.test(url)) { 307 | return url; 308 | } 309 | return (baseUrl || '') + url; 310 | } 311 | function setDefaultHeaders(headers, defaultHeaders) { 312 | for (var name_2 in defaultHeaders || {}) { 313 | if (defaultHeaders.hasOwnProperty(name_2) && !headers.has(name_2)) { 314 | headers.set(name_2, defaultHeaders[name_2]); 315 | } 316 | } 317 | } 318 | function processRequest(request, interceptors, http) { 319 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 320 | } 321 | function processResponse(response, interceptors, request, http) { 322 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 323 | } 324 | function applyInterceptors(input, interceptors, successName, errorName) { 325 | var interceptorArgs = []; 326 | for (var _i = 4; _i < arguments.length; _i++) { 327 | interceptorArgs[_i - 4] = arguments[_i]; 328 | } 329 | return (interceptors || []) 330 | .reduce(function (chain, interceptor) { 331 | var successHandler = interceptor[successName]; 332 | var errorHandler = interceptor[errorName]; 333 | return chain.then(successHandler && (function (value) { return successHandler.call.apply(successHandler, [interceptor, value].concat(interceptorArgs)); }) || identity, errorHandler && (function (reason) { return errorHandler.call.apply(errorHandler, [interceptor, reason].concat(interceptorArgs)); }) || thrower); 334 | }, Promise.resolve(input)); 335 | } 336 | function isJSON(str) { 337 | try { 338 | JSON.parse(str); 339 | } 340 | catch (err) { 341 | return false; 342 | } 343 | return true; 344 | } 345 | function identity(x) { 346 | return x; 347 | } 348 | function thrower(x) { 349 | throw x; 350 | } 351 | function callFetch(client, input, body, init, method) { 352 | if (!init) { 353 | init = {}; 354 | } 355 | init.method = method; 356 | if (body) { 357 | init.body = body; 358 | } 359 | return client.fetch(input, init); 360 | } 361 | 362 | } 363 | }; 364 | }); 365 | -------------------------------------------------------------------------------- /dist/umd-es2015/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-pal')) : 3 | typeof define === 'function' && define.amd ? define(['exports', 'aurelia-pal'], factory) : 4 | (global = global || self, factory((global.au = global.au || {}, global.au.fetchClient = {}), global.au)); 5 | }(this, function (exports, aureliaPal) { 'use strict'; 6 | 7 | function json(body, replacer) { 8 | return JSON.stringify((body !== undefined ? body : {}), replacer); 9 | } 10 | 11 | const retryStrategy = { 12 | fixed: 0, 13 | incremental: 1, 14 | exponential: 2, 15 | random: 3 16 | }; 17 | const defaultRetryConfig = { 18 | maxRetries: 3, 19 | interval: 1000, 20 | strategy: retryStrategy.fixed 21 | }; 22 | class RetryInterceptor { 23 | constructor(retryConfig) { 24 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 25 | if (this.retryConfig.strategy === retryStrategy.exponential && 26 | this.retryConfig.interval <= 1000) { 27 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 28 | } 29 | } 30 | request(request) { 31 | let $r = request; 32 | if (!$r.retryConfig) { 33 | $r.retryConfig = Object.assign({}, this.retryConfig); 34 | $r.retryConfig.counter = 0; 35 | } 36 | $r.retryConfig.requestClone = request.clone(); 37 | return request; 38 | } 39 | response(response, request) { 40 | delete request.retryConfig; 41 | return response; 42 | } 43 | responseError(error, request, httpClient) { 44 | const { retryConfig } = request; 45 | const { requestClone } = retryConfig; 46 | return Promise.resolve().then(() => { 47 | if (retryConfig.counter < retryConfig.maxRetries) { 48 | const result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 49 | return Promise.resolve(result).then(doRetry => { 50 | if (doRetry) { 51 | retryConfig.counter++; 52 | return new Promise(resolve => aureliaPal.PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0)) 53 | .then(() => { 54 | let newRequest = requestClone.clone(); 55 | if (typeof (retryConfig.beforeRetry) === 'function') { 56 | return retryConfig.beforeRetry(newRequest, httpClient); 57 | } 58 | return newRequest; 59 | }) 60 | .then(newRequest => { 61 | return httpClient.fetch(Object.assign(newRequest, { retryConfig })); 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | } 68 | delete request.retryConfig; 69 | throw error; 70 | }); 71 | } 72 | } 73 | function calculateDelay(retryConfig) { 74 | const { interval, strategy, minRandomInterval, maxRandomInterval, counter } = retryConfig; 75 | if (typeof (strategy) === 'function') { 76 | return retryConfig.strategy(counter); 77 | } 78 | switch (strategy) { 79 | case (retryStrategy.fixed): 80 | return retryStrategies[retryStrategy.fixed](interval); 81 | case (retryStrategy.incremental): 82 | return retryStrategies[retryStrategy.incremental](counter, interval); 83 | case (retryStrategy.exponential): 84 | return retryStrategies[retryStrategy.exponential](counter, interval); 85 | case (retryStrategy.random): 86 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 87 | default: 88 | throw new Error('Unrecognized retry strategy'); 89 | } 90 | } 91 | const retryStrategies = [ 92 | interval => interval, 93 | (retryCount, interval) => interval * retryCount, 94 | (retryCount, interval) => retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000, 95 | (retryCount, interval, minRandomInterval = 0, maxRandomInterval = 60000) => { 96 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 97 | } 98 | ]; 99 | 100 | class HttpClientConfiguration { 101 | constructor() { 102 | this.baseUrl = ''; 103 | this.defaults = {}; 104 | this.interceptors = []; 105 | } 106 | withBaseUrl(baseUrl) { 107 | this.baseUrl = baseUrl; 108 | return this; 109 | } 110 | withDefaults(defaults) { 111 | this.defaults = defaults; 112 | return this; 113 | } 114 | withInterceptor(interceptor) { 115 | this.interceptors.push(interceptor); 116 | return this; 117 | } 118 | useStandardConfiguration() { 119 | let standardConfig = { credentials: 'same-origin' }; 120 | Object.assign(this.defaults, standardConfig, this.defaults); 121 | return this.rejectErrorResponses(); 122 | } 123 | rejectErrorResponses() { 124 | return this.withInterceptor({ response: rejectOnError }); 125 | } 126 | withRetry(config) { 127 | const interceptor = new RetryInterceptor(config); 128 | return this.withInterceptor(interceptor); 129 | } 130 | } 131 | function rejectOnError(response) { 132 | if (!response.ok) { 133 | throw response; 134 | } 135 | return response; 136 | } 137 | 138 | class HttpClient { 139 | constructor() { 140 | this.activeRequestCount = 0; 141 | this.isRequesting = false; 142 | this.isConfigured = false; 143 | this.baseUrl = ''; 144 | this.defaults = null; 145 | this.interceptors = []; 146 | if (typeof fetch === 'undefined') { 147 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 148 | } 149 | } 150 | configure(config) { 151 | let normalizedConfig; 152 | if (typeof config === 'object') { 153 | normalizedConfig = { defaults: config }; 154 | } 155 | else if (typeof config === 'function') { 156 | normalizedConfig = new HttpClientConfiguration(); 157 | normalizedConfig.baseUrl = this.baseUrl; 158 | normalizedConfig.defaults = Object.assign({}, this.defaults); 159 | normalizedConfig.interceptors = this.interceptors; 160 | let c = config(normalizedConfig); 161 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 162 | normalizedConfig = c; 163 | } 164 | } 165 | else { 166 | throw new Error('invalid config'); 167 | } 168 | let defaults = normalizedConfig.defaults; 169 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 170 | throw new Error('Default headers must be a plain object.'); 171 | } 172 | let interceptors = normalizedConfig.interceptors; 173 | if (interceptors && interceptors.length) { 174 | if (interceptors.filter(x => RetryInterceptor.prototype.isPrototypeOf(x)).length > 1) { 175 | throw new Error('Only one RetryInterceptor is allowed.'); 176 | } 177 | const retryInterceptorIndex = interceptors.findIndex(x => RetryInterceptor.prototype.isPrototypeOf(x)); 178 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 179 | throw new Error('The retry interceptor must be the last interceptor defined.'); 180 | } 181 | } 182 | this.baseUrl = normalizedConfig.baseUrl; 183 | this.defaults = defaults; 184 | this.interceptors = normalizedConfig.interceptors || []; 185 | this.isConfigured = true; 186 | return this; 187 | } 188 | fetch(input, init) { 189 | trackRequestStart(this); 190 | let request = this.buildRequest(input, init); 191 | return processRequest(request, this.interceptors, this).then(result => { 192 | let response = null; 193 | if (Response.prototype.isPrototypeOf(result)) { 194 | response = Promise.resolve(result); 195 | } 196 | else if (Request.prototype.isPrototypeOf(result)) { 197 | request = result; 198 | response = fetch(result); 199 | } 200 | else { 201 | throw new Error(`An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [${result}]`); 202 | } 203 | return processResponse(response, this.interceptors, request, this); 204 | }) 205 | .then(result => { 206 | if (Request.prototype.isPrototypeOf(result)) { 207 | return this.fetch(result); 208 | } 209 | return result; 210 | }) 211 | .then(result => { 212 | trackRequestEnd(this); 213 | return result; 214 | }, error => { 215 | trackRequestEnd(this); 216 | throw error; 217 | }); 218 | } 219 | buildRequest(input, init) { 220 | let defaults = this.defaults || {}; 221 | let request; 222 | let body; 223 | let requestContentType; 224 | let parsedDefaultHeaders = parseHeaderValues(defaults.headers); 225 | if (Request.prototype.isPrototypeOf(input)) { 226 | request = input; 227 | requestContentType = new Headers(request.headers).get('Content-Type'); 228 | } 229 | else { 230 | if (!init) { 231 | init = {}; 232 | } 233 | body = init.body; 234 | let bodyObj = body ? { body } : null; 235 | let requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 236 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 237 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 238 | } 239 | if (!requestContentType) { 240 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 241 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 242 | } 243 | else if (body && isJSON(body)) { 244 | request.headers.set('Content-Type', 'application/json'); 245 | } 246 | } 247 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 248 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 249 | request.headers.set('Content-Type', body.type); 250 | } 251 | return request; 252 | } 253 | get(input, init) { 254 | return this.fetch(input, init); 255 | } 256 | post(input, body, init) { 257 | return callFetch(this, input, body, init, 'POST'); 258 | } 259 | put(input, body, init) { 260 | return callFetch(this, input, body, init, 'PUT'); 261 | } 262 | patch(input, body, init) { 263 | return callFetch(this, input, body, init, 'PATCH'); 264 | } 265 | delete(input, body, init) { 266 | return callFetch(this, input, body, init, 'DELETE'); 267 | } 268 | } 269 | const absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 270 | function trackRequestStart(client) { 271 | client.isRequesting = !!(++client.activeRequestCount); 272 | if (client.isRequesting) { 273 | let evt = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 274 | setTimeout(() => aureliaPal.DOM.dispatchEvent(evt), 1); 275 | } 276 | } 277 | function trackRequestEnd(client) { 278 | client.isRequesting = !!(--client.activeRequestCount); 279 | if (!client.isRequesting) { 280 | let evt = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 281 | setTimeout(() => aureliaPal.DOM.dispatchEvent(evt), 1); 282 | } 283 | } 284 | function parseHeaderValues(headers) { 285 | let parsedHeaders = {}; 286 | for (let name in headers || {}) { 287 | if (headers.hasOwnProperty(name)) { 288 | parsedHeaders[name] = (typeof headers[name] === 'function') ? headers[name]() : headers[name]; 289 | } 290 | } 291 | return parsedHeaders; 292 | } 293 | function getRequestUrl(baseUrl, url) { 294 | if (absoluteUrlRegexp.test(url)) { 295 | return url; 296 | } 297 | return (baseUrl || '') + url; 298 | } 299 | function setDefaultHeaders(headers, defaultHeaders) { 300 | for (let name in defaultHeaders || {}) { 301 | if (defaultHeaders.hasOwnProperty(name) && !headers.has(name)) { 302 | headers.set(name, defaultHeaders[name]); 303 | } 304 | } 305 | } 306 | function processRequest(request, interceptors, http) { 307 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 308 | } 309 | function processResponse(response, interceptors, request, http) { 310 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 311 | } 312 | function applyInterceptors(input, interceptors, successName, errorName, ...interceptorArgs) { 313 | return (interceptors || []) 314 | .reduce((chain, interceptor) => { 315 | let successHandler = interceptor[successName]; 316 | let errorHandler = interceptor[errorName]; 317 | return chain.then(successHandler && (value => successHandler.call(interceptor, value, ...interceptorArgs)) || identity, errorHandler && (reason => errorHandler.call(interceptor, reason, ...interceptorArgs)) || thrower); 318 | }, Promise.resolve(input)); 319 | } 320 | function isJSON(str) { 321 | try { 322 | JSON.parse(str); 323 | } 324 | catch (err) { 325 | return false; 326 | } 327 | return true; 328 | } 329 | function identity(x) { 330 | return x; 331 | } 332 | function thrower(x) { 333 | throw x; 334 | } 335 | function callFetch(client, input, body, init, method) { 336 | if (!init) { 337 | init = {}; 338 | } 339 | init.method = method; 340 | if (body) { 341 | init.body = body; 342 | } 343 | return client.fetch(input, init); 344 | } 345 | 346 | exports.json = json; 347 | exports.retryStrategy = retryStrategy; 348 | exports.RetryInterceptor = RetryInterceptor; 349 | exports.HttpClientConfiguration = HttpClientConfiguration; 350 | exports.HttpClient = HttpClient; 351 | 352 | Object.defineProperty(exports, '__esModule', { value: true }); 353 | 354 | })); 355 | -------------------------------------------------------------------------------- /dist/umd/aurelia-fetch-client.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-pal')) : 3 | typeof define === 'function' && define.amd ? define(['exports', 'aurelia-pal'], factory) : 4 | (global = global || self, factory((global.au = global.au || {}, global.au.fetchClient = {}), global.au)); 5 | }(this, function (exports, aureliaPal) { 'use strict'; 6 | 7 | function json(body, replacer) { 8 | return JSON.stringify((body !== undefined ? body : {}), replacer); 9 | } 10 | 11 | var retryStrategy = { 12 | fixed: 0, 13 | incremental: 1, 14 | exponential: 2, 15 | random: 3 16 | }; 17 | var defaultRetryConfig = { 18 | maxRetries: 3, 19 | interval: 1000, 20 | strategy: retryStrategy.fixed 21 | }; 22 | var RetryInterceptor = (function () { 23 | function RetryInterceptor(retryConfig) { 24 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 25 | if (this.retryConfig.strategy === retryStrategy.exponential && 26 | this.retryConfig.interval <= 1000) { 27 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 28 | } 29 | } 30 | RetryInterceptor.prototype.request = function (request) { 31 | var $r = request; 32 | if (!$r.retryConfig) { 33 | $r.retryConfig = Object.assign({}, this.retryConfig); 34 | $r.retryConfig.counter = 0; 35 | } 36 | $r.retryConfig.requestClone = request.clone(); 37 | return request; 38 | }; 39 | RetryInterceptor.prototype.response = function (response, request) { 40 | delete request.retryConfig; 41 | return response; 42 | }; 43 | RetryInterceptor.prototype.responseError = function (error, request, httpClient) { 44 | var retryConfig = request.retryConfig; 45 | var requestClone = retryConfig.requestClone; 46 | return Promise.resolve().then(function () { 47 | if (retryConfig.counter < retryConfig.maxRetries) { 48 | var result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 49 | return Promise.resolve(result).then(function (doRetry) { 50 | if (doRetry) { 51 | retryConfig.counter++; 52 | return new Promise(function (resolve) { return aureliaPal.PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0); }) 53 | .then(function () { 54 | var newRequest = requestClone.clone(); 55 | if (typeof (retryConfig.beforeRetry) === 'function') { 56 | return retryConfig.beforeRetry(newRequest, httpClient); 57 | } 58 | return newRequest; 59 | }) 60 | .then(function (newRequest) { 61 | return httpClient.fetch(Object.assign(newRequest, { retryConfig: retryConfig })); 62 | }); 63 | } 64 | delete request.retryConfig; 65 | throw error; 66 | }); 67 | } 68 | delete request.retryConfig; 69 | throw error; 70 | }); 71 | }; 72 | return RetryInterceptor; 73 | }()); 74 | function calculateDelay(retryConfig) { 75 | var interval = retryConfig.interval, strategy = retryConfig.strategy, minRandomInterval = retryConfig.minRandomInterval, maxRandomInterval = retryConfig.maxRandomInterval, counter = retryConfig.counter; 76 | if (typeof (strategy) === 'function') { 77 | return retryConfig.strategy(counter); 78 | } 79 | switch (strategy) { 80 | case (retryStrategy.fixed): 81 | return retryStrategies[retryStrategy.fixed](interval); 82 | case (retryStrategy.incremental): 83 | return retryStrategies[retryStrategy.incremental](counter, interval); 84 | case (retryStrategy.exponential): 85 | return retryStrategies[retryStrategy.exponential](counter, interval); 86 | case (retryStrategy.random): 87 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 88 | default: 89 | throw new Error('Unrecognized retry strategy'); 90 | } 91 | } 92 | var retryStrategies = [ 93 | function (interval) { return interval; }, 94 | function (retryCount, interval) { return interval * retryCount; }, 95 | function (retryCount, interval) { return retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000; }, 96 | function (retryCount, interval, minRandomInterval, maxRandomInterval) { 97 | if (minRandomInterval === void 0) { minRandomInterval = 0; } 98 | if (maxRandomInterval === void 0) { maxRandomInterval = 60000; } 99 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 100 | } 101 | ]; 102 | 103 | var HttpClientConfiguration = (function () { 104 | function HttpClientConfiguration() { 105 | this.baseUrl = ''; 106 | this.defaults = {}; 107 | this.interceptors = []; 108 | } 109 | HttpClientConfiguration.prototype.withBaseUrl = function (baseUrl) { 110 | this.baseUrl = baseUrl; 111 | return this; 112 | }; 113 | HttpClientConfiguration.prototype.withDefaults = function (defaults) { 114 | this.defaults = defaults; 115 | return this; 116 | }; 117 | HttpClientConfiguration.prototype.withInterceptor = function (interceptor) { 118 | this.interceptors.push(interceptor); 119 | return this; 120 | }; 121 | HttpClientConfiguration.prototype.useStandardConfiguration = function () { 122 | var standardConfig = { credentials: 'same-origin' }; 123 | Object.assign(this.defaults, standardConfig, this.defaults); 124 | return this.rejectErrorResponses(); 125 | }; 126 | HttpClientConfiguration.prototype.rejectErrorResponses = function () { 127 | return this.withInterceptor({ response: rejectOnError }); 128 | }; 129 | HttpClientConfiguration.prototype.withRetry = function (config) { 130 | var interceptor = new RetryInterceptor(config); 131 | return this.withInterceptor(interceptor); 132 | }; 133 | return HttpClientConfiguration; 134 | }()); 135 | function rejectOnError(response) { 136 | if (!response.ok) { 137 | throw response; 138 | } 139 | return response; 140 | } 141 | 142 | var HttpClient = (function () { 143 | function HttpClient() { 144 | this.activeRequestCount = 0; 145 | this.isRequesting = false; 146 | this.isConfigured = false; 147 | this.baseUrl = ''; 148 | this.defaults = null; 149 | this.interceptors = []; 150 | if (typeof fetch === 'undefined') { 151 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 152 | } 153 | } 154 | HttpClient.prototype.configure = function (config) { 155 | var normalizedConfig; 156 | if (typeof config === 'object') { 157 | normalizedConfig = { defaults: config }; 158 | } 159 | else if (typeof config === 'function') { 160 | normalizedConfig = new HttpClientConfiguration(); 161 | normalizedConfig.baseUrl = this.baseUrl; 162 | normalizedConfig.defaults = Object.assign({}, this.defaults); 163 | normalizedConfig.interceptors = this.interceptors; 164 | var c = config(normalizedConfig); 165 | if (HttpClientConfiguration.prototype.isPrototypeOf(c)) { 166 | normalizedConfig = c; 167 | } 168 | } 169 | else { 170 | throw new Error('invalid config'); 171 | } 172 | var defaults = normalizedConfig.defaults; 173 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 174 | throw new Error('Default headers must be a plain object.'); 175 | } 176 | var interceptors = normalizedConfig.interceptors; 177 | if (interceptors && interceptors.length) { 178 | if (interceptors.filter(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }).length > 1) { 179 | throw new Error('Only one RetryInterceptor is allowed.'); 180 | } 181 | var retryInterceptorIndex = interceptors.findIndex(function (x) { return RetryInterceptor.prototype.isPrototypeOf(x); }); 182 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 183 | throw new Error('The retry interceptor must be the last interceptor defined.'); 184 | } 185 | } 186 | this.baseUrl = normalizedConfig.baseUrl; 187 | this.defaults = defaults; 188 | this.interceptors = normalizedConfig.interceptors || []; 189 | this.isConfigured = true; 190 | return this; 191 | }; 192 | HttpClient.prototype.fetch = function (input, init) { 193 | var _this = this; 194 | trackRequestStart(this); 195 | var request = this.buildRequest(input, init); 196 | return processRequest(request, this.interceptors, this).then(function (result) { 197 | var response = null; 198 | if (Response.prototype.isPrototypeOf(result)) { 199 | response = Promise.resolve(result); 200 | } 201 | else if (Request.prototype.isPrototypeOf(result)) { 202 | request = result; 203 | response = fetch(result); 204 | } 205 | else { 206 | throw new Error("An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [" + result + "]"); 207 | } 208 | return processResponse(response, _this.interceptors, request, _this); 209 | }) 210 | .then(function (result) { 211 | if (Request.prototype.isPrototypeOf(result)) { 212 | return _this.fetch(result); 213 | } 214 | return result; 215 | }) 216 | .then(function (result) { 217 | trackRequestEnd(_this); 218 | return result; 219 | }, function (error) { 220 | trackRequestEnd(_this); 221 | throw error; 222 | }); 223 | }; 224 | HttpClient.prototype.buildRequest = function (input, init) { 225 | var defaults = this.defaults || {}; 226 | var request; 227 | var body; 228 | var requestContentType; 229 | var parsedDefaultHeaders = parseHeaderValues(defaults.headers); 230 | if (Request.prototype.isPrototypeOf(input)) { 231 | request = input; 232 | requestContentType = new Headers(request.headers).get('Content-Type'); 233 | } 234 | else { 235 | if (!init) { 236 | init = {}; 237 | } 238 | body = init.body; 239 | var bodyObj = body ? { body: body } : null; 240 | var requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 241 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 242 | request = new Request(getRequestUrl(this.baseUrl, input), requestInit); 243 | } 244 | if (!requestContentType) { 245 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 246 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 247 | } 248 | else if (body && isJSON(body)) { 249 | request.headers.set('Content-Type', 'application/json'); 250 | } 251 | } 252 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 253 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 254 | request.headers.set('Content-Type', body.type); 255 | } 256 | return request; 257 | }; 258 | HttpClient.prototype.get = function (input, init) { 259 | return this.fetch(input, init); 260 | }; 261 | HttpClient.prototype.post = function (input, body, init) { 262 | return callFetch(this, input, body, init, 'POST'); 263 | }; 264 | HttpClient.prototype.put = function (input, body, init) { 265 | return callFetch(this, input, body, init, 'PUT'); 266 | }; 267 | HttpClient.prototype.patch = function (input, body, init) { 268 | return callFetch(this, input, body, init, 'PATCH'); 269 | }; 270 | HttpClient.prototype.delete = function (input, body, init) { 271 | return callFetch(this, input, body, init, 'DELETE'); 272 | }; 273 | return HttpClient; 274 | }()); 275 | var absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 276 | function trackRequestStart(client) { 277 | client.isRequesting = !!(++client.activeRequestCount); 278 | if (client.isRequesting) { 279 | var evt_1 = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 280 | setTimeout(function () { return aureliaPal.DOM.dispatchEvent(evt_1); }, 1); 281 | } 282 | } 283 | function trackRequestEnd(client) { 284 | client.isRequesting = !!(--client.activeRequestCount); 285 | if (!client.isRequesting) { 286 | var evt_2 = aureliaPal.DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 287 | setTimeout(function () { return aureliaPal.DOM.dispatchEvent(evt_2); }, 1); 288 | } 289 | } 290 | function parseHeaderValues(headers) { 291 | var parsedHeaders = {}; 292 | for (var name_1 in headers || {}) { 293 | if (headers.hasOwnProperty(name_1)) { 294 | parsedHeaders[name_1] = (typeof headers[name_1] === 'function') ? headers[name_1]() : headers[name_1]; 295 | } 296 | } 297 | return parsedHeaders; 298 | } 299 | function getRequestUrl(baseUrl, url) { 300 | if (absoluteUrlRegexp.test(url)) { 301 | return url; 302 | } 303 | return (baseUrl || '') + url; 304 | } 305 | function setDefaultHeaders(headers, defaultHeaders) { 306 | for (var name_2 in defaultHeaders || {}) { 307 | if (defaultHeaders.hasOwnProperty(name_2) && !headers.has(name_2)) { 308 | headers.set(name_2, defaultHeaders[name_2]); 309 | } 310 | } 311 | } 312 | function processRequest(request, interceptors, http) { 313 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 314 | } 315 | function processResponse(response, interceptors, request, http) { 316 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 317 | } 318 | function applyInterceptors(input, interceptors, successName, errorName) { 319 | var interceptorArgs = []; 320 | for (var _i = 4; _i < arguments.length; _i++) { 321 | interceptorArgs[_i - 4] = arguments[_i]; 322 | } 323 | return (interceptors || []) 324 | .reduce(function (chain, interceptor) { 325 | var successHandler = interceptor[successName]; 326 | var errorHandler = interceptor[errorName]; 327 | return chain.then(successHandler && (function (value) { return successHandler.call.apply(successHandler, [interceptor, value].concat(interceptorArgs)); }) || identity, errorHandler && (function (reason) { return errorHandler.call.apply(errorHandler, [interceptor, reason].concat(interceptorArgs)); }) || thrower); 328 | }, Promise.resolve(input)); 329 | } 330 | function isJSON(str) { 331 | try { 332 | JSON.parse(str); 333 | } 334 | catch (err) { 335 | return false; 336 | } 337 | return true; 338 | } 339 | function identity(x) { 340 | return x; 341 | } 342 | function thrower(x) { 343 | throw x; 344 | } 345 | function callFetch(client, input, body, init, method) { 346 | if (!init) { 347 | init = {}; 348 | } 349 | init.method = method; 350 | if (body) { 351 | init.body = body; 352 | } 353 | return client.fetch(input, init); 354 | } 355 | 356 | exports.json = json; 357 | exports.retryStrategy = retryStrategy; 358 | exports.RetryInterceptor = RetryInterceptor; 359 | exports.HttpClientConfiguration = HttpClientConfiguration; 360 | exports.HttpClient = HttpClient; 361 | 362 | Object.defineProperty(exports, '__esModule', { value: true }); 363 | 364 | })); 365 | -------------------------------------------------------------------------------- /doc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.8.3](https://github.com/aurelia/fetch-client/compare/1.8.0...1.8.3) (2019-03-22) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **all:** change es2015 back to native-modules ([14245e8](https://github.com/aurelia/fetch-client/commit/14245e8)) 11 | * **build:** adjust build script, add umd es2015, fix unpkg field ([b62089f](https://github.com/aurelia/fetch-client/commit/b62089f)) 12 | * **ci:** adjust test scripts, separate single/watch mode ([0309253](https://github.com/aurelia/fetch-client/commit/0309253)) 13 | * **retry-interceptor:** conform to Interceptor interface ([daae14b](https://github.com/aurelia/fetch-client/commit/daae14b)) 14 | 15 | 16 | 17 | ## [1.8.1](https://github.com/aurelia/fetch-client/compare/1.8.0...1.8.1) (2019-03-15) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **all:** change es2015 back to native-modules ([14245e8](https://github.com/aurelia/fetch-client/commit/14245e8)) 23 | * **build:** adjust build script, add umd es2015, fix unpkg field ([b62089f](https://github.com/aurelia/fetch-client/commit/b62089f)) 24 | * **ci:** adjust test scripts, separate single/watch mode ([0309253](https://github.com/aurelia/fetch-client/commit/0309253)) 25 | * **retry-interceptor:** conform to Interceptor interface ([daae14b](https://github.com/aurelia/fetch-client/commit/daae14b)) 26 | 27 | 28 | 29 | # [1.8.0](https://github.com/aurelia/fetch-client/compare/1.7.0...1.8.0) (2019-01-18) 30 | 31 | ### Bug Fixes 32 | 33 | * **http-client:** call trackRequestEnd when fetch fails ([cf64989](https://github.com/aurelia/fetch-client/commit/cf64989)) 34 | * Conversion to TypeScript 35 | 36 | 37 | # [1.7.0](https://github.com/aurelia/fetch-client/compare/1.6.0...1.7.0) (2018-12-01) 38 | 39 | ### Features 40 | 41 | * added 'aurelia-fetch-client-request-started' event 42 | 43 | 44 | # [1.6.0](https://github.com/aurelia/fetch-client/compare/1.5.0...1.6.0) (2018-09-25) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **doc:** fix polyfill example with whatwg-fetch ([df50f6d](https://github.com/aurelia/fetch-client/commit/df50f6d)) 50 | 51 | 52 | 53 | 54 | # [1.5.0](https://github.com/aurelia/fetch-client/compare/1.4.0...1.5.0) (2018-09-11) 55 | 56 | ### Features 57 | 58 | * add an aurelia-fetch-client-requests-drained event 59 | * add get/post/put/delete helpder methods 60 | 61 | 62 | # [1.4.0](https://github.com/aurelia/fetch-client/compare/1.3.1...1.4.0) (2018-06-14) 63 | 64 | 65 | ### Features 66 | 67 | * **fetch-client:** add retry functionality ([d16447a](https://github.com/aurelia/fetch-client/commit/d16447a)) 68 | * **http-client:** Expose buildRequest helper API ([33d364d](https://github.com/aurelia/fetch-client/commit/33d364d)) 69 | * **http-client:** Expose HttpClient to interceptors ([36518bc](https://github.com/aurelia/fetch-client/commit/36518bc)) 70 | * **http-client:** Forward Request from response interceptor ([cc91034](https://github.com/aurelia/fetch-client/commit/cc91034)) 71 | * **interface:** add signal to RequestInit interface ([7a056c0](https://github.com/aurelia/fetch-client/commit/7a056c0)) 72 | 73 | 74 | 75 | 76 | ## [1.3.1](https://github.com/aurelia/fetch-client/compare/1.3.0...v1.3.1) (2018-01-30) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * **http-client:** Rework application/json header ([946273a](https://github.com/aurelia/fetch-client/commit/946273a)), closes [#90](https://github.com/aurelia/fetch-client/issues/90) 82 | 83 | 84 | 85 | 86 | # [1.3.0](https://github.com/aurelia/fetch-client/compare/1.2.0...v1.3.0) (2018-01-24) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * **util:** Discontinue using Blob for JSON ([03ae35f](https://github.com/aurelia/fetch-client/commit/03ae35f)), closes [#90](https://github.com/aurelia/fetch-client/issues/90) 92 | 93 | 94 | 95 | 96 | # [1.2.0](https://github.com/aurelia/fetch-client/compare/1.1.3...v1.2.0) (2017-12-20) 97 | 98 | 99 | ### Features 100 | 101 | * **HttpClient:** add JSON.stringify replacer ([2fc49a9](https://github.com/aurelia/fetch-client/commit/2fc49a9)) 102 | 103 | 104 | 105 | 106 | ## [1.1.3](https://github.com/aurelia/fetch-client/compare/1.1.2...v1.1.3) (2017-08-22) 107 | 108 | ### Bug Fixes 109 | 110 | * fix style typo in error message that affected links displayed in editors 111 | 112 | 113 | ## [1.1.2](https://github.com/aurelia/fetch-client/compare/1.1.1...v1.1.2) (2017-03-23) 114 | 115 | ### Bug Fixes 116 | 117 | * Fix blob serialization for undefined body. 118 | 119 | 120 | ## [1.1.1](https://github.com/aurelia/fetch-client/compare/1.1.0...v1.1.1) (2017-02-21) 121 | 122 | * Documentation update. 123 | 124 | 125 | # [1.1.0](https://github.com/aurelia/fetch-client/compare/1.0.1...v1.1.0) (2016-12-03) 126 | 127 | 128 | ### Features 129 | 130 | * passing current config to configure(function(config)) ([124c28b](https://github.com/aurelia/fetch-client/commit/124c28b)), closes [#74](https://github.com/aurelia/fetch-client/issues/74) 131 | 132 | 133 | 134 | 135 | ## [1.0.1](https://github.com/aurelia/fetch-client/compare/1.0.0...v1.0.1) (2016-08-26) 136 | 137 | 138 | 139 | 140 | # [1.0.0](https://github.com/aurelia/fetch-client/compare/1.0.0-rc.1.0.1...v1.0.0) (2016-07-27) 141 | 142 | 143 | 144 | 145 | # [1.0.0-rc.1.0.1](https://github.com/aurelia/fetch-client/compare/1.0.0-rc.1.0.0...v1.0.0-rc.1.0.1) (2016-07-12) 146 | 147 | 148 | 149 | 150 | # [1.0.0-rc.1.0.0](https://github.com/aurelia/fetch-client/compare/1.0.0-beta.2.0.1...v1.0.0-rc.1.0.0) (2016-06-22) 151 | 152 | 153 | 154 | ### 1.0.0-beta.1.2.5 (2016-05-10) 155 | 156 | 157 | ### 1.0.0-beta.1.2.4 (2016-05-10) 158 | 159 | 160 | ### 1.0.0-beta.1.2.3 (2016-04-29) 161 | 162 | 163 | ### 1.0.0-beta.1.2.2 (2016-04-29) 164 | 165 | * documentation update 166 | 167 | ### 1.0.0-beta.1.2.1 (2016-04-08) 168 | 169 | #### Bug Fixes 170 | * **http-client:** Don't create a new request when one is passed to fetch but do apply any default headers 171 | 172 | ### 1.0.0-beta.1.2.0 (2016-03-22) 173 | 174 | * Update to Babel 6 175 | 176 | ### 1.0.0-beta.1.1.1 (2016-03-01) 177 | 178 | 179 | #### Bug Fixes 180 | 181 | * **all:** remove core-js dependency ([f91bd742](http://github.com/aurelia/fetch-client/commit/f91bd742ebb9377904d202e689af3df6fe1a2a7d)) 182 | * **http-client:** 183 | * don't combine request url with base url when request url is absolute ([d1be3b4e](http://github.com/aurelia/fetch-client/commit/d1be3b4e75fd9d65efac2b2b29bb52f5b4959e01)) 184 | * handle last null param in fetch method ([5b5d1333](http://github.com/aurelia/fetch-client/commit/5b5d13331d425c8988fd28d3b7245734bffa6188)) 185 | 186 | 187 | ### 1.0.0-beta.1.1.0 (2016-01-29) 188 | 189 | 190 | #### Bug Fixes 191 | 192 | * **http-client:** ensure default content-type is respected ([f001ebaf](http://github.com/aurelia/fetch-client/commit/f001ebafe47ecc0ebbc74f597ac7ee904194b734), closes [#32](http://github.com/aurelia/fetch-client/issues/32)) 193 | 194 | 195 | #### Features 196 | 197 | * **all:** update jspm meta; core-js ([dd62f230](http://github.com/aurelia/fetch-client/commit/dd62f23099f3e6851eb394b57de6d4da121a241c)) 198 | * **interceptors:** provide Request to response interceptors ([2d24beaa](http://github.com/aurelia/fetch-client/commit/2d24beaa39104074a3c094f5544afc3d7d8ace75), closes [#33](http://github.com/aurelia/fetch-client/issues/33)) 199 | 200 | 201 | ### 1.0.0-beta.1.0.2 (2015-12-17) 202 | 203 | 204 | #### Bug Fixes 205 | 206 | * **http-client:** 207 | * work around bug in IE/Edge where Blob types are ignored ([36407e27](http://github.com/aurelia/fetch-client/commit/36407e27c5b1881473151126fed53f74299ad296)) 208 | * correct type check ([d38d1b34](http://github.com/aurelia/fetch-client/commit/d38d1b34373c50907e7c7673def8ba0ebc5a5427)) 209 | 210 | 211 | ## 1.0.0-beta.1.0.1 (2015-12-03) 212 | 213 | 214 | #### Bug Fixes 215 | 216 | * **build:** 217 | * fix duplicate type definition error when building docs ([15d7213c](http://github.com/aurelia/fetch-client/commit/15d7213cd2173b3cdb03c9267fb64112d7d978c9)) 218 | * include fetch API typings with this library's typings ([b2869d57](http://github.com/aurelia/fetch-client/commit/b2869d5741bccbea1e12a8d40e19d5f1fa1aedfa), closes [#15](http://github.com/aurelia/fetch-client/issues/15), [#23](http://github.com/aurelia/fetch-client/issues/23)) 219 | 220 | 221 | ### 1.0.0-beta.1 (2015-11-16) 222 | 223 | 224 | #### Features 225 | 226 | * **http-client:** throw an error with a helpful message when used in an environment with no fetch ([843451da](http://github.com/aurelia/fetch-client/commit/843451da2aeb166dc25258738351b71c925eeca6), closes [#21](http://github.com/aurelia/fetch-client/issues/21)) 227 | 228 | 229 | ## 0.4.0 (2015-11-09) 230 | 231 | 232 | #### Features 233 | 234 | * **http-client:** clean up configuration ([48c82048](http://github.com/aurelia/fetch-client/commit/48c8204888a0bdcadffd82a42a3a338549fee1d3)) 235 | 236 | 237 | ## 0.3.0 (2015-10-13) 238 | 239 | 240 | #### Bug Fixes 241 | 242 | * **HttpClient:** fix crash in FF caused by attempting to iterate over Headers ([f45dd86e](http://github.com/aurelia/fetch-client/commit/f45dd86ecea4373b40391c0a87078e39af3b15ff)) 243 | * **all:** add corejs ([abc6fcf8](http://github.com/aurelia/fetch-client/commit/abc6fcf8e96fb2336ca156d7c4f0dddba8676f87)) 244 | * **build:** update linting, testing and tools ([12f0cd93](http://github.com/aurelia/fetch-client/commit/12f0cd93a3c31f3076f2e2c1a1ca1f1f87956d2e)) 245 | * **http-client:** 246 | * fix firefox crash ([939f1a95](http://github.com/aurelia/fetch-client/commit/939f1a9583a290ffb4fd9eac9e747f75c3943b07)) 247 | * fix bug where default headers were sometimes not applied correctly ([c3ed06ce](http://github.com/aurelia/fetch-client/commit/c3ed06ce5fb9a814b9320e40499eea99358a8a0b)) 248 | * inline ConfigOrCallback type definition ([6a062601](http://github.com/aurelia/fetch-client/commit/6a062601904d66b12d02290ee7ca7bfb3892bf8a)) 249 | * wrap request creation in a Promise so requestError interceptors will see errors ([522212b6](http://github.com/aurelia/fetch-client/commit/522212b6595af2d7724ad25b2477b4fbfd42bc82)) 250 | * **request-init:** adjust type annotation on headers to allow objects ([aeffb65f](http://github.com/aurelia/fetch-client/commit/aeffb65fa498a78842fb2060cf8e9d0f03fa3024), closes [#16](http://github.com/aurelia/fetch-client/issues/16)) 251 | 252 | 253 | #### Features 254 | 255 | * **HttpClient:** allow functions as default header values ([4f9153a4](http://github.com/aurelia/fetch-client/commit/4f9153a407a8b0c8c13c10b9d06409f57a72985d), closes [#17](http://github.com/aurelia/fetch-client/issues/17)) 256 | * **all:** 257 | * add type annotations ([15fbbbde](http://github.com/aurelia/fetch-client/commit/15fbbbde2be466a4558a87f7e72211ebc739d936)) 258 | * add initial implementation ([dd63fb8d](http://github.com/aurelia/fetch-client/commit/dd63fb8dc1a4261c325a7a5f82c1b9b54fb8f000)) 259 | * **docs:** 260 | * generate api.json from .d.ts file ([80ccb0c2](http://github.com/aurelia/fetch-client/commit/80ccb0c24c6be7f955958a292bef1ca2a8604374)) 261 | * generate api.json from .d.ts file ([6d1cf4cc](http://github.com/aurelia/fetch-client/commit/6d1cf4cc415f24b4385888107bdc63ff093a1ede)) 262 | * **http-client:** make configure chainable ([946ba2c1](http://github.com/aurelia/fetch-client/commit/946ba2c1f3d29870bdfd34ead5998c29e143bae0)) 263 | * **http-client-configuration:** add chainable helpers for all configuration properties ([26aa9df8](http://github.com/aurelia/fetch-client/commit/26aa9df81ad24cfb9f05abdbe3341463833478f0)) 264 | 265 | 266 | ## 0.2.0 (2015-09-04) 267 | 268 | 269 | #### Bug Fixes 270 | 271 | * **build:** update linting, testing and tools ([12f0cd93](http://github.com/aurelia/fetch-client/commit/12f0cd93a3c31f3076f2e2c1a1ca1f1f87956d2e)) 272 | 273 | 274 | #### Features 275 | 276 | * **docs:** 277 | * generate api.json from .d.ts file ([80ccb0c2](http://github.com/aurelia/fetch-client/commit/80ccb0c24c6be7f955958a292bef1ca2a8604374)) 278 | * generate api.json from .d.ts file ([6d1cf4cc](http://github.com/aurelia/fetch-client/commit/6d1cf4cc415f24b4385888107bdc63ff093a1ede)) 279 | 280 | 281 | ### 0.1.2 (2015-08-14) 282 | 283 | 284 | #### Bug Fixes 285 | 286 | * **http-client:** inline ConfigOrCallback type definition ([6a062601](http://github.com/aurelia/fetch-client/commit/6a062601904d66b12d02290ee7ca7bfb3892bf8a)) 287 | 288 | 289 | ### 0.1.1 (2015-07-29) 290 | 291 | 292 | #### Bug Fixes 293 | 294 | * **HttpClient:** fix crash in FF caused by attempting to iterate over Headers ([f45dd86e](http://github.com/aurelia/fetch-client/commit/f45dd86ecea4373b40391c0a87078e39af3b15ff)) 295 | * **all:** add corejs ([abc6fcf8](http://github.com/aurelia/fetch-client/commit/abc6fcf8e96fb2336ca156d7c4f0dddba8676f87)) 296 | * **http-client:** wrap request creation in a Promise so requestError interceptors will see errors ([522212b6](http://github.com/aurelia/fetch-client/commit/522212b6595af2d7724ad25b2477b4fbfd42bc82)) 297 | 298 | 299 | #### Features 300 | 301 | * **all:** add type annotations ([15fbbbde](http://github.com/aurelia/fetch-client/commit/15fbbbde2be466a4558a87f7e72211ebc739d936)) 302 | * **http-client:** make configure chainable ([946ba2c1](http://github.com/aurelia/fetch-client/commit/946ba2c1f3d29870bdfd34ead5998c29e143bae0)) 303 | 304 | 305 | ## 0.1.0 (2015-07-01) 306 | 307 | 308 | #### Features 309 | 310 | * **all:** add initial implementation ([dd63fb8d](http://github.com/aurelia/fetch-client/commit/dd63fb8dc1a4261c325a7a5f82c1b9b54fb8f000)) 311 | * **http-client-configuration:** add chainable helpers for all configuration properties ([26aa9df8](http://github.com/aurelia/fetch-client/commit/26aa9df81ad24cfb9f05abdbe3341463833478f0)) 312 | -------------------------------------------------------------------------------- /doc/dom.d.ts: -------------------------------------------------------------------------------- 1 | declare class BufferSource { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /doc/url.d.ts: -------------------------------------------------------------------------------- 1 | declare class URLSearchParams { 2 | append(name: string, value: string): void; 3 | delete(name: string):void; 4 | get(name: string): string; 5 | getAll(name: string): Array; 6 | has(name: string): boolean; 7 | set(name: string, value: string): void; 8 | } 9 | -------------------------------------------------------------------------------- /doc/whatwg-fetch.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for fetch API 2 | // Project: https://github.com/github/fetch 3 | // Definitions by: Ryan Graham 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare class Request { 7 | constructor(input: string|Request, init?:RequestInit); 8 | method: string; 9 | url: string; 10 | headers: Headers; 11 | context: RequestContext; 12 | referrer: string; 13 | mode: RequestMode; 14 | credentials: RequestCredentials; 15 | cache: RequestCache; 16 | } 17 | 18 | interface RequestInit { 19 | method?: string; 20 | headers?: HeaderInit|{ [index: string]: string }; 21 | body?: BodyInit; 22 | mode?: RequestMode; 23 | credentials?: RequestCredentials; 24 | cache?: RequestCache; 25 | } 26 | 27 | declare enum RequestContext { 28 | "audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch", 29 | "font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import", 30 | "internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script", 31 | "serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker", 32 | "xmlhttprequest", "xslt" 33 | } 34 | declare enum RequestMode { "same-origin", "no-cors", "cors" } 35 | declare enum RequestCredentials { "omit", "same-origin", "include" } 36 | declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" } 37 | 38 | declare class Headers { 39 | append(name: string, value: string): void; 40 | delete(name: string):void; 41 | get(name: string): string; 42 | getAll(name: string): Array; 43 | has(name: string): boolean; 44 | set(name: string, value: string): void; 45 | } 46 | 47 | declare class Body { 48 | bodyUsed: boolean; 49 | arrayBuffer(): Promise; 50 | blob(): Promise; 51 | formData(): Promise; 52 | json(): Promise; 53 | text(): Promise; 54 | } 55 | declare class Response extends Body { 56 | constructor(body?: BodyInit, init?: ResponseInit); 57 | error(): Response; 58 | redirect(url: string, status: number): Response; 59 | type: ResponseType; 60 | url: string; 61 | status: number; 62 | ok: boolean; 63 | statusText: string; 64 | headers: Headers; 65 | clone(): Response; 66 | } 67 | 68 | declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } 69 | 70 | declare class ResponseInit { 71 | status: number; 72 | statusText: string; 73 | headers: HeaderInit; 74 | } 75 | 76 | declare type HeaderInit = Headers|Array; 77 | declare type BodyInit = Blob|FormData|string; 78 | declare type RequestInfo = Request|string; 79 | 80 | interface Window { 81 | fetch(url: string, init?: RequestInit): Promise; 82 | } 83 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function(config) { 4 | const browsers = config.browsers; 5 | config.set({ 6 | 7 | basePath: '', 8 | frameworks: ["jasmine"], 9 | files: ["test/*.spec.ts"], 10 | preprocessors: { 11 | "test/*.spec.ts": ["webpack", "sourcemap"] 12 | }, 13 | webpack: { 14 | mode: "development", 15 | resolve: { 16 | extensions: [".ts", ".js"], 17 | modules: ["src", 'test', "node_modules"], 18 | alias: { 19 | src: path.resolve(__dirname, "src"), 20 | test: path.resolve(__dirname, 'test') 21 | } 22 | }, 23 | devtool: browsers.indexOf('ChromeDebugging') > -1 ? 'eval-source-map' : 'inline-source-map', 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.ts$/, 28 | loader: "ts-loader", 29 | exclude: /node_modules/, 30 | options: { 31 | compilerOptions: { 32 | sourceMap: true 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | }, 39 | mime: { 40 | "text/x-typescript": ["ts"] 41 | }, 42 | reporters: ["mocha"], 43 | webpackServer: { noInfo: config.noInfo }, 44 | browsers: browsers && browsers.length > 0 ? browsers : ['ChromeHeadlessOpt'], 45 | customLaunchers: { 46 | ChromeDebugging: { 47 | base: "Chrome", 48 | flags: [...commonChromeFlags, "--remote-debugging-port=9333"], 49 | debug: true 50 | }, 51 | ChromeHeadlessOpt: { 52 | base: 'ChromeHeadless', 53 | flags: [ 54 | ...commonChromeFlags 55 | ] 56 | } 57 | }, 58 | singleRun: false, 59 | mochaReporter: { 60 | ignoreSkipped: true 61 | } 62 | }); 63 | }; 64 | 65 | 66 | const commonChromeFlags = [ 67 | '--no-default-browser-check', 68 | '--no-first-run', 69 | '--no-managed-user-acknowledgment-check', 70 | '--no-pings', 71 | '--no-sandbox', 72 | '--no-wifi', 73 | '--no-zygote', 74 | '--disable-background-networking', 75 | '--disable-background-timer-throttling', 76 | '--disable-backing-store-limit', 77 | '--disable-boot-animation', 78 | '--disable-breakpad', 79 | '--disable-cache', 80 | '--disable-clear-browsing-data-counters', 81 | '--disable-cloud-import', 82 | '--disable-component-extensions-with-background-pages', 83 | '--disable-contextual-search', 84 | '--disable-default-apps', 85 | '--disable-extensions', 86 | '--disable-infobars', 87 | '--disable-translate', 88 | '--disable-sync' 89 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurelia-fetch-client", 3 | "version": "1.8.3", 4 | "description": "A simple client based on the Fetch standard.", 5 | "keywords": [ 6 | "aurelia", 7 | "http", 8 | "ajax", 9 | "fetch" 10 | ], 11 | "homepage": "http://aurelia.io", 12 | "bugs": { 13 | "url": "https://github.com/aurelia/fetch-client/issues" 14 | }, 15 | "license": "MIT", 16 | "author": "Rob Eisenberg (http://robeisenberg.com/)", 17 | "main": "dist/commonjs/aurelia-fetch-client.js", 18 | "module": "dist/native-modules/aurelia-fetch-client.js", 19 | "browser": "dist/umd/aurelia-fetch-client.js", 20 | "unpkg": "dist/umd-es2015/aurelia-fetch-client.js", 21 | "typings": "dist/aurelia-fetch-client.d.ts", 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/aurelia/fetch-client" 25 | }, 26 | "files": [ 27 | "dist", 28 | "doc", 29 | "package.json", 30 | "README.md", 31 | "LICENSE", 32 | "typings.json" 33 | ], 34 | "scripts": { 35 | "prebuild": "rimraf dist", 36 | "build": "rollup -c", 37 | "postbuild": "dts-bundle-generator -o dist/aurelia-fetch-client.d.ts src/aurelia-fetch-client.ts", 38 | "test": "karma start --single-run", 39 | "test:watch": "karma start", 40 | "test:debugger": "karma start --browsers ChromeDebugging", 41 | "precut-release": "npm run test", 42 | "cut-release": "standard-version -t \"\" -i doc/CHANGELOG.md && npm run build" 43 | }, 44 | "jspm": { 45 | "registry": "npm", 46 | "main": "aurelia-fetch-client", 47 | "format": "amd", 48 | "directories": { 49 | "dist": "dist/amd" 50 | }, 51 | "dependencies": { 52 | "aurelia-pal": "^1.8.0" 53 | }, 54 | "devDependencies": { 55 | "aurelia-pal-browser": "^1.8.0", 56 | "aurelia-polyfills": "^1.3.0" 57 | } 58 | }, 59 | "dependencies": { 60 | "aurelia-pal": "^1.3.0" 61 | }, 62 | "devDependencies": { 63 | "@types/jasmine": "^3.3.5", 64 | "aurelia-pal-browser": "^1.8.0", 65 | "aurelia-polyfills": "^1.3.0", 66 | "babel-plugin-syntax-flow": "^6.8.0", 67 | "cross-env": "^5.2.0", 68 | "dts-bundle-generator": "^2.0.0", 69 | "jasmine-core": "^3.3.0", 70 | "jspm": "^0.16.53", 71 | "karma": "^3.1.4", 72 | "karma-chrome-launcher": "^2.2.0", 73 | "karma-coverage": "^1.1.2", 74 | "karma-jasmine": "^2.0.1", 75 | "karma-mocha-reporter": "^2.2.5", 76 | "karma-sourcemap-loader": "^0.3.7", 77 | "karma-webpack": "^3.0.5", 78 | "rollup": "^1.1.0", 79 | "rollup-plugin-typescript2": "^0.18.1", 80 | "standard-version": "^5.0.1", 81 | "ts-loader": "^5.3.3", 82 | "typescript": "^3.2.2", 83 | "webpack": "^4.28.4", 84 | "webpack-cli": "^3.2.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | 3 | export default [ 4 | { 5 | input: 'src/aurelia-fetch-client.ts', 6 | output: [ 7 | { 8 | file: 'dist/es2015/aurelia-fetch-client.js', 9 | format: 'esm' 10 | }, 11 | { 12 | file: 'dist/umd-es2015/aurelia-fetch-client.js', 13 | format: 'umd', 14 | name: 'au.fetchClient', 15 | globals: { 16 | 'aurelia-binding': 'au', 17 | 'aurelia-dependency-injection': 'au', 18 | 'aurelia-pal': 'au', 19 | 'aurelia-templating': 'au', 20 | 'aurelia-templating-resources': 'au', 21 | } 22 | } 23 | ], 24 | plugins: [ 25 | typescript({ 26 | cacheRoot: '.rollupcache', 27 | tsconfigOverride: { 28 | compilerOptions: { 29 | removeComments: true 30 | } 31 | } 32 | }) 33 | ] 34 | }, 35 | { 36 | input: 'src/aurelia-fetch-client.ts', 37 | output: { 38 | file: 'dist/es2017/aurelia-fetch-client.js', 39 | format: 'esm' 40 | }, 41 | plugins: [ 42 | typescript({ 43 | cacheRoot: '.rollupcache', 44 | tsconfigOverride: { 45 | compilerOptions: { 46 | target: 'es2017', 47 | removeComments: true 48 | } 49 | } 50 | }) 51 | ] 52 | }, 53 | { 54 | input: 'src/aurelia-fetch-client.ts', 55 | output: [ 56 | { file: 'dist/amd/aurelia-fetch-client.js', format: 'amd', id: 'aurelia-fetch-client' }, 57 | { file: 'dist/commonjs/aurelia-fetch-client.js', format: 'cjs' }, 58 | { file: 'dist/system/aurelia-fetch-client.js', format: 'system' }, 59 | { file: 'dist/native-modules/aurelia-fetch-client.js', format: 'esm' }, 60 | { 61 | file: 'dist/umd/aurelia-fetch-client.js', 62 | format: 'umd', 63 | name: 'au.fetchClient', 64 | globals: { 65 | 'aurelia-binding': 'au', 66 | 'aurelia-dependency-injection': 'au', 67 | 'aurelia-pal': 'au', 68 | 'aurelia-templating': 'au', 69 | 'aurelia-templating-resources': 'au', 70 | } 71 | }, 72 | ], 73 | plugins: [ 74 | typescript({ 75 | cacheRoot: '.rollupcache', 76 | tsconfigOverride: { 77 | compilerOptions: { 78 | target: 'es5', 79 | removeComments: true 80 | } 81 | } 82 | }) 83 | ] 84 | } 85 | ].map(config => { 86 | config.external = [ 87 | 'aurelia-binding', 88 | 'aurelia-dependency-injection', 89 | 'aurelia-pal', 90 | 'aurelia-templating', 91 | 'aurelia-templating-resources' 92 | ]; 93 | return config; 94 | }); 95 | -------------------------------------------------------------------------------- /src/aurelia-fetch-client.ts: -------------------------------------------------------------------------------- 1 | export { Interceptor, RequestInit, RetryConfiguration, ValidInterceptorMethodName } from './interfaces'; 2 | export { json } from './util'; 3 | export { retryStrategy, RetryInterceptor } from './retry-interceptor'; 4 | export { HttpClientConfiguration } from './http-client-configuration'; 5 | export { HttpClient } from './http-client'; 6 | -------------------------------------------------------------------------------- /src/http-client-configuration.ts: -------------------------------------------------------------------------------- 1 | import {RequestInit, Interceptor, RetryConfiguration} from './interfaces'; 2 | import {RetryInterceptor} from './retry-interceptor'; 3 | 4 | /** 5 | * A class for configuring HttpClients. 6 | */ 7 | export class HttpClientConfiguration { 8 | /** 9 | * The base URL to be prepended to each Request's url before sending. 10 | */ 11 | baseUrl: string = ''; 12 | 13 | /** 14 | * Default values to apply to init objects when creating Requests. Note that 15 | * defaults cannot be applied when Request objects are manually created because 16 | * Request provides its own defaults and discards the original init object. 17 | * See also https://developer.mozilla.org/en-US/docs/Web/API/Request/Request 18 | */ 19 | defaults: RequestInit = {}; 20 | 21 | /** 22 | * Interceptors to be added to the HttpClient. 23 | */ 24 | interceptors: Interceptor[] = []; 25 | 26 | /** 27 | * Sets the baseUrl. 28 | * 29 | * @param baseUrl The base URL. 30 | * @returns The chainable instance of this configuration object. 31 | * @chainable 32 | */ 33 | withBaseUrl(baseUrl: string): HttpClientConfiguration { 34 | this.baseUrl = baseUrl; 35 | return this; 36 | } 37 | 38 | /** 39 | * Sets the defaults. 40 | * 41 | * @param defaults The defaults. 42 | * @returns The chainable instance of this configuration object. 43 | * @chainable 44 | */ 45 | withDefaults(defaults: RequestInit): HttpClientConfiguration { 46 | this.defaults = defaults; 47 | return this; 48 | } 49 | 50 | /** 51 | * Adds an interceptor to be run on all requests or responses. 52 | * 53 | * @param interceptor An object with request, requestError, 54 | * response, or responseError methods. request and requestError act as 55 | * resolve and reject handlers for the Request before it is sent. 56 | * response and responseError act as resolve and reject handlers for 57 | * the Response after it has been received. 58 | * @returns The chainable instance of this configuration object. 59 | * @chainable 60 | */ 61 | withInterceptor(interceptor: Interceptor): HttpClientConfiguration { 62 | this.interceptors.push(interceptor); 63 | return this; 64 | } 65 | 66 | /** 67 | * Applies a configuration that addresses common application needs, including 68 | * configuring same-origin credentials, and using rejectErrorResponses. 69 | * @returns The chainable instance of this configuration object. 70 | * @chainable 71 | */ 72 | useStandardConfiguration(): HttpClientConfiguration { 73 | let standardConfig = { credentials: 'same-origin' }; 74 | Object.assign(this.defaults, standardConfig, this.defaults); 75 | return this.rejectErrorResponses(); 76 | } 77 | 78 | /** 79 | * Causes Responses whose status codes fall outside the range 200-299 to reject. 80 | * The fetch API only rejects on network errors or other conditions that prevent 81 | * the request from completing, meaning consumers must inspect Response.ok in the 82 | * Promise continuation to determine if the server responded with a success code. 83 | * This method adds a response interceptor that causes Responses with error codes 84 | * to be rejected, which is common behavior in HTTP client libraries. 85 | * @returns The chainable instance of this configuration object. 86 | * @chainable 87 | */ 88 | rejectErrorResponses(): HttpClientConfiguration { 89 | return this.withInterceptor({ response: rejectOnError }); 90 | } 91 | 92 | withRetry( config?: RetryConfiguration) { 93 | const interceptor: Interceptor = new RetryInterceptor(config); 94 | 95 | return this.withInterceptor(interceptor); 96 | } 97 | } 98 | 99 | function rejectOnError(response) { 100 | if (!response.ok) { 101 | throw response; 102 | } 103 | 104 | return response; 105 | } 106 | -------------------------------------------------------------------------------- /src/http-client.ts: -------------------------------------------------------------------------------- 1 | import { DOM } from 'aurelia-pal'; 2 | import { HttpClientConfiguration } from './http-client-configuration'; 3 | import { Interceptor, RequestInit, ValidInterceptorMethodName } from './interfaces'; 4 | import { RetryInterceptor } from './retry-interceptor'; 5 | 6 | /** 7 | * An HTTP client based on the Fetch API. 8 | */ 9 | export class HttpClient { 10 | /** 11 | * The current number of active requests. 12 | * Requests being processed by interceptors are considered active. 13 | */ 14 | activeRequestCount: number = 0; 15 | 16 | /** 17 | * Indicates whether or not the client is currently making one or more requests. 18 | */ 19 | isRequesting: boolean = false; 20 | 21 | /** 22 | * Indicates whether or not the client has been configured. 23 | */ 24 | isConfigured: boolean = false; 25 | 26 | /** 27 | * The base URL set by the config. 28 | */ 29 | baseUrl: string = ''; 30 | 31 | /** 32 | * The default request init to merge with values specified at request time. 33 | */ 34 | defaults: RequestInit = null; 35 | 36 | /** 37 | * The interceptors to be run during requests. 38 | */ 39 | interceptors: Interceptor[] = []; 40 | 41 | /** 42 | * Creates an instance of HttpClient. 43 | */ 44 | constructor() { 45 | if (typeof fetch === 'undefined') { 46 | // tslint:disable-next-line:max-line-length 47 | throw new Error('HttpClient requires a Fetch API implementation, but the current environment doesn\'t support it. You may need to load a polyfill such as https://github.com/github/fetch'); 48 | } 49 | } 50 | 51 | /** 52 | * Configure this client with default settings to be used by all requests. 53 | * 54 | * @param config A configuration object, or a function that takes a config 55 | * object and configures it. 56 | * @returns The chainable instance of this HttpClient. 57 | * @chainable 58 | */ 59 | configure(config: RequestInit | ((config: HttpClientConfiguration) => void) | HttpClientConfiguration): HttpClient { 60 | let normalizedConfig: HttpClientConfiguration; 61 | 62 | if (typeof config === 'object') { 63 | normalizedConfig = { defaults: config as RequestInit } as HttpClientConfiguration; 64 | } else if (typeof config === 'function') { 65 | normalizedConfig = new HttpClientConfiguration(); 66 | normalizedConfig.baseUrl = this.baseUrl; 67 | normalizedConfig.defaults = Object.assign({}, this.defaults); 68 | normalizedConfig.interceptors = this.interceptors; 69 | 70 | let c = config(normalizedConfig); 71 | if (HttpClientConfiguration.prototype.isPrototypeOf(c as any)) { 72 | normalizedConfig = c as any; 73 | } 74 | } else { 75 | throw new Error('invalid config'); 76 | } 77 | 78 | let defaults = normalizedConfig.defaults; 79 | if (defaults && Headers.prototype.isPrototypeOf(defaults.headers)) { 80 | // Headers instances are not iterable in all browsers. Require a plain 81 | // object here to allow default headers to be merged into request headers. 82 | throw new Error('Default headers must be a plain object.'); 83 | } 84 | 85 | let interceptors = normalizedConfig.interceptors; 86 | 87 | if (interceptors && interceptors.length ) { 88 | // find if there is a RetryInterceptor 89 | if (interceptors.filter(x => RetryInterceptor.prototype.isPrototypeOf(x)).length > 1) { 90 | throw new Error('Only one RetryInterceptor is allowed.'); 91 | } 92 | 93 | const retryInterceptorIndex = interceptors.findIndex( x => RetryInterceptor.prototype.isPrototypeOf(x)); 94 | 95 | if (retryInterceptorIndex >= 0 && retryInterceptorIndex !== interceptors.length - 1) { 96 | throw new Error('The retry interceptor must be the last interceptor defined.'); 97 | } 98 | } 99 | 100 | this.baseUrl = normalizedConfig.baseUrl; 101 | this.defaults = defaults; 102 | this.interceptors = normalizedConfig.interceptors || []; 103 | this.isConfigured = true; 104 | 105 | return this; 106 | } 107 | 108 | /** 109 | * Starts the process of fetching a resource. Default configuration parameters 110 | * will be applied to the Request. The constructed Request will be passed to 111 | * registered request interceptors before being sent. The Response will be passed 112 | * to registered Response interceptors before it is returned. 113 | * 114 | * See also https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 115 | * 116 | * @param input The resource that you wish to fetch. Either a 117 | * Request object, or a string containing the URL of the resource. 118 | * @param init An options object containing settings to be applied to 119 | * the Request. 120 | * @returns A Promise for the Response from the fetch request. 121 | */ 122 | fetch(input: Request|string, init?: RequestInit): Promise { 123 | trackRequestStart(this); 124 | 125 | let request = this.buildRequest(input, init); 126 | return processRequest(request, this.interceptors, this).then(result => { 127 | let response = null; 128 | 129 | if (Response.prototype.isPrototypeOf(result)) { 130 | response = Promise.resolve(result); 131 | } else if (Request.prototype.isPrototypeOf(result)) { 132 | request = result; 133 | response = fetch(result); 134 | } else { 135 | // tslint:disable-next-line:max-line-length 136 | throw new Error(`An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [${result}]`); 137 | } 138 | 139 | return processResponse(response, this.interceptors, request, this); 140 | }) 141 | .then(result => { 142 | if (Request.prototype.isPrototypeOf(result)) { 143 | return this.fetch(result); 144 | } 145 | return result; 146 | }) 147 | .then( 148 | result => { 149 | trackRequestEnd(this); 150 | return result; 151 | }, 152 | error => { 153 | trackRequestEnd(this); 154 | throw error; 155 | } 156 | ); 157 | } 158 | 159 | buildRequest(input: string | Request, init: RequestInit): Request { 160 | let defaults = this.defaults || {}; 161 | let request: Request; 162 | let body: any; 163 | let requestContentType: string; 164 | 165 | let parsedDefaultHeaders = parseHeaderValues(defaults.headers); 166 | if (Request.prototype.isPrototypeOf(input)) { 167 | request = input as Request; 168 | requestContentType = new Headers(request.headers).get('Content-Type'); 169 | } else { 170 | if (!init) { 171 | init = {} as RequestInit; 172 | } 173 | body = init.body; 174 | let bodyObj = body ? { body } : null; 175 | let requestInit = Object.assign({}, defaults, { headers: {} }, init, bodyObj); 176 | requestContentType = new Headers(requestInit.headers).get('Content-Type'); 177 | request = new Request(getRequestUrl(this.baseUrl, input as string), requestInit); 178 | } 179 | if (!requestContentType) { 180 | if (new Headers(parsedDefaultHeaders).has('content-type')) { 181 | request.headers.set('Content-Type', new Headers(parsedDefaultHeaders).get('content-type')); 182 | } else if (body && isJSON(body)) { 183 | request.headers.set('Content-Type', 'application/json'); 184 | } 185 | } 186 | setDefaultHeaders(request.headers, parsedDefaultHeaders); 187 | if (body && Blob.prototype.isPrototypeOf(body) && body.type) { 188 | // work around bug in IE & Edge where the Blob type is ignored in the request 189 | // https://connect.microsoft.com/IE/feedback/details/2136163 190 | request.headers.set('Content-Type', body.type); 191 | } 192 | return request; 193 | } 194 | 195 | /** 196 | * Calls fetch as a GET request. 197 | * 198 | * @param input The resource that you wish to fetch. Either a 199 | * Request object, or a string containing the URL of the resource. 200 | * @param init An options object containing settings to be applied to 201 | * the Request. 202 | * @returns A Promise for the Response from the fetch request. 203 | */ 204 | get(input: Request|string, init?: RequestInit): Promise { 205 | return this.fetch(input, init); 206 | } 207 | 208 | /** 209 | * Calls fetch with request method set to POST. 210 | * 211 | * @param input The resource that you wish to fetch. Either a 212 | * Request object, or a string containing the URL of the resource. 213 | * @param body The body of the request. 214 | * @param init An options object containing settings to be applied to 215 | * the Request. 216 | * @returns A Promise for the Response from the fetch request. 217 | */ 218 | post(input: Request|string, body?: any, init?: RequestInit): Promise { 219 | return callFetch(this, input, body, init, 'POST'); 220 | } 221 | 222 | /** 223 | * Calls fetch with request method set to PUT. 224 | * 225 | * @param input The resource that you wish to fetch. Either a 226 | * Request object, or a string containing the URL of the resource. 227 | * @param body The body of the request. 228 | * @param init An options object containing settings to be applied to 229 | * the Request. 230 | * @returns A Promise for the Response from the fetch request. 231 | */ 232 | put(input: Request|string, body?: any, init?: RequestInit): Promise { 233 | return callFetch(this, input, body, init, 'PUT'); 234 | } 235 | 236 | /** 237 | * Calls fetch with request method set to PATCH. 238 | * 239 | * @param input The resource that you wish to fetch. Either a 240 | * Request object, or a string containing the URL of the resource. 241 | * @param body The body of the request. 242 | * @param init An options object containing settings to be applied to 243 | * the Request. 244 | * @returns A Promise for the Response from the fetch request. 245 | */ 246 | patch(input: Request|string, body?: any, init?: RequestInit): Promise { 247 | return callFetch(this, input, body, init, 'PATCH'); 248 | } 249 | 250 | /** 251 | * Calls fetch with request method set to DELETE. 252 | * 253 | * @param input The resource that you wish to fetch. Either a 254 | * Request object, or a string containing the URL of the resource. 255 | * @param body The body of the request. 256 | * @param init An options object containing settings to be applied to 257 | * the Request. 258 | * @returns A Promise for the Response from the fetch request. 259 | */ 260 | delete(input: Request|string, body?: any, init?: RequestInit): Promise { 261 | return callFetch(this, input, body, init, 'DELETE'); 262 | } 263 | } 264 | 265 | const absoluteUrlRegexp = /^([a-z][a-z0-9+\-.]*:)?\/\//i; 266 | 267 | function trackRequestStart(client: HttpClient) { 268 | client.isRequesting = !!(++client.activeRequestCount); 269 | if (client.isRequesting) { 270 | let evt = DOM.createCustomEvent('aurelia-fetch-client-request-started', { bubbles: true, cancelable: true }); 271 | setTimeout(() => DOM.dispatchEvent(evt), 1); 272 | } 273 | } 274 | 275 | function trackRequestEnd(client: HttpClient) { 276 | client.isRequesting = !!(--client.activeRequestCount); 277 | if (!client.isRequesting) { 278 | let evt = DOM.createCustomEvent('aurelia-fetch-client-requests-drained', { bubbles: true, cancelable: true }); 279 | setTimeout(() => DOM.dispatchEvent(evt), 1); 280 | } 281 | } 282 | 283 | function parseHeaderValues(headers: object) { 284 | let parsedHeaders = {}; 285 | for (let name in headers || {}) { 286 | if (headers.hasOwnProperty(name)) { 287 | parsedHeaders[name] = (typeof headers[name] === 'function') ? headers[name]() : headers[name]; 288 | } 289 | } 290 | return parsedHeaders; 291 | } 292 | 293 | function getRequestUrl(baseUrl: string, url: string) { 294 | if (absoluteUrlRegexp.test(url)) { 295 | return url; 296 | } 297 | 298 | return (baseUrl || '') + url; 299 | } 300 | 301 | function setDefaultHeaders(headers: Headers, defaultHeaders: object) { 302 | for (let name in defaultHeaders || {}) { 303 | if (defaultHeaders.hasOwnProperty(name) && !headers.has(name)) { 304 | headers.set(name, defaultHeaders[name]); 305 | } 306 | } 307 | } 308 | 309 | function processRequest(request: Request, interceptors: Interceptor[], http: HttpClient) { 310 | return applyInterceptors(request, interceptors, 'request', 'requestError', http); 311 | } 312 | 313 | function processResponse(response: Promise, interceptors: Interceptor[], request: Request, http: HttpClient) { 314 | return applyInterceptors(response, interceptors, 'response', 'responseError', request, http); 315 | } 316 | 317 | // tslint:disable-next-line:max-line-length 318 | function applyInterceptors(input: Request | Promise, interceptors: Interceptor[], successName: ValidInterceptorMethodName, errorName: ValidInterceptorMethodName, ...interceptorArgs: any[]) { 319 | return (interceptors || []) 320 | .reduce((chain, interceptor) => { 321 | let successHandler = interceptor[successName]; 322 | let errorHandler = interceptor[errorName]; 323 | 324 | return chain.then( 325 | successHandler && (value => successHandler.call(interceptor, value, ...interceptorArgs)) || identity, 326 | errorHandler && (reason => errorHandler.call(interceptor, reason, ...interceptorArgs)) || thrower); 327 | }, Promise.resolve(input)); 328 | } 329 | 330 | function isJSON(str) { 331 | try { 332 | JSON.parse(str); 333 | } catch (err) { 334 | return false; 335 | } 336 | 337 | return true; 338 | } 339 | 340 | function identity(x: any): any { 341 | return x; 342 | } 343 | 344 | function thrower(x: any): never { 345 | throw x; 346 | } 347 | 348 | function callFetch(client: HttpClient, input: string | Request, body: any, init: RequestInit, method: string) { 349 | if (!init) { 350 | init = {}; 351 | } 352 | init.method = method; 353 | if (body) { 354 | init.body = body; 355 | } 356 | return client.fetch(input, init); 357 | } 358 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from './http-client'; 2 | 3 | /** 4 | * Interceptors can process requests before they are sent, and responses 5 | * before they are returned to callers. 6 | */ 7 | export interface Interceptor { 8 | /** 9 | * Called with the request before it is sent. Request interceptors can modify and 10 | * return the request, or return a new one to be sent. If desired, the interceptor 11 | * may return a Response in order to short-circuit the HTTP request itself. 12 | * 13 | * @param request The request to be sent. 14 | * @returns The existing request, a new request or a response; or a Promise for any of these. 15 | */ 16 | request?: (request: Request) => Request|Response|Promise; 17 | 18 | /** 19 | * Handles errors generated by previous request interceptors. This function acts 20 | * as a Promise rejection handler. It may rethrow the error to propagate the 21 | * failure, or return a new Request or Response to recover. 22 | * 23 | * @param error The rejection value from the previous interceptor. 24 | * @returns The existing request, a new request or a response; or a Promise for any of these. 25 | */ 26 | requestError?: (error: any) => Request|Response|Promise; 27 | 28 | /** 29 | * Called with the response after it is received. Response interceptors can modify 30 | * and return the Response, or create a new one to be returned to the caller. 31 | * 32 | * @param response The response. 33 | * @returns The response; or a Promise for one. 34 | */ 35 | response?: (response: Response, request?: Request) => Response|Promise; 36 | 37 | /** 38 | * Handles fetch errors and errors generated by previous interceptors. This 39 | * function acts as a Promise rejection handler. It may rethrow the error 40 | * to propagate the failure, or return a new Response to recover. 41 | * 42 | * @param error The rejection value from the fetch request or from a 43 | * previous interceptor. 44 | * @returns The response; or a Promise for one. 45 | */ 46 | responseError?: (error: any, request?: Request, httpClient?: HttpClient) => Response|Promise; 47 | } 48 | 49 | export type ValidInterceptorMethodName = keyof Interceptor; 50 | 51 | /** 52 | * The init object used to initialize a fetch Request. 53 | * See https://developer.mozilla.org/en-US/docs/Web/API/Request/Request 54 | */ 55 | export interface RequestInit { 56 | /** 57 | * The request method, e.g., GET, POST. 58 | */ 59 | method?: string; 60 | 61 | /** 62 | * Any headers you want to add to your request, contained within a Headers object or an object literal with ByteString values. 63 | */ 64 | headers?: Headers|Object; 65 | 66 | /** 67 | * Any body that you want to add to your request: this can be a Blob, BufferSource, FormData, 68 | * URLSearchParams, ReadableStream, or USVString object. 69 | * 70 | * Note that a request using the GET or HEAD method cannot have a body. 71 | */ 72 | body?: Blob|BufferSource|FormData|URLSearchParams|ReadableStream|string|null; 73 | 74 | /** 75 | * The mode you want to use for the request, e.g., cors, no-cors, same-origin, or navigate. 76 | * The default is cors. 77 | * 78 | * In Chrome the default is no-cors before Chrome 47 and same-origin starting with Chrome 47. 79 | */ 80 | mode?: string; 81 | 82 | /** 83 | * The request credentials you want to use for the request: omit, same-origin, or include. 84 | * The default is omit. 85 | * 86 | * In Chrome the default is same-origin before Chrome 47 and include starting with Chrome 47. 87 | */ 88 | credentials?: string; 89 | 90 | /** 91 | * The cache mode you want to use for the request: default, no-store, reload, no-cache, or force-cache. 92 | */ 93 | cache?: string; 94 | 95 | /** 96 | * The redirect mode to use: follow, error, or manual. 97 | * 98 | * In Chrome the default is follow before Chrome 47 and manual starting with Chrome 47. 99 | */ 100 | redirect?: string; 101 | 102 | /** 103 | * A USVString specifying no-referrer, client, or a URL. The default is client. 104 | */ 105 | referrer?: string; 106 | 107 | /** 108 | * Contains the subresource integrity value of the request (e.g., sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=). 109 | */ 110 | integrity?: string; 111 | 112 | /** 113 | * An AbortSignal to set request’s signal. 114 | */ 115 | signal?: AbortSignal | null; 116 | } 117 | 118 | export interface RetryConfiguration { 119 | maxRetries: number; 120 | interval?: number; 121 | strategy?: number| ((retryCount: number) => number); 122 | minRandomInterval?: number; 123 | maxRandomInterval?: number; 124 | counter?: number; 125 | requestClone?: Request; 126 | doRetry?: (response: Response, request: Request) => boolean | Promise; 127 | beforeRetry?: (request: Request, client: HttpClient) => Request | Promise; 128 | } 129 | -------------------------------------------------------------------------------- /src/retry-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { PLATFORM } from 'aurelia-pal'; 2 | import { Interceptor, RetryConfiguration } from './interfaces'; 3 | import { HttpClient } from './http-client'; 4 | 5 | export const retryStrategy: { 6 | fixed: 0; 7 | incremental: 1; 8 | exponential: 2; 9 | random: 3; 10 | } = { 11 | fixed: 0, 12 | incremental: 1, 13 | exponential: 2, 14 | random: 3 15 | }; 16 | 17 | const defaultRetryConfig: RetryConfiguration = { 18 | maxRetries: 3, 19 | interval: 1000, 20 | strategy: retryStrategy.fixed 21 | }; 22 | 23 | export class RetryInterceptor implements Interceptor { 24 | retryConfig: RetryConfiguration; 25 | 26 | constructor(retryConfig?: RetryConfiguration) { 27 | this.retryConfig = Object.assign({}, defaultRetryConfig, retryConfig || {}); 28 | 29 | if (this.retryConfig.strategy === retryStrategy.exponential && 30 | this.retryConfig.interval <= 1000) { 31 | throw new Error('An interval less than or equal to 1 second is not allowed when using the exponential retry strategy'); 32 | } 33 | } 34 | 35 | request(request: Request): Request { 36 | let $r = request as Request & { retryConfig?: RetryConfiguration }; 37 | if (!$r.retryConfig) { 38 | $r.retryConfig = Object.assign({}, this.retryConfig); 39 | $r.retryConfig.counter = 0; 40 | } 41 | 42 | // do this on every request 43 | $r.retryConfig.requestClone = request.clone(); 44 | 45 | return request; 46 | } 47 | 48 | response(response: Response, request?: Request): Response { 49 | // retry was successful, so clean up after ourselves 50 | delete (request as any).retryConfig; 51 | return response; 52 | } 53 | 54 | responseError(error: Response, request?: Request, httpClient?: HttpClient) { 55 | const { retryConfig } = request as Request & { retryConfig: RetryConfiguration }; 56 | const { requestClone } = retryConfig; 57 | return Promise.resolve().then(() => { 58 | if (retryConfig.counter < retryConfig.maxRetries) { 59 | const result = retryConfig.doRetry ? retryConfig.doRetry(error, request) : true; 60 | 61 | return Promise.resolve(result).then(doRetry => { 62 | if (doRetry) { 63 | retryConfig.counter++; 64 | return new Promise(resolve => PLATFORM.global.setTimeout(resolve, calculateDelay(retryConfig) || 0)) 65 | .then(() => { 66 | let newRequest = requestClone.clone(); 67 | if (typeof (retryConfig.beforeRetry) === 'function') { 68 | return retryConfig.beforeRetry(newRequest, httpClient); 69 | } 70 | return newRequest; 71 | }) 72 | .then(newRequest => { 73 | return httpClient.fetch(Object.assign(newRequest, { retryConfig })); 74 | }); 75 | } 76 | 77 | // no more retries, so clean up 78 | delete (request as any).retryConfig; 79 | throw error; 80 | }); 81 | } 82 | // no more retries, so clean up 83 | delete (request as any).retryConfig; 84 | throw error; 85 | }); 86 | } 87 | } 88 | 89 | function calculateDelay(retryConfig: RetryConfiguration) { 90 | const { interval, strategy, minRandomInterval, maxRandomInterval, counter } = retryConfig; 91 | 92 | if (typeof (strategy) === 'function') { 93 | return (retryConfig.strategy as Function)(counter); 94 | } 95 | 96 | switch (strategy) { 97 | case (retryStrategy.fixed): 98 | return retryStrategies[retryStrategy.fixed](interval); 99 | case (retryStrategy.incremental): 100 | return retryStrategies[retryStrategy.incremental](counter, interval); 101 | case (retryStrategy.exponential): 102 | return retryStrategies[retryStrategy.exponential](counter, interval); 103 | case (retryStrategy.random): 104 | return retryStrategies[retryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); 105 | default: 106 | throw new Error('Unrecognized retry strategy'); 107 | } 108 | } 109 | 110 | const retryStrategies = [ 111 | // fixed 112 | interval => interval, 113 | 114 | // incremental 115 | (retryCount, interval) => interval * retryCount, 116 | 117 | // exponential 118 | (retryCount, interval) => retryCount === 1 ? interval : Math.pow(interval, retryCount) / 1000, 119 | 120 | // random 121 | (retryCount, interval, minRandomInterval = 0, maxRandomInterval = 60000) => { 122 | return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; 123 | } 124 | ] as [ 125 | (interval: number) => number, 126 | (retryCount: number, interval: number) => number, 127 | (retryCount: number, interval: number) => number, 128 | (retryCount: number, interval: number, minRandomInterval?: number, maxRandomInterval?: number) => number 129 | ]; 130 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Serialize an object to JSON. Useful for easily creating JSON fetch request bodies. 3 | * 4 | * @param body The object to be serialized to JSON. 5 | * @param replacer The JSON.stringify replacer used when serializing. 6 | * @returns A JSON string. 7 | */ 8 | export function json(body: any, replacer?: any): string { 9 | return JSON.stringify((body !== undefined ? body : {}), replacer); 10 | } 11 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import 'aurelia-polyfills'; 2 | import {initialize} from 'aurelia-pal-browser'; 3 | 4 | initialize(); 5 | -------------------------------------------------------------------------------- /test/util.spec.ts: -------------------------------------------------------------------------------- 1 | import {json} from '../src/util'; 2 | 3 | describe('util', () => { 4 | describe('json', () => { 5 | it('returns valid JSON', () => { 6 | let data = { test: 'data' }; 7 | let result = JSON.parse(json(data)); 8 | expect(data).toEqual(result); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": false, 7 | "moduleResolution": "node", 8 | "stripInternal": true, 9 | "declaration": false, 10 | "lib": ["es2015", "dom"] 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "dist", 15 | "build", 16 | "doc", 17 | "test", 18 | "config.js", 19 | "gulpfile.js", 20 | "karma.conf.js" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": true, 9 | "eofline": true, 10 | "encoding": true, 11 | "forin": false, 12 | "indent": [ 13 | true, 14 | "spaces" 15 | ], 16 | "label-position": true, 17 | "max-line-length": [ 18 | true, 19 | 160 20 | ], 21 | "no-consecutive-blank-lines": [ 22 | true, 23 | 3 24 | ], 25 | "member-access": false, 26 | "member-ordering": false, 27 | "no-arg": true, 28 | "no-bitwise": true, 29 | "no-console": [ 30 | true, 31 | "debug", 32 | "info", 33 | "time", 34 | "timeEnd", 35 | "trace" 36 | ], 37 | "no-construct": true, 38 | "no-debugger": true, 39 | "no-duplicate-variable": true, 40 | "no-empty": true, 41 | "no-eval": true, 42 | "no-inferrable-types": false, 43 | "no-shadowed-variable": false, 44 | "newline-before-return": false, 45 | "no-string-literal": false, 46 | "no-switch-case-fall-through": true, 47 | "no-trailing-whitespace": true, 48 | "no-unused-expression": true, 49 | "no-use-before-declare": false, 50 | "no-var-keyword": true, 51 | "object-literal-sort-keys": false, 52 | "one-line": [ 53 | true, 54 | "check-open-brace", 55 | "check-catch", 56 | "check-else", 57 | "check-finally", 58 | "check-whitespace" 59 | ], 60 | "quotemark": [ 61 | true, 62 | "single", 63 | "avoid-escape" 64 | ], 65 | "radix": true, 66 | "semicolon": [ 67 | true 68 | ], 69 | "trailing-comma": [ 70 | true, 71 | { 72 | "singleline": "never", 73 | "multiline": "never" 74 | } 75 | ], 76 | "triple-equals": [ 77 | true, 78 | "allow-null-check" 79 | ], 80 | "typedef-whitespace": [ 81 | true, 82 | { 83 | "call-signature": "nospace", 84 | "index-signature": "nospace", 85 | "parameter": "nospace", 86 | "property-declaration": "nospace", 87 | "variable-declaration": "nospace" 88 | } 89 | ], 90 | "variable-name": false, 91 | "no-misused-new": true, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | "interface-name": false, 101 | "prefer-const": false 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurelia-fetch-client", 3 | "main": "dist/aurelia-fetch-client.d.ts" 4 | } 5 | --------------------------------------------------------------------------------